From 36353286c7f5fc7bf31298dd2846ee49135986d7 Mon Sep 17 00:00:00 2001 From: Ruben van de Ven Date: Wed, 3 Jul 2019 17:54:14 +0200 Subject: [PATCH] Fix #60 - collective moment implementation --- hugvey/central_command.py | 42 +++++++++++++++++- hugvey/story.py | 41 +++++++++++++++++- www/index.html | 4 +- www/js/hugvey_console.js | 90 +++++++++++++++++++++++++++++++++++++-- www/panopticon.html | 4 +- 5 files changed, 170 insertions(+), 11 deletions(-) diff --git a/hugvey/central_command.py b/hugvey/central_command.py index 7c2b749..203c8da 100644 --- a/hugvey/central_command.py +++ b/hugvey/central_command.py @@ -12,7 +12,7 @@ from zmq.asyncio import Context import asyncio from hugvey.communication import getTopic, zmqSend, zmqReceive, LOG_BS from hugvey.panopticon import Panopticon -from hugvey.story import Story +from hugvey.story import Story, Stopwatch from hugvey.speech.google import GoogleVoiceClient from hugvey.speech.player import Player from hugvey.speech.streamer import AudioStreamer @@ -23,8 +23,9 @@ import threading from hugvey.voice import VoiceStorage import multiprocessing from hugvey.speech.recorder import Recorder -from pythonosc import udp_client +from pythonosc import udp_client, osc_server, dispatcher import copy +from pythonosc.osc_server import AsyncIOOSCUDPServer mainLogger = logging.getLogger("hugvey") @@ -68,6 +69,8 @@ class CentralCommand(object): self.languageFiles = {} self.languageConfig = {} self.args = args # cli args + + self.timer = Stopwatch() eventLogger.addHandler(logging.handlers.QueueHandler(self.logQueue)) @@ -139,6 +142,7 @@ class CentralCommand(object): def getStatusSummary(self, selected_ids = []): status = { 'uptime': "-" if not self.start_time else (time.time() - self.start_time), + 'loop_timer': self.timer.getElapsed(), 'languages': self.config['languages'], 'hugvey_ids': self.hugvey_ids, 'hugveys': [], @@ -239,6 +243,33 @@ class CentralCommand(object): logger.warn('Stopping light sender') lightConn._sock.close() + + def restartTimerHandler(self, address, *args): + """ + See self.oscListener + """ + logger.warn(f"Restart loop timer") + self.timer.reset() + if len(args) > 0 and float(args[0]) > 0: + print(args, args[0]) + logger.warn(f"Set timer to custom time: {float(args[0])} seconds ago") + self.timer.setMark('start', time.time() - float(args[0])) + + async def oscListener(self): + """ + OSC server, listens for loop restarts + """ + dispatch = dispatcher.Dispatcher() + dispatch.map("/loop", self.restartTimerHandler) + + server = osc_server.AsyncIOOSCUDPServer( + ("0.0.0.0", 9000), dispatch, asyncio.get_event_loop() + ) + logger.info('Start OSC server to receive loop re-starts') +# await server.serve() + transport, protocol = await server.create_serve_endpoint() +# logger.critical(f"{transport}, {protocol}") +# transport.close() async def redLightController(self): """ @@ -384,6 +415,8 @@ class CentralCommand(object): self.catchException(self.commandSender())) self.tasks['lightSender'] = self.loop.create_task( self.catchException(self.lightSender())) + self.tasks['oscListener'] = self.loop.create_task( + self.catchException(self.oscListener())) self.tasks['redLightController'] = self.loop.create_task( self.catchException(self.redLightController())) @@ -680,6 +713,11 @@ class HugveyState(object): self.logger.log(LOG_BS, f"Send /hugvey {self.lightStatus}") self.command.commandLight('/hugvey', [self.lightId, self.lightStatus]) + + def transitionLight(self, intensity, duration): + self.lightIntensity = intensity + self.logger.log(LOG_BS, f"Send /hugvey_fade {self.lightIntensity} {duration}") + self.command.commandLight('/hugvey_fade', [self.lightId, intensity, duration]) def setLightId(self, id): """ diff --git a/hugvey/story.py b/hugvey/story.py index 8066e34..de80ea9 100644 --- a/hugvey/story.py +++ b/hugvey/story.py @@ -610,6 +610,8 @@ class Diversion(object): if type == 'timeout': self.method = self._divergeIfTimeout self.finaliseMethod = self._returnAfterTimeout + if type == 'collective_moment': + self.method = self._divergeIfCollectiveMoment if type == 'repeat': self.method = self._divergeIfRepeatRequest self.regex = re.compile(self.params['regex']) @@ -864,7 +866,39 @@ class Diversion(object): story.logger.info(f"Finalise diversion: {self.id}") # if self.params['returnAfterStrand']: # await story.setCurrentMessage(self.returnMessage) + async def _divergeIfCollectiveMoment(self, story, msgFrom, msgTo, direction): + """ + Central command timer times to a collective moment + """ + #: :var story: Story + if story.currentDiversion or not msgFrom or not msgTo: + return False + + if not msgTo.chapterStart: + # only when changing chapter + return + + window_open_second = float(self.params['start_minute']) * 60 + window_close_second = window_open_second + float(self.params['window']) + + now = story.hugvey.command.timer.getElapsed() + if now < window_open_second or now > window_close_second: + return + + #open! + msg = story.get(self.params['msgId']) + if msg is None: + story.logger.critical(f"Not a valid message id for diversion: {self.id} {self.params['msgId']}") + return + + self.returnMessage = msgTo + + self.createReturnDirectionsTo(story, msg, msgTo, direction, inheritTiming=True) + await story.setCurrentMessage(msg) + story.currentDiversion = self + return True + async def _divergeIfRepeatRequest(self, story, msgFrom, msgTo, direction): """ Participant asks if message can be repeated. @@ -1054,8 +1088,11 @@ class Stopwatch(object): self.paused_at = 0 self.isRunning.set() - def setMark(self, name): - self.marks[name] = time.time() + def setMark(self, name, overrideValue = None): + """ + Set a marker to current time. Or , if given, to any float one desires + """ + self.marks[name] = overrideValue if overrideValue else time.time() def hasMark(self, name): return name in self.marks diff --git a/www/index.html b/www/index.html index 068c001..dde0913 100644 --- a/www/index.html +++ b/www/index.html @@ -17,8 +17,8 @@
-
Uptime
-
{{uptime}}
+
Timer
+
{{loop_timer}} (up: {{uptime}})
    diff --git a/www/js/hugvey_console.js b/www/js/hugvey_console.js index d223c9c..f9ea16b 100644 --- a/www/js/hugvey_console.js +++ b/www/js/hugvey_console.js @@ -10,6 +10,7 @@ class Panopticon { el: "#interface", data: { uptime: 0, + loop_timer: 0, languages: [], hugveys: [], selectedId: null, @@ -124,6 +125,7 @@ class Panopticon { case 'status': this.hugveys.uptime = this.stringToHHMMSS( msg['uptime'] ); + this.hugveys.loop_timer = this.stringToHHMMSS( msg['loop_timer'] ); this.hugveys.languages = msg['languages']; this.languages = msg['languages']; this.hugveys.hugveys = msg['hugveys']; @@ -411,7 +413,12 @@ class Graph { } else if(type == 'repeat') { div['params']['regex'] = "can you repeat that\\?"; - } else { + } + else if(type == 'collective_moment') { + div['params']['start_minute'] = 20; // minute to start + div['params']['window'] = 60; // how long to wait, in seconds + } + else { console.log("invalid type", type); alert('invalid type for diversion'); } @@ -437,7 +444,7 @@ class Graph { let msgEl = document.getElementById( 'msg' ); msgEl.innerHTML = ""; - let divsNoResponse =[], divsRepeat = [], divsReplyContains = [], divsTimeouts = [], divsInterrupts = []; + let divsNoResponse =[], divsRepeat = [], divsReplyContains = [], divsTimeouts = [], divsInterrupts = [], divsCollectiveMoment = []; for(let div of this.diversions) { let notAfterMsgIdEl = ""; @@ -630,6 +637,70 @@ class Graph { notAfterMsgIdEl )); } + if(div['type'] == 'collective_moment') { + let msgOptions = [crel('option',"")]; + let starts = this.messages.filter( m => m.hasOwnProperty('start') && m['start'] == true); + for(let startMsg of starts) { + let optionParams = {}; + if(div['params']['msgId'] == startMsg['@id']) { + optionParams['selected'] = 'selected'; + } + msgOptions.push(crel('option', optionParams , startMsg['@id'])); + } + + divsCollectiveMoment.push(crel( + 'div', { + 'class': 'diversion', + 'on': { + 'mouseover': function(e) { + if(div['params']['msgId']) + document.getElementById(div['params']['msgId']).classList.add('selectedMsg'); + }, + 'mouseout': function(e) { + if(div['params']['msgId']) + document.getElementById(div['params']['msgId']).classList.remove('selectedMsg'); + } + } + }, + crel('h3', div['@id']), + crel( + 'div', { + 'class':'btn btn--delete', + 'on': { + 'click': (e) => this.deleteDiversion(div) + } + }, 'Delete diversion'), + crel('label', 'Start time (minute)', + crel('input', { + 'type': 'number', + 'max': 59, + 'min': 0, + 'value': div['params']['start_minute'], + 'placeholder': 'time (minute)', + 'on': { + 'change': (e) => div['params']['start_minute'] = e.target.value + } + }) + ), + crel('label', 'Duration (seconds)', + crel('input', { + 'type': 'number', + 'max': 60*15, + 'min': 0, + 'value': div['params']['window'], + 'placeholder': 'seconds', + 'on': { + 'change': (e) => div['params']['window'] = e.target.value + } + }) + ), + crel('label', 'Go to (start message)', + crel('select', {'on': { + 'change': (e) => div['params']['msgId'] = e.target.value + }}, ...msgOptions) + ) + )); + } if(div['type'] == 'timeout') { let returnAttrs = { 'type': 'checkbox', @@ -777,7 +848,7 @@ class Graph { } } - console.log(divsReplyContains, divsNoResponse, divsRepeat, divsTimeouts, divsInterrupts); + console.log(divsReplyContains, divsNoResponse, divsRepeat, divsTimeouts, divsInterrupts, divsCollectiveMoment); let divEl = crel( 'div', @@ -836,6 +907,19 @@ class Graph { }, 'New case for timeout' ) + ), + crel('div', + crel('h2', 'Collective moments'), + ...divsCollectiveMoment, + crel('div', + { + 'class': 'btn', + 'on': { + 'click': (e) => this.createDiversion('collective_moment') + } + }, + 'New collective moment' + ) ) // , // crel('div', diff --git a/www/panopticon.html b/www/panopticon.html index efc6826..5343993 100644 --- a/www/panopticon.html +++ b/www/panopticon.html @@ -17,8 +17,8 @@
    -
    Uptime
    -
    {{uptime}}
    +
    Timer
    +
    {{loop_timer}} (up: {{uptime}})