diff --git a/README.md b/README.md index 9c5154c..ed16384 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ chown=pi:pi ## Deploy / usefull commands ```bash -for i in {1..6}; do rsync -av ~/hugvey/ pi@hugvey$i.local:/home/pi/hugvey/ --exclude=www --exclude=venv --exclude=local --exclude=*.pyc --exclude=.git; done +for i in {1..26}; do echo $i; rsync -av ~/hugvey/ pi@hugvey$i.local:/home/pi/hugvey/ --exclude=www --exclude=venv --exclude=local --exclude=*.pyc --exclude=.git --exclude=recordings --exclude=/voice* --exclude=/pd; done ``` ```bash @@ -204,4 +204,4 @@ times occured/only on n-th instance: determines the order of diversions of the s ## 4G Modem Visit 192.168.5.1 -The password is at the bottom of the device. \ No newline at end of file +The password is at the bottom of the device. diff --git a/client_config.yml b/client_config.yml index a49d591..5ec33d3 100644 --- a/client_config.yml +++ b/client_config.yml @@ -5,7 +5,7 @@ voice: input_rate: 44100 target_rate: 16000 port: 4444 - input_name: 'AK5371' + input_name: 'USB Audio Device' output_name: 'USB Audio Device' input_mixer: 'Mic' output_mixer: 'PCM' @@ -13,6 +13,3 @@ voice: output_volume: 30 file_address: "http://hugveycmd.local:8888" output_driver: pulseaudio - - - diff --git a/hugvey/central_command.py b/hugvey/central_command.py index 2f27175..26d9aa0 100644 --- a/hugvey/central_command.py +++ b/hugvey/central_command.py @@ -24,6 +24,7 @@ from hugvey.voice import VoiceStorage import multiprocessing from hugvey.speech.recorder import Recorder from pythonosc import udp_client +import copy mainLogger = logging.getLogger("hugvey") @@ -67,7 +68,7 @@ class CentralCommand(object): self.languageFiles = {} self.languageConfig = {} self.args = args # cli args - + eventLogger.addHandler(logging.handlers.QueueHandler(self.logQueue)) def loadConfig(self, filename): @@ -85,14 +86,14 @@ class CentralCommand(object): self.hugvey_ids = [i + 1 for i in range(self.config['hugveys'])] self.loadLanguages() - - + + voice_dir = os.path.join(self.config['web']['files_dir'], 'voices') self.voiceStorage = VoiceStorage(voice_dir, self.languageConfig) - + self.panopticon = Panopticon(self, self.config, self.voiceStorage) - - + + def loadLanguages(self): logger.debug('load language files') self.languages = {} @@ -116,7 +117,7 @@ class CentralCommand(object): # if not hv.story: # status['status'] = 'off' # return status - + status['status'] = hv.getStatus() status['language'] = hv.language_code status['light_id'] = hv.lightId @@ -126,7 +127,7 @@ class CentralCommand(object): # status['history'] = hv.story.getLogSummary() # disabled as it is a bit slow. We now have eventLog # status['counts'] = {t: len(a) for t, a in status['history'].items() if t != 'directions' } status['counts'] = {} if not hv.story else hv.story.getLogCounts() - status['duration'] = 0 if not hv.story else hv.story.timer.getElapsed() + status['duration'] = 0 if not hv.story else hv.story.timer.getElapsed() return status @@ -139,10 +140,10 @@ class CentralCommand(object): 'logbookId': None, 'logbook': [], } - + #use this to test if any threads stay open # eg. after killing/dying of a hugvey -# print(threading.enumerate()) +# print(threading.enumerate()) for hv_id in self.hugvey_ids: status['hugveys'].append(self.getHugveyStatus(hv_id, selected_id == hv_id)) @@ -151,7 +152,7 @@ class CentralCommand(object): if self.hugveys[selected_id].recorder: status['logbook'] = self.hugveys[selected_id].recorder.currentLog status['logbookId'] = selected_id - + return status def commandHugvey(self, hv_id, msg): @@ -169,8 +170,8 @@ class CentralCommand(object): def _queueCommand(self, hv_id, msg): self.commandQueue.put_nowait((hv_id, msg)) - - + + def commandLight(self, route, data): """ Buffer light commands @@ -215,13 +216,13 @@ class CentralCommand(object): logger.warn('Stopping command sender') s.close() - - + + async def lightSender(self): lightConn = udp_client.SimpleUDPClient( self.config['light']['ip'], self.config['light']['port']) - + logger.info(f"Ready to send light commands to: {self.config['light']['ip']}:{self.config['light']['port']}") while self.isRunning.is_set(): @@ -232,7 +233,7 @@ class CentralCommand(object): logger.warn('Stopping light sender') lightConn._sock.close() - + async def redLightController(self): """ Every second, check if no hugveys are available. If so, the red light should be @@ -264,7 +265,7 @@ class CentralCommand(object): thread = threading.Thread( target=self.hugveyStateRunner, args=(hugvey_id,), name=f"hugvey#{hugvey_id}") thread.start() - + def hugveyStateRunner(self, hugvey_id): while self.isRunning.is_set(): logger.info(f'Instantiate hugvey #{hugvey_id}') @@ -278,7 +279,7 @@ class CentralCommand(object): return logger.critical(f'Hugvey stopped (crashed?). Reinstantiate after 5 sec') time.sleep(5) - + async def timerEmitter(self): """ This is fixed: a one hour loop with a collective moment 10-15 minutes, @@ -288,25 +289,25 @@ class CentralCommand(object): intervals = [ { 'start_time': 10*60, - 'duration': 5 * 60, + 'duration': 5 * 60, }, { 'start_time': 30*60, - 'duration': 5 * 60, + 'duration': 5 * 60, }, { 'start_time': 50*60, - 'duration': 5 * 60, + 'duration': 5 * 60, } ] self.start_time = time.time() - + # TODO: emit start event - + while self.isRunning.is_set(): - + pass - + async def eventListener(self): s = self.ctx.socket(zmq.SUB) s.bind(self.config['events']['listen_address']) @@ -319,7 +320,7 @@ class CentralCommand(object): while self.isRunning.is_set(): try: hugvey_id, msg = await zmqReceive(s) - + if hugvey_id not in self.hugvey_ids: logger.critical( "Message from alien Hugvey: {}".format(hugvey_id)) @@ -355,7 +356,7 @@ class CentralCommand(object): fn = await self.voiceStorage.requestFile(hv.language_code, text, isVariable) if fn is None: eventLogger.getChild(f"{hugvey_id}").critical("error: No voice file fetched, check logs.") - fn = 'local/crash.wav' + fn = 'local/crash.wav' # TODO: trigger a repeat/crash event. await s.send_string(fn) except Exception as e: @@ -376,7 +377,7 @@ class CentralCommand(object): self.catchException(self.lightSender())) self.tasks['redLightController'] = self.loop.create_task( self.catchException(self.redLightController())) - + for hid in self.hugvey_ids: self.tasks['voiceListener'] = self.loop.create_task( self.catchException(self.voiceListener(hid))) @@ -386,12 +387,12 @@ class CentralCommand(object): self.panopticon_thread = threading.Thread( target=self.panopticon.start, name="Panopticon") self.panopticon_thread.start() - + self.loop.run_forever() def stop(self): self.isRunning.clear() - + async def catchException(self, awaitable): try: # print(awaitable) @@ -405,7 +406,7 @@ class HugveyState(object): """Represents the state of a Hugvey client on the server. Manages server connections & voice parsing etc. """ - + # all statusses can only go up or down, except for gone, which is an error state: # off <-> blocked <-> available <-> running <-> paused STATE_OFF = "off" @@ -424,7 +425,7 @@ class HugveyState(object): self.isConfigured = None self.isRunning = asyncio.Event(loop=self.loop) self.isRunning.clear() - + self.eventQueue = None self.language_code = 'en-GB' self.story = None @@ -435,24 +436,24 @@ class HugveyState(object): self.notShuttingDown = True # TODO: allow shutdown of object self.startMsgId = None self.eventLogger = eventLogger.getChild(f"{self.id}") - + self.setStatus(self.STATE_GONE) - + self.requireRestartAfterStop = None def __del__(self): self.logger.warn("Destroying hugvey object") - + def getStatus(self): return self.status - + def setStatus(self, status): self.status = status lightOn = status in [self.STATE_AVAILABLE, self.STATE_PAUSE] self.setLightStatus(lightOn) self.eventLogger.info(f"status: {self.status}") - - + + def config(self, hostname, ip): self.ip = ip self.hostname = hostname @@ -463,7 +464,7 @@ class HugveyState(object): else: self.logger.info( f"Hugvey {self.id} at {self.ip}, host: {self.hostname}") - + if self.status == self.STATE_GONE: # turn on :-) self.setStatus(self.STATE_BLOCKED) @@ -496,7 +497,7 @@ class HugveyState(object): self.logger.exception(e) self.logger.critical(f"Hugvey crash") self.eventLogger.critical(f"error: {e}") - + # restart # TODO: test proper functioning self.shutdown() @@ -510,18 +511,18 @@ class HugveyState(object): else: # Allow for both the Hugvey Command, or the Story handle the event. self.loop.call_soon_threadsafe(self._queueEvent, msg) - + def _queueEvent(self, msg): """ Put event in both the event loop for the story as well as the Hugvey State handler """ self.logger.debug(f"Queue event in hugvey loop: {msg}") self.eventQueue.put_nowait(msg) - + # connection events don't need to go to the story if msg['event'] == 'connection': return - + if self.story: self.story.events.append(msg) else: @@ -538,7 +539,7 @@ class HugveyState(object): self.logger.error("Hugvey did not send heartbeat.") self.gone() continue - + self.logger.debug("Received: {}".format(event)) if event['event'] == 'connection': # 'event': 'connection', @@ -546,11 +547,11 @@ class HugveyState(object): # 'host': socket.gethostname(), # 'ip': self.getIp(), self.config(event['host'], event['ip']) - - + + if event['event'] == 'language': self.setLanguage(event['code']) - + if event['event'] == 'pause': self.pause() if event['event'] == 'block': @@ -563,7 +564,7 @@ class HugveyState(object): self.story._finish() # finish story AND hugvey state if event['event'] == 'resume': self.resume() - + if event['event'] == 'change_language': self.setLanguage(event['lang_code']) if event['event'] == 'change_light': @@ -577,7 +578,7 @@ class HugveyState(object): # self.restart() if self.story is None: return - + self.startMsgId = event['msg_id'] self.logger.debug(f"Restart from {self.startMsgId}") self.restart() @@ -587,18 +588,18 @@ class HugveyState(object): def setLanguage(self, language_code): if language_code not in self.command.languages: raise Exception("Invalid language {}".format(language_code)) - + self.logger.info(f"set language: {language_code}") self.language_code = language_code - + if self.google: self.google.setLanguage(language_code) - + if self.isRunning.is_set(): self.restart() # self.story.reset() # self.story.setStoryData(self.command.languages[language_code]) - + def pause(self): self.logger.info('Pause') if self.google: @@ -607,7 +608,7 @@ class HugveyState(object): self.story.pause() self.isRunning.clear() self.setStatus(self.STATE_PAUSE) - + def resume(self): """ Start playing without reset""" self.logger.info('Resume') @@ -617,14 +618,14 @@ class HugveyState(object): self.story.resume() self.isRunning.set() self.setStatus(self.STATE_RUNNING) - + def restart(self): """Start playing with reset""" self.logger.info('Restart') if self.story: self.story.stop() self.resume() - + def block(self): """Block a hugvey""" self.logger.info('block') @@ -634,37 +635,37 @@ class HugveyState(object): self.story.finish() self.isRunning.clear() self.setStatus(self.STATE_BLOCKED) - + def available(self): """Put in available mode""" self.logger.info('Finish/Await') self.pause() self.setStatus(self.STATE_AVAILABLE) - + def setLightStatus(self, on): status = 1 if on else 0 self.logger.log(LOG_BS, f"Send /hugvey {status}") - + self.command.commandLight('/hugvey', [self.lightId, status]) - + def setLightId(self, id): """ Connect hugvey to another light """ self.lightId = id - + def gone(self): '''Status to 'gone' as in, shutdown/crashed/whatever ''' self.pause() if self.story: self.story.stop() - + self.logger.warn('Gone') self.eventLogger.warn("Gone") self.isConfigured = None self.setStatus(self.STATE_GONE) - + def shutdown(self, definitive = False): self.logger.info(f"Start shutdown sequence {definitive}") self.eventLogger.critical(f"error: shutting down") @@ -673,7 +674,7 @@ class HugveyState(object): if self.story: self.story.shutdown() self.story = None - + # shutdown for stream consumers already ran. Only clear references if self.google: self.google = None @@ -681,14 +682,14 @@ class HugveyState(object): self.player = None if self.recorder: self.recorder = None - + if self.requireRestartAfterStop is None: # prevent double setting of the same variable # first call sometimes triggers second self.requireRestartAfterStop = not definitive - + self.notShuttingDown = False - + async def playStory(self): while self.notShuttingDown: @@ -706,13 +707,13 @@ class HugveyState(object): self.logger.warn(f"Starting from {startMsgId}") if not self.streamer: await asyncio.sleep(1) - + self.streamer.triggerStart() - self.story.setStoryData(self.command.languages[self.language_code]) + self.story.setStoryData(copy.deepcopy(self.command.languages[self.language_code])) self.setLightStatus(False) await self.story.run(startMsgId) # self.story = None - + def getStreamer(self): if not self.streamer: self.streamer = AudioStreamer( @@ -720,19 +721,19 @@ class HugveyState(object): self.ip, int(self.command.config['voice']['port']) + self.id, self.id) - + if self.command.config['voyeur']: self.logger.warn("Debug on: Connecting Audio player") self.player = Player( self.command.config['voice']['src_rate'], self.command.config['voice']['out_rate']) self.streamer.addConsumer(self.player) - + if self.command.config['voice']['record_dir']: self.logger.warn("Record Audio of conversation") self.recorder = Recorder( self.id, self.command.config['voice']['src_rate'], self.command.config['voice']['record_dir']) self.streamer.addConsumer(self.recorder) - + self.logger.debug("Start Speech") self.google = GoogleVoiceClient( hugvey=self, @@ -747,7 +748,7 @@ class HugveyState(object): ''' Start the audio streamer service ''' - + self.logger.debug("Start audio loop") while self.notShuttingDown: diff --git a/install_server.sh b/install_server.sh index d009e02..dc4a2bb 100755 --- a/install_server.sh +++ b/install_server.sh @@ -1,6 +1,6 @@ apt-get update -apt-get install -y munin-node bc supervisor libsox-fmt-pulse +apt-get install -y munin-node bc supervisor cp installation/rpi-internal-temp /usr/share/munin/plugins ln -sf /usr/share/munin/plugins/rpi-internal-temp /etc/munin/plugins/rpi-internal-temp rm /etc/munin/plugins/irqstats diff --git a/server_config.yml b/server_config.yml index e39086c..2d6f3e7 100644 --- a/server_config.yml +++ b/server_config.yml @@ -7,7 +7,7 @@ voice: port: 4444 chunk: 2972 google_credentials: "../test_googlespeech/My First Project-0c7833e0d5fa.json" -hugveys: 25 +hugveys: 26 languages: - code: en-GB file: story_en.json @@ -28,7 +28,7 @@ languages: ms_lang: "fr-FR" web: port: 8888 - files_dir: "local/" + files_dir: "local/" light: ip: "192.168.178.15" - port: 7400 \ No newline at end of file + port: 7400 diff --git a/www/js/hugvey_timeline.js b/www/js/hugvey_timeline.js index 560ba49..4b9c94a 100644 --- a/www/js/hugvey_timeline.js +++ b/www/js/hugvey_timeline.js @@ -18,60 +18,60 @@ class Timeline{ {content: '.', start: new Date(), type: 'point', group: 1} ]); console.log('init timeline'); - + let groups = []; for(let hid = 1; hid<=this.count; hid++) { groups.push({id: parseInt(hid), content: 'Hugvey #'+hid}); this.eventDataSet.add({content: 'initiate', start: new Date(), type: 'point', group: parseInt(hid)}) } - + let dataGroups = new vis.DataSet(groups); let options = { // 'rollingMode': {'follow': true, 'offset': .8 } }; console.log('groups', dataGroups, groups, options); - + this.timeline = new vis.Timeline(this.el, this.eventDataSet, dataGroups, options); - + let tl = this.timeline; let startDate = new Date(); startDate.setMinutes(startDate.getMinutes()-1); let endDate = new Date(); endDate.setMinutes(endDate.getMinutes()+20); setTimeout(function(){ - tl.setWindow(startDate, endDate); + tl.setWindow(startDate, endDate); }, 500); this.moveInterval = setInterval(function(){ // skip movement if not visible tl.moveTo(new Date()); }, 1000); - + ws.addEventListener( 'message', this); } - + handleEvent(e) { console.log('handle', e, this); if(e.type == 'message') { this.wsOnMessage(e) } } - + wsOnMessage(e) { let msg = JSON.parse( e.data ); - + if ( typeof msg['action'] === 'undefined' ) { console.error( "not a valid message: " + e.data ); return; } - + if(msg['action'] != 'log') { return; } - + console.debug(msg, this); - + let hv_id = parseInt(msg['id']); // {'action': 'log', 'id':hugvey_id, 'type': items[0], 'info', 'args'} let d, parts; @@ -91,7 +91,7 @@ class Timeline{ this.eventDataSet.update(d); console.log('update', d); } else { - this.eventDataSet.add({id: mId, content: msgContent, title: `${msgContent} (${msgId})`, start: new Date(), group: hv_id, 'className': 'message'}); + this.eventDataSet.add({id: mId, content: msgContent, title: `${msgContent} (${msgId})`, start: new Date(), group: hv_id, 'className': 'message'}); } break; case 'speaking': @@ -101,7 +101,7 @@ class Timeline{ let id = parts.shift(); let content = parts.join(' '); let scId = 'sc-'+id+'-'+hv_id; - + if(info.startsWith('start')){ this.eventDataSet.add({content: info, start: new Date(), type: 'point', group: hv_id, 'className': 'speech'}); } @@ -115,7 +115,7 @@ class Timeline{ this.eventDataSet.update(d); } else { console.log('add'); - this.eventDataSet.add({id: scId, content: content, title: content, start: new Date(), group: hv_id, 'className': 'speech'}); + this.eventDataSet.add({id: scId, content: content, title: content, start: new Date(), group: hv_id, 'className': 'speech'}); } } if(info.startsWith('end')){ @@ -125,7 +125,7 @@ class Timeline{ this.eventDataSet.update(d); } } - + break; case 'story': // 'info': 'start'/'finished' @@ -147,4 +147,4 @@ class Timeline{ } } -var tl = new Timeline(ws, document.getElementById('line'), 25); +var tl = new Timeline(ws, document.getElementById('line'), 26);