# HG changeset patch
# User drewp@bigasterisk.com
# Date 1653979567 25200
# Node ID ddbd0fca89ff8797d0243fb9ad04fe1a4d83eac7
# Parent 8516a39eedc92d1aae9e3d3484165c9abbce2b81
file moves for effectSequencer
diff -r 8516a39eedc9 -r ddbd0fca89ff bin/effectSequencer
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/bin/effectSequencer Mon May 30 23:46:07 2022 -0700
@@ -0,0 +1,5 @@
+#!/bin/sh
+pnpx vite -c light9/effect/sequencer/web/vite.config.ts &
+pdm run uvicorn light9.effect.sequencer.service:app --host 0.0.0.0 --port 8213 --no-access-log
+wait
+
diff -r 8516a39eedc9 -r ddbd0fca89ff bin/effectsequencer
--- a/bin/effectsequencer Mon May 30 23:45:28 2022 -0700
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,84 +0,0 @@
-#!bin/python
-"""
-plays back effect notes from the timeline
-"""
-
-from run_local import log
-from twisted.internet import reactor
-from light9.metrics import metrics, metricsRoute
-from rdfdb.syncedgraph import SyncedGraph
-from light9 import networking, showconfig
-import optparse, sys, logging
-import cyclone.web
-from rdflib import URIRef
-from light9.effect.sequencer import Sequencer, Updates
-from light9.collector.collector_client import sendToCollector
-
-from light9 import clientsession
-
-
-class App(object):
-
- def __init__(self, show, session):
- self.show = show
- self.session = session
-
- self.graph = SyncedGraph(networking.rdfdb.url, "effectSequencer")
- self.graph.initiallySynced.addCallback(self.launch)
-
- def launch(self, *args):
- self.seq = Sequencer(
- self.graph,
- lambda settings: sendToCollector(
- 'effectSequencer',
- self.session,
- settings,
- # This seems to be safe here (and lets us get from
- # 20fpx to 40fpx), even though it leads to big stalls
- # if I use it on KC.
- useZmq=True))
-
- self.cycloneApp = cyclone.web.Application(handlers=[
- (r'/()', cyclone.web.StaticFileHandler, {
- "path": "light9/effect/",
- "default_filename": "sequencer.html"
- }),
- (r'/updates', Updates),
- metricsRoute(),
- ],
- debug=True,
- seq=self.seq,
- graph=self.graph,
- # stats=self.stats
- )
- reactor.listenTCP(networking.effectSequencer.port, self.cycloneApp)
- log.info("listening on %s" % networking.effectSequencer.port)
-
-
-if __name__ == "__main__":
- parser = optparse.OptionParser()
- parser.add_option(
- '--show',
- help='show URI, like http://light9.bigasterisk.com/show/dance2008',
- default=showconfig.showUri())
- parser.add_option("-v",
- "--verbose",
- action="store_true",
- help="logging.DEBUG")
- parser.add_option("--twistedlog",
- action="store_true",
- help="twisted logging")
- clientsession.add_option(parser)
- (options, args) = parser.parse_args()
- log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
-
- if not options.show:
- raise ValueError("missing --show http://...")
-
- session = clientsession.getUri('effectSequencer', options)
-
- app = App(URIRef(options.show), session)
- if options.twistedlog:
- from twisted.python import log as twlog
- twlog.startLogging(sys.stderr)
- reactor.run()
diff -r 8516a39eedc9 -r ddbd0fca89ff light9/effect/sequencer.html
--- a/light9/effect/sequencer.html Mon May 30 23:45:28 2022 -0700
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,149 +0,0 @@
-
-
-
- effect sequencer
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Song
-
-
- t={{report.roundT}}
-
- Notes
-
-
-
- Note |
- Effect class |
- Effect settings |
- Devices affected |
-
-
-
-
-
-
- |
-
-
- |
-
-
-
-
- :
- {{item.value}}
-
-
-
- |
-
- {{item.devicesAffected}}
- |
-
-
-
-
-
-
-
-
-
-
-
-
diff -r 8516a39eedc9 -r ddbd0fca89ff light9/effect/sequencer.py
--- a/light9/effect/sequencer.py Mon May 30 23:45:28 2022 -0700
+++ /dev/null Thu Jan 01 00:00:00 1970 +0000
@@ -1,288 +0,0 @@
-'''
-copies from effectloop.py, which this should replace
-'''
-
-from louie import dispatcher
-from rdflib import URIRef
-from twisted.internet import reactor
-from twisted.internet import defer
-from twisted.internet.defer import Deferred, inlineCallbacks
-from twisted.internet.inotify import INotify
-from twisted.python.filepath import FilePath
-import cyclone.sse
-import logging, bisect, time
-import traceback
-from decimal import Decimal
-from typing import Any, Callable, Dict, List, Tuple, cast, Union
-
-from light9.ascoltami.musictime_client import MusicTime
-from light9.effect import effecteval
-from light9.effect.settings import DeviceSettings
-from light9.effect.simple_outputs import SimpleOutputs
-from light9.namespaces import L9, RDF
-from light9.newtypes import DeviceUri, DeviceAttr, NoteUri, Curve, Song
-from rdfdb.syncedgraph import SyncedGraph
-from light9.metrics import metrics
-
-import imp
-
-log = logging.getLogger('sequencer')
-
-
-def pyType(n):
- ret = n.toPython()
- if isinstance(ret, Decimal):
- return float(ret)
- return ret
-
-
-class Note(object):
-
- def __init__(self, graph: SyncedGraph, uri: NoteUri, effectevalModule,
- simpleOutputs):
- g = self.graph = graph
- self.uri = uri
- 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 = settingValues[L9['effectAttr']]
- self.baseEffectSettings[ea] = pyType(settingValues[L9['value']])
-
- def floatVal(s, p):
- return float(g.value(s, p).toPython())
-
- originTime = floatVal(uri, L9['originTime'])
- self.points: List[Tuple[float, float]] = []
- for curve in g.objects(uri, L9['curve']):
- self.points.extend(
- self.getCurvePoints(curve, L9['strength'], originTime))
- self.points.sort()
-
- def getCurvePoints(self, curve: Curve, attr,
- originTime: float) -> List[Tuple[float, float]]:
- points = []
- po = list(self.graph.predicate_objects(curve))
- if dict(po).get(L9['attr'], None) != attr:
- return []
- for point in [row[1] for row in po if row[0] == L9['point']]:
- po2 = dict(self.graph.predicate_objects(point))
- points.append(
- (originTime + float(po2[L9['time']]), float(po2[L9['value']])))
- return points
-
- def activeAt(self, t: float) -> bool:
- return self.points[0][0] <= t <= self.points[-1][0]
-
- def evalCurve(self, t: float) -> float:
- i = bisect.bisect_left(self.points, (t, None)) - 1
-
- if i == -1:
- return self.points[0][1]
- if self.points[i][0] > t:
- return self.points[i][1]
- if i >= len(self.points) - 1:
- return self.points[i][1]
-
- p1, p2 = self.points[i], self.points[i + 1]
- frac = (t - p1[0]) / (p2[0] - p1[0])
- y = p1[1] + (p2[1] - p1[1]) * frac
- return y
-
- def outputSettings(
- self,
- t: float) -> Tuple[List[Tuple[DeviceUri, DeviceAttr, float]], Dict]:
- """
- list of (device, attr, value), and a report for web
- """
- report = {
- 'note': str(self.uri),
- 'effectClass': self.effectEval.effect,
- }
- effectSettings: Dict[DeviceAttr, Union[float, str]] = dict(
- (DeviceAttr(da), v) for da, v in self.baseEffectSettings.items())
- effectSettings[L9['strength']] = self.evalCurve(t)
-
- def prettyFormat(x: Union[float, str]):
- if isinstance(x, float):
- return round(x, 4)
- return x
-
- report['effectSettings'] = dict(
- (str(k), prettyFormat(v))
- for k, v in sorted(effectSettings.items()))
- report['nonZero'] = cast(float, effectSettings[L9['strength']]) > 0
- out, evalReport = self.effectEval.outputFromEffect(
- list(effectSettings.items()),
- songTime=t,
- # note: not using origin here since it's going away
- noteTime=t - self.points[0][0])
- report['devicesAffected'] = len(out.devices())
- return out, report
-
-
-class CodeWatcher(object):
-
- 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)
-
-
-class Sequencer(object):
-
- def __init__(self,
- graph: SyncedGraph,
- sendToCollector: Callable[[DeviceSettings], Deferred],
- 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, pollCurvecalc=False)
-
- self.recentUpdateTimes: List[float] = []
- self.lastStatLog = 0.0
- self._compileGraphCall = None
- self.notes: Dict[Song, List[Note]] = {} # song: [notes]
- self.simpleOutputs = SimpleOutputs(self.graph)
- self.graph.addHandler(self.compileGraph)
- self.lastLoopSucceeded = False
-
- self.codeWatcher = CodeWatcher(onChange=self.onCodeChange)
- self.updateLoop()
-
- def onCodeChange(self):
- log.debug('seq.onCodeChange')
- self.graph.addHandler(self.compileGraph)
- #self.updateLoop()
-
- @metrics('compile_graph').time()
- def compileGraph(self) -> None:
- """rebuild our data from the graph"""
- for song in self.graph.subjects(RDF.type, L9['Song']):
-
- def compileSong(song: Song = cast(Song, song)) -> None:
- self.compileSong(song)
-
- self.graph.addHandler(compileSong)
-
- @metrics('compile_song').time()
- def compileSong(self, song: Song) -> None:
- anyErrors = False
- self.notes[song] = []
- for note in self.graph.objects(song, L9['note']):
- try:
- n = Note(self.graph, NoteUri(note), effecteval,
- self.simpleOutputs)
- except Exception:
- log.warn(f"failed to build Note {note} - skipping")
- anyErrors = True
- continue
- self.notes[song].append(n)
- if not anyErrors:
- log.info('built all notes')
-
- @inlineCallbacks
- def updateLoop(self) -> None:
- frameStart = time.time()
- try:
- sec = yield self.update()
- except Exception as e:
- self.lastLoopSucceeded = False
- traceback.print_exc()
- log.warn('updateLoop: %r', e)
- reactor.callLater(1, self.updateLoop)
- else:
- took = time.time() - frameStart
- metrics('update_loop_latency').observe(took)
-
- if not self.lastLoopSucceeded:
- log.info('Sequencer.update is working')
- self.lastLoopSucceeded = True
-
- delay = max(0, 1 / self.fps - took)
- reactor.callLater(delay, self.updateLoop)
-
- @metrics('update_call').time()
- @inlineCallbacks
- def update(self) -> Deferred:
-
- with metrics('update_s0_getMusic').time():
- musicState = self.music.getLatest()
- if not musicState.get('song') or not isinstance(
- musicState.get('t'), float):
- return defer.succeed(0.0)
- song = Song(URIRef(musicState['song']))
- dispatcher.send('state',
- update={
- 'song': str(song),
- 't': musicState['t']
- })
-
- with metrics('update_s1_eval').time():
- settings = []
- songNotes = sorted(self.notes.get(song, []), key=lambda n: n.uri)
- noteReports = []
- for note in songNotes:
- try:
- s, report = note.outputSettings(musicState['t'])
- except Exception:
- traceback.print_exc()
- raise
- noteReports.append(report)
- settings.append(s)
- devSettings = DeviceSettings.fromList(self.graph, settings)
-
- dispatcher.send('state', update={'songNotes': noteReports})
-
- with metrics('update_s3_send').time(): # our measurement
- sendSecs = yield self.sendToCollector(devSettings)
-
- # sendToCollector's own measurement.
- # (sometimes it's None, not sure why, and neither is mypy)
- #if isinstance(sendSecs, float):
- # metrics('update_s3_send_client').observe(sendSecs)
-
-
-class Updates(cyclone.sse.SSEHandler):
-
- def __init__(self, application, request, **kwargs) -> None:
- cyclone.sse.SSEHandler.__init__(self, application, request, **kwargs)
- self.state: Dict = {}
- dispatcher.connect(self.updateState, 'state')
- self.numConnected = 0
-
- def updateState(self, update: Dict):
- self.state.update(update)
-
- def bind(self) -> None:
- self.numConnected += 1
-
- if self.numConnected == 1:
- self.loop()
-
- def loop(self) -> None:
- if self.numConnected == 0:
- return
- self.sendEvent(self.state)
- reactor.callLater(.1, self.loop)
-
- def unbind(self) -> None:
- self.numConnected -= 1
diff -r 8516a39eedc9 -r ddbd0fca89ff light9/effect/sequencer/__init__.py
diff -r 8516a39eedc9 -r ddbd0fca89ff light9/effect/sequencer/sequencer.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/effect/sequencer/sequencer.py Mon May 30 23:46:07 2022 -0700
@@ -0,0 +1,288 @@
+'''
+copies from effectloop.py, which this should replace
+'''
+
+from louie import dispatcher
+from rdflib import URIRef
+from twisted.internet import reactor
+from twisted.internet import defer
+from twisted.internet.defer import Deferred, inlineCallbacks
+from twisted.internet.inotify import INotify
+from twisted.python.filepath import FilePath
+import cyclone.sse
+import logging, bisect, time
+import traceback
+from decimal import Decimal
+from typing import Any, Callable, Dict, List, Tuple, cast, Union
+
+from light9.ascoltami.musictime_client import MusicTime
+from light9.effect import effecteval
+from light9.effect.settings import DeviceSettings
+from light9.effect.simple_outputs import SimpleOutputs
+from light9.namespaces import L9, RDF
+from light9.newtypes import DeviceUri, DeviceAttr, NoteUri, Curve, Song
+from rdfdb.syncedgraph import SyncedGraph
+from light9.metrics import metrics
+
+import imp
+
+log = logging.getLogger('sequencer')
+
+
+def pyType(n):
+ ret = n.toPython()
+ if isinstance(ret, Decimal):
+ return float(ret)
+ return ret
+
+
+class Note(object):
+
+ def __init__(self, graph: SyncedGraph, uri: NoteUri, effectevalModule,
+ simpleOutputs):
+ g = self.graph = graph
+ self.uri = uri
+ 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 = settingValues[L9['effectAttr']]
+ self.baseEffectSettings[ea] = pyType(settingValues[L9['value']])
+
+ def floatVal(s, p):
+ return float(g.value(s, p).toPython())
+
+ originTime = floatVal(uri, L9['originTime'])
+ self.points: List[Tuple[float, float]] = []
+ for curve in g.objects(uri, L9['curve']):
+ self.points.extend(
+ self.getCurvePoints(curve, L9['strength'], originTime))
+ self.points.sort()
+
+ def getCurvePoints(self, curve: Curve, attr,
+ originTime: float) -> List[Tuple[float, float]]:
+ points = []
+ po = list(self.graph.predicate_objects(curve))
+ if dict(po).get(L9['attr'], None) != attr:
+ return []
+ for point in [row[1] for row in po if row[0] == L9['point']]:
+ po2 = dict(self.graph.predicate_objects(point))
+ points.append(
+ (originTime + float(po2[L9['time']]), float(po2[L9['value']])))
+ return points
+
+ def activeAt(self, t: float) -> bool:
+ return self.points[0][0] <= t <= self.points[-1][0]
+
+ def evalCurve(self, t: float) -> float:
+ i = bisect.bisect_left(self.points, (t, None)) - 1
+
+ if i == -1:
+ return self.points[0][1]
+ if self.points[i][0] > t:
+ return self.points[i][1]
+ if i >= len(self.points) - 1:
+ return self.points[i][1]
+
+ p1, p2 = self.points[i], self.points[i + 1]
+ frac = (t - p1[0]) / (p2[0] - p1[0])
+ y = p1[1] + (p2[1] - p1[1]) * frac
+ return y
+
+ def outputSettings(
+ self,
+ t: float) -> Tuple[List[Tuple[DeviceUri, DeviceAttr, float]], Dict]:
+ """
+ list of (device, attr, value), and a report for web
+ """
+ report = {
+ 'note': str(self.uri),
+ 'effectClass': self.effectEval.effect,
+ }
+ effectSettings: Dict[DeviceAttr, Union[float, str]] = dict(
+ (DeviceAttr(da), v) for da, v in self.baseEffectSettings.items())
+ effectSettings[L9['strength']] = self.evalCurve(t)
+
+ def prettyFormat(x: Union[float, str]):
+ if isinstance(x, float):
+ return round(x, 4)
+ return x
+
+ report['effectSettings'] = dict(
+ (str(k), prettyFormat(v))
+ for k, v in sorted(effectSettings.items()))
+ report['nonZero'] = cast(float, effectSettings[L9['strength']]) > 0
+ out, evalReport = self.effectEval.outputFromEffect(
+ list(effectSettings.items()),
+ songTime=t,
+ # note: not using origin here since it's going away
+ noteTime=t - self.points[0][0])
+ report['devicesAffected'] = len(out.devices())
+ return out, report
+
+
+class CodeWatcher(object):
+
+ 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)
+
+
+class Sequencer(object):
+
+ def __init__(self,
+ graph: SyncedGraph,
+ sendToCollector: Callable[[DeviceSettings], Deferred],
+ 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, pollCurvecalc=False)
+
+ self.recentUpdateTimes: List[float] = []
+ self.lastStatLog = 0.0
+ self._compileGraphCall = None
+ self.notes: Dict[Song, List[Note]] = {} # song: [notes]
+ self.simpleOutputs = SimpleOutputs(self.graph)
+ self.graph.addHandler(self.compileGraph)
+ self.lastLoopSucceeded = False
+
+ self.codeWatcher = CodeWatcher(onChange=self.onCodeChange)
+ self.updateLoop()
+
+ def onCodeChange(self):
+ log.debug('seq.onCodeChange')
+ self.graph.addHandler(self.compileGraph)
+ #self.updateLoop()
+
+ @metrics('compile_graph').time()
+ def compileGraph(self) -> None:
+ """rebuild our data from the graph"""
+ for song in self.graph.subjects(RDF.type, L9['Song']):
+
+ def compileSong(song: Song = cast(Song, song)) -> None:
+ self.compileSong(song)
+
+ self.graph.addHandler(compileSong)
+
+ @metrics('compile_song').time()
+ def compileSong(self, song: Song) -> None:
+ anyErrors = False
+ self.notes[song] = []
+ for note in self.graph.objects(song, L9['note']):
+ try:
+ n = Note(self.graph, NoteUri(note), effecteval,
+ self.simpleOutputs)
+ except Exception:
+ log.warn(f"failed to build Note {note} - skipping")
+ anyErrors = True
+ continue
+ self.notes[song].append(n)
+ if not anyErrors:
+ log.info('built all notes')
+
+ @inlineCallbacks
+ def updateLoop(self) -> None:
+ frameStart = time.time()
+ try:
+ sec = yield self.update()
+ except Exception as e:
+ self.lastLoopSucceeded = False
+ traceback.print_exc()
+ log.warn('updateLoop: %r', e)
+ reactor.callLater(1, self.updateLoop)
+ else:
+ took = time.time() - frameStart
+ metrics('update_loop_latency').observe(took)
+
+ if not self.lastLoopSucceeded:
+ log.info('Sequencer.update is working')
+ self.lastLoopSucceeded = True
+
+ delay = max(0, 1 / self.fps - took)
+ reactor.callLater(delay, self.updateLoop)
+
+ @metrics('update_call').time()
+ @inlineCallbacks
+ def update(self) -> Deferred:
+
+ with metrics('update_s0_getMusic').time():
+ musicState = self.music.getLatest()
+ if not musicState.get('song') or not isinstance(
+ musicState.get('t'), float):
+ return defer.succeed(0.0)
+ song = Song(URIRef(musicState['song']))
+ dispatcher.send('state',
+ update={
+ 'song': str(song),
+ 't': musicState['t']
+ })
+
+ with metrics('update_s1_eval').time():
+ settings = []
+ songNotes = sorted(self.notes.get(song, []), key=lambda n: n.uri)
+ noteReports = []
+ for note in songNotes:
+ try:
+ s, report = note.outputSettings(musicState['t'])
+ except Exception:
+ traceback.print_exc()
+ raise
+ noteReports.append(report)
+ settings.append(s)
+ devSettings = DeviceSettings.fromList(self.graph, settings)
+
+ dispatcher.send('state', update={'songNotes': noteReports})
+
+ with metrics('update_s3_send').time(): # our measurement
+ sendSecs = yield self.sendToCollector(devSettings)
+
+ # sendToCollector's own measurement.
+ # (sometimes it's None, not sure why, and neither is mypy)
+ #if isinstance(sendSecs, float):
+ # metrics('update_s3_send_client').observe(sendSecs)
+
+
+class Updates(cyclone.sse.SSEHandler):
+
+ def __init__(self, application, request, **kwargs) -> None:
+ cyclone.sse.SSEHandler.__init__(self, application, request, **kwargs)
+ self.state: Dict = {}
+ dispatcher.connect(self.updateState, 'state')
+ self.numConnected = 0
+
+ def updateState(self, update: Dict):
+ self.state.update(update)
+
+ def bind(self) -> None:
+ self.numConnected += 1
+
+ if self.numConnected == 1:
+ self.loop()
+
+ def loop(self) -> None:
+ if self.numConnected == 0:
+ return
+ self.sendEvent(self.state)
+ reactor.callLater(.1, self.loop)
+
+ def unbind(self) -> None:
+ self.numConnected -= 1
diff -r 8516a39eedc9 -r ddbd0fca89ff light9/effect/sequencer/service.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/effect/sequencer/service.py Mon May 30 23:46:07 2022 -0700
@@ -0,0 +1,83 @@
+"""
+plays back effect notes from the timeline
+"""
+
+from run_local import log
+from twisted.internet import reactor
+from light9.metrics import metrics, metricsRoute
+from rdfdb.syncedgraph import SyncedGraph
+from light9 import networking, showconfig
+import optparse, sys, logging
+import cyclone.web
+from rdflib import URIRef
+from light9.effect.sequencer.sequencer import Sequencer, Updates
+from light9.collector.collector_client import sendToCollector
+
+from light9 import clientsession
+
+
+class App(object):
+
+ def __init__(self, show, session):
+ self.show = show
+ self.session = session
+
+ self.graph = SyncedGraph(networking.rdfdb.url, "effectSequencer")
+ self.graph.initiallySynced.addCallback(self.launch)
+
+ def launch(self, *args):
+ self.seq = Sequencer(
+ self.graph,
+ lambda settings: sendToCollector(
+ 'effectSequencer',
+ self.session,
+ settings,
+ # This seems to be safe here (and lets us get from
+ # 20fpx to 40fpx), even though it leads to big stalls
+ # if I use it on KC.
+ useZmq=True))
+
+ self.cycloneApp = cyclone.web.Application(handlers=[
+ (r'/()', cyclone.web.StaticFileHandler, {
+ "path": "light9/effect/",
+ "default_filename": "sequencer.html"
+ }),
+ (r'/updates', Updates),
+ metricsRoute(),
+ ],
+ debug=True,
+ seq=self.seq,
+ graph=self.graph,
+ # stats=self.stats
+ )
+ reactor.listenTCP(networking.effectSequencer.port, self.cycloneApp)
+ log.info("listening on %s" % networking.effectSequencer.port)
+
+
+if __name__ == "__main__":
+ parser = optparse.OptionParser()
+ parser.add_option(
+ '--show',
+ help='show URI, like http://light9.bigasterisk.com/show/dance2008',
+ default=showconfig.showUri())
+ parser.add_option("-v",
+ "--verbose",
+ action="store_true",
+ help="logging.DEBUG")
+ parser.add_option("--twistedlog",
+ action="store_true",
+ help="twisted logging")
+ clientsession.add_option(parser)
+ (options, args) = parser.parse_args()
+ log.setLevel(logging.DEBUG if options.verbose else logging.INFO)
+
+ if not options.show:
+ raise ValueError("missing --show http://...")
+
+ session = clientsession.getUri('effectSequencer', options)
+
+ app = App(URIRef(options.show), session)
+ if options.twistedlog:
+ from twisted.python import log as twlog
+ twlog.startLogging(sys.stderr)
+ reactor.run()
diff -r 8516a39eedc9 -r ddbd0fca89ff light9/effect/sequencer/web/index.html
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/effect/sequencer/web/index.html Mon May 30 23:46:07 2022 -0700
@@ -0,0 +1,149 @@
+
+
+
+ effect sequencer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Song
+
+
+ t={{report.roundT}}
+
+ Notes
+
+
+
+ Note |
+ Effect class |
+ Effect settings |
+ Devices affected |
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+
+
+ :
+ {{item.value}}
+
+
+
+ |
+
+ {{item.devicesAffected}}
+ |
+
+
+
+
+
+
+
+
+
+
+
+
diff -r 8516a39eedc9 -r ddbd0fca89ff light9/effect/sequencer/web/vite.config.ts
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/light9/effect/sequencer/web/vite.config.ts Mon May 30 23:46:07 2022 -0700
@@ -0,0 +1,20 @@
+import { defineConfig } from "vite";
+
+const servicePort = 8213;
+export default defineConfig({
+ base: "/effectSeequencer/",
+ root: "./light9/effect/sequencer/web",
+ publicDir: "../web",
+ server: {
+ host: "0.0.0.0",
+ strictPort: true,
+ port: servicePort + 100,
+ hmr: {
+ port: servicePort + 200,
+ },
+ },
+ clearScreen: false,
+ define: {
+ global: {},
+ },
+});