Merge branch 'master' of gitlab.com:hugvey/hugvey

This commit is contained in:
Ruben van de Ven 2019-05-13 14:46:46 +02:00
commit 42b4185a69
8 changed files with 407 additions and 222 deletions

View file

@ -30,7 +30,7 @@ def getWebSocketHandler(central_command):
class WebSocketHandler(tornado.websocket.WebSocketHandler): class WebSocketHandler(tornado.websocket.WebSocketHandler):
CORS_ORIGINS = ['localhost'] CORS_ORIGINS = ['localhost']
connections = set() connections = set()
def check_origin(self, origin): def check_origin(self, origin):
parsed_origin = urlparse(origin) parsed_origin = urlparse(origin)
# parsed_origin.netloc.lower() gives localhost:3333 # parsed_origin.netloc.lower() gives localhost:3333
@ -79,7 +79,7 @@ def getWebSocketHandler(central_command):
logger.exception(e) logger.exception(e)
def send(self, message): def send(self, message):
# Possible useless method: use self.write_message() # Possible useless method: use self.write_message()
j = json.dumps(message) j = json.dumps(message)
self.write_message(j) self.write_message(j)
@ -100,40 +100,40 @@ def getWebSocketHandler(central_command):
def msgInit(self): def msgInit(self):
msg = self.getStatusMsg() msg = self.getStatusMsg()
self.send(msg) self.send(msg)
def msgBlock(self, hv_id): def msgBlock(self, hv_id):
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'block'}) central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'block'})
def msgUnblock(self, hv_id): def msgUnblock(self, hv_id):
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'unblock'}) central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'unblock'})
def msgResume(self, hv_id): def msgResume(self, hv_id):
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'resume'}) central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'resume'})
def msgPause(self, hv_id): def msgPause(self, hv_id):
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'pause'}) central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'pause'})
def msgRestart(self, hv_id): def msgRestart(self, hv_id):
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'restart'}) central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'restart'})
def msgFinish(self, hv_id): def msgFinish(self, hv_id):
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'finish'}) central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'finish'})
def msgChangeLanguage(self, hv_id, lang_code): def msgChangeLanguage(self, hv_id, lang_code):
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'change_language', 'lang_code': lang_code}) central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'change_language', 'lang_code': lang_code})
def msgChangeLightId(self, hv_id, lightId): def msgChangeLightId(self, hv_id, lightId):
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'change_light', 'light_id': lightId}) central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'change_light', 'light_id': lightId})
def msgPlayMsg(self, hv_id, msg_id, reloadStory): def msgPlayMsg(self, hv_id, msg_id, reloadStory):
central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'play_msg', 'msg_id': msg_id, 'reloadStory':bool(reloadStory)}) central_command.hugveys[hv_id].eventQueue.put_nowait({'event': 'play_msg', 'msg_id': msg_id, 'reloadStory':bool(reloadStory)})
@classmethod @classmethod
def write_to_clients(wsHandlerClass, msg): def write_to_clients(wsHandlerClass, msg):
if msg is None: if msg is None:
logger.critical("Tried to send 'none' to Panopticon") logger.critical("Tried to send 'none' to Panopticon")
return return
for client in wsHandlerClass.connections: for client in wsHandlerClass.connections:
client.write_message(msg) client.write_message(msg)
@ -143,7 +143,7 @@ class NonCachingStaticFileHandler(tornado.web.StaticFileHandler):
def set_extra_headers(self, path): def set_extra_headers(self, path):
# Disable cache # Disable cache
self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') self.set_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
def getUploadHandler(central_command): def getUploadHandler(central_command):
class UploadHandler(tornado.web.RequestHandler): class UploadHandler(tornado.web.RequestHandler):
def set_default_headers(self): def set_default_headers(self):
@ -160,12 +160,12 @@ def getUploadHandler(central_command):
logger.info('upload') logger.info('upload')
langCode = self.get_argument("language") langCode = self.get_argument("language")
langFile = os.path.join(central_command.config['web']['files_dir'] , central_command.languageFiles[langCode]) langFile = os.path.join(central_command.config['web']['files_dir'] , central_command.languageFiles[langCode])
storyData = json.loads(self.request.files['json'][0]['body']) storyData = json.loads(self.request.files['json'][0]['body'])
# print(json.dumps(storyData)) # print(json.dumps(storyData))
# self.finish() # self.finish()
# return # return
if 'audio' in self.request.files: if 'audio' in self.request.files:
msgId = self.get_argument("message_id") msgId = self.get_argument("message_id")
audioFile = self.request.files['audio'][0] audioFile = self.request.files['audio'][0]
@ -179,7 +179,7 @@ def getUploadHandler(central_command):
if 'audio' in storyData[i] and storyData[i]['audio'] is not None and os.path.exists(storyData[i]['audio']['file']): if 'audio' in storyData[i] and storyData[i]['audio'] is not None and os.path.exists(storyData[i]['audio']['file']):
logger.info(f"Remove previous file {storyData[i]['audio']['file']} ({storyData[i]['audio']['original_name']})") logger.info(f"Remove previous file {storyData[i]['audio']['file']} ({storyData[i]['audio']['original_name']})")
os.unlink(storyData[i]['audio']['file']) os.unlink(storyData[i]['audio']['file'])
storyData[i]['audio'] = { storyData[i]['audio'] = {
'file': audioFilename, 'file': audioFilename,
'original_name': original_fname 'original_name': original_fname
@ -188,17 +188,17 @@ def getUploadHandler(central_command):
logger.info(f'Save {original_fname} to {audioFilename}') logger.info(f'Save {original_fname} to {audioFilename}')
fp.write(audioFile['body']) fp.write(audioFile['body'])
break break
# logger.info(os.path.abspath(langFile)) # logger.info(os.path.abspath(langFile))
langFile = os.path.abspath(langFile) langFile = os.path.abspath(langFile)
with open(langFile, 'w') as json_fp: with open(langFile, 'w') as json_fp:
logger.info(f'Save story to {langFile} {json_fp}') logger.info(f'Save story to {langFile} {json_fp}')
json.dump(storyData, json_fp, indent=2) json.dump(storyData, json_fp, indent=2)
# Reload language files for new instances # Reload language files for new instances
central_command.loadLanguages() central_command.loadLanguages()
self.finish() self.finish()
return UploadHandler return UploadHandler
def getVoiceHandler(voiceStorage): def getVoiceHandler(voiceStorage):
@ -212,7 +212,7 @@ def getVoiceHandler(voiceStorage):
fn = await voiceStorage.requestFile(lang_code, text, isVariable) fn = await voiceStorage.requestFile(lang_code, text, isVariable)
if not fn: if not fn:
raise Exception(f"No Filename for text: {text}") raise Exception(f"No Filename for text: {text}")
if int(self.get_argument('filename')) == 1: if int(self.get_argument('filename')) == 1:
self.set_header("Content-Type","text/plain") self.set_header("Content-Type","text/plain")
self.write(fn) self.write(fn)
@ -221,25 +221,30 @@ def getVoiceHandler(voiceStorage):
with open(fn, 'rb') as fp: with open(fn, 'rb') as fp:
self.write(fp.read()) self.write(fp.read())
self.finish() self.finish()
return VoiceHandler return VoiceHandler
class StaticFileWithHeaderHandler(tornado.web.StaticFileHandler):
def set_extra_headers(self, path):
"""For subclass to add extra headers to the response"""
if path[-5:] == '.html':
self.set_header("Access-Control-Allow-Origin", "*")
class Panopticon(object): class Panopticon(object):
def __init__(self, central_command, config, voiceStorage): def __init__(self, central_command, config, voiceStorage):
self.command = central_command self.command = central_command
self.config = config self.config = config
self.voiceStorage = voiceStorage self.voiceStorage = voiceStorage
self.wsHandler = getWebSocketHandler(self.command) self.wsHandler = getWebSocketHandler(self.command)
self.application = tornado.web.Application([ self.application = tornado.web.Application([
(r"/ws(.*)", self.wsHandler), (r"/ws(.*)", self.wsHandler),
(r"/local/(.*)", NonCachingStaticFileHandler, (r"/local/(.*)", NonCachingStaticFileHandler,
{"path": config['web']['files_dir']}), {"path": config['web']['files_dir']}),
(r"/upload", getUploadHandler(self.command)), (r"/upload", getUploadHandler(self.command)),
(r"/voice", getVoiceHandler(self.voiceStorage)), (r"/voice", getVoiceHandler(self.voiceStorage)),
(r"/(.*)", tornado.web.StaticFileHandler, (r"/(.*)", StaticFileWithHeaderHandler,
{"path": web_dir, "default_filename": 'index.html'}), {"path": web_dir, "default_filename": 'index.html'}),
], debug=True) ], debug=True)
@ -251,17 +256,17 @@ class Panopticon(object):
asyncio.set_event_loop(evt_loop) asyncio.set_event_loop(evt_loop)
self.loop = tornado.ioloop.IOLoop.current() self.loop = tornado.ioloop.IOLoop.current()
thread = threading.Thread( thread = threading.Thread(
target=self.broadcastLoggingQueueToWs, kwargs={'wsHandler': self.wsHandler, 'q': self.command.logQueue}, name=f"panopticon/logws") target=self.broadcastLoggingQueueToWs, kwargs={'wsHandler': self.wsHandler, 'q': self.command.logQueue}, name=f"panopticon/logws")
thread.start() thread.start()
logger.info(f"Start Panopticon on http://localhost:{self.config['web']['port']}") logger.info(f"Start Panopticon on http://localhost:{self.config['web']['port']}")
self.loop.start() self.loop.start()
def stop(self): def stop(self):
self.loop.stop() self.loop.stop()
def broadcastLoggingQueueToWs(self, wsHandler, q: Queue): def broadcastLoggingQueueToWs(self, wsHandler, q: Queue):
while True: while True:
record = q.get() record = q.get()
@ -278,4 +283,3 @@ class Panopticon(object):
j = json.dumps(msg) j = json.dumps(msg)
logger.debug(j) logger.debug(j)
self.loop.add_callback(wsHandler.write_to_clients, j) self.loop.add_callback(wsHandler.write_to_clients, j)

View file

@ -40,8 +40,8 @@ class Utterance(object):
def isFinished(self): def isFinished(self):
return self.endTime is not None return self.endTime is not None
def __getstate__(self): def __getstate__(self):
# print(f'get utterance {self}') # print(f'get utterance {self}')
state = self.__dict__.copy() state = self.__dict__.copy()
@ -68,7 +68,7 @@ class Message(object):
self.parseForVariables() self.parseForVariables()
self.uuid = None # Have a unique id each time the message is played back. self.uuid = None # Have a unique id each time the message is played back.
self.color = None self.color = None
def __getstate__(self): def __getstate__(self):
# Copy the object's state from self.__dict__ which contains # Copy the object's state from self.__dict__ which contains
# all our instance attributes. Always use the dict.copy() # all our instance attributes. Always use the dict.copy()
@ -78,7 +78,7 @@ class Message(object):
# Remove the unpicklable entries. # Remove the unpicklable entries.
del state['filenameFetchLock'] del state['filenameFetchLock']
return state return state
def __setstate__(self, state): def __setstate__(self, state):
self.__dict__.update(state) self.__dict__.update(state)
self.filenameFetchLock = asyncio.Lock() self.filenameFetchLock = asyncio.Lock()
@ -103,9 +103,9 @@ class Message(object):
if not 'vol' in msg.params: if not 'vol' in msg.params:
# prevent clipping on some Lyrebird tracks # prevent clipping on some Lyrebird tracks
msg.params['vol'] = .8 msg.params['vol'] = .8
msg.params['vol'] = float(msg.params['vol']) msg.params['vol'] = float(msg.params['vol'])
return msg return msg
def parseForVariables(self): def parseForVariables(self):
@ -215,8 +215,8 @@ class Reply(object):
self.forMessage = None self.forMessage = None
self.utterances = [] self.utterances = []
self.setForMessage(message) self.setForMessage(message)
def __getstate__(self): def __getstate__(self):
# print(f'get reply {self}') # print(f'get reply {self}')
state = self.__dict__.copy() state = self.__dict__.copy()
@ -297,8 +297,8 @@ class Condition(object):
self.logInfo = None self.logInfo = None
self.originalJsonString = None self.originalJsonString = None
self.usedContainsDuration = None self.usedContainsDuration = None
def __getstate__(self): def __getstate__(self):
# print(f'get condition {self.id}') # print(f'get condition {self.id}')
state = self.__dict__.copy() state = self.__dict__.copy()
@ -500,8 +500,8 @@ class Direction(object):
self.conditions = [] self.conditions = []
self.conditionMet = None self.conditionMet = None
self.isDiversionReturn = False self.isDiversionReturn = False
def __getstate__(self): def __getstate__(self):
# print(f'get direction {self.id}') # print(f'get direction {self.id}')
state = self.__dict__.copy() state = self.__dict__.copy()
@ -568,8 +568,8 @@ class Diversion(object):
if not self.method: if not self.method:
raise Exception("No valid type given for diversion") raise Exception("No valid type given for diversion")
def __getstate__(self): def __getstate__(self):
# print(f'get diversion {self.id}') # print(f'get diversion {self.id}')
state = self.__dict__.copy() state = self.__dict__.copy()
@ -628,7 +628,7 @@ class Diversion(object):
}] }]
""" """
self.counter +=1 self.counter +=1
# story.logger.warn(f"CREATING DIRECTIONS FOR {startMsg.id}")
finishMessageIds = story.getFinishesForMsg(startMsg) finishMessageIds = story.getFinishesForMsg(startMsg)
finalTimeoutDuration = timeoutDuration finalTimeoutDuration = timeoutDuration
finalContainsDurations = replyContainsDurations finalContainsDurations = replyContainsDurations
@ -647,6 +647,7 @@ class Diversion(object):
finalContainsDurations = json.loads(condition.originalJsonString)['vars']['delays'] finalContainsDurations = json.loads(condition.originalJsonString)['vars']['delays']
i = 0 i = 0
# story.logger.warn(f"FINISHES: {finishMessageIds}")
for msgId in finishMessageIds: for msgId in finishMessageIds:
# Some very ugly hack to add a direction & condition # Some very ugly hack to add a direction & condition
i+=1 i+=1
@ -693,6 +694,7 @@ class Diversion(object):
story.logger.info(f"Created direction: {direction.id} {condition.id} with timeout {finalTimeoutDuration}s") story.logger.info(f"Created direction: {direction.id} {condition.id} with timeout {finalTimeoutDuration}s")
story.add(condition) story.add(condition)
story.add(direction) story.add(direction)
# story.logger.warn(f"ADDED DIRECTION {direction.id}")
@ -944,7 +946,7 @@ class Configuration(object):
id = 'configuration' id = 'configuration'
volume = 1 # Volume multiplier for 'play' command volume = 1 # Volume multiplier for 'play' command
nothing_text = "nothing" # When variable is not set, but used in sentence, replace it with this word. nothing_text = "nothing" # When variable is not set, but used in sentence, replace it with this word.
@classmethod @classmethod
def initFromJson(configClass, data, story): def initFromJson(configClass, data, story):
config = Configuration() config = Configuration()
@ -1007,22 +1009,22 @@ class Stopwatch(object):
def clearMark(self, name): def clearMark(self, name):
if name in self.marks: if name in self.marks:
self.marks.pop(name) self.marks.pop(name)
def __getstate__(self): def __getstate__(self):
# print(f'get stopwatch') # print(f'get stopwatch')
state = self.__dict__.copy() state = self.__dict__.copy()
state['isRunning'] = self.isRunning.is_set() state['isRunning'] = self.isRunning.is_set()
return state return state
def __setstate__(self, state): def __setstate__(self, state):
self.__dict__.update(state) self.__dict__.update(state)
self.isRunning = asyncio.Event() self.isRunning = asyncio.Event()
if 'isRunning' in state and state['isRunning']: if 'isRunning' in state and state['isRunning']:
self.isRunning.set() self.isRunning.set()
else: else:
self.isRunning.clear() self.isRunning.clear()
class StoryState(object): class StoryState(object):
""" """
@ -1065,7 +1067,7 @@ class StoryState(object):
def __init__(self): def __init__(self):
pass pass
# #
class Story(object): class Story(object):
"""Story represents and manages a story/narrative flow""" """Story represents and manages a story/narrative flow"""
@ -1485,7 +1487,7 @@ class Story(object):
# TODO create timer event # TODO create timer event
# self.commands.append({'msg':'TEST!'}) # self.commands.append({'msg':'TEST!'})
# Test stability of Central Command with deliberate crash # Test stability of Central Command with deliberate crash
# if self.timer.getElapsed() > 10: # if self.timer.getElapsed() > 10:
# raise Exception("Test exception") # raise Exception("Test exception")
@ -1553,10 +1555,10 @@ class Story(object):
self.logger.critical(f"error: crash when reading wave file: {fn}") self.logger.critical(f"error: crash when reading wave file: {fn}")
self.logger.exception(e) self.logger.exception(e)
duration = 10 # some default duration to have something to fall back to duration = 10 # some default duration to have something to fall back to
params = message.getParams().copy() params = message.getParams().copy()
params['vol'] = params['vol'] * self.configuration.volume if 'vol' in params else self.configuration.volume params['vol'] = params['vol'] * self.configuration.volume if 'vol' in params else self.configuration.volume
# self.hugvey.google.pause() # pause STT to avoid text events while decision is made # self.hugvey.google.pause() # pause STT to avoid text events while decision is made
self.hugvey.sendCommand({ self.hugvey.sendCommand({
'action': 'play', 'action': 'play',
@ -1621,7 +1623,7 @@ class Story(object):
self.isRunning = True self.isRunning = True
if not self.lastMsgFinishTime and self.currentMessage: if not self.lastMsgFinishTime and self.currentMessage:
await self.setCurrentMessage(self.currentMessage) await self.setCurrentMessage(self.currentMessage)
await self._renderer() await self._renderer()
def isFinished(self): def isFinished(self):
@ -1649,16 +1651,16 @@ class Story(object):
self.timer.pause() self.timer.pause()
def calculateFinishesForMsg(self, msgId, depth = 0, checked = []): def calculateFinishesForMsg(self, msgId, depth = 0, checked = []):
if msgId in checked: # if msgId in checked:
return [] # return []
#
checked.append(msgId) # checked.append(msgId)
if not msgId in self.directionsPerMsg or len(self.directionsPerMsg[msgId]) < 1: if not msgId in self.directionsPerMsg or len(self.directionsPerMsg[msgId]) < 1:
# is finish # is finish
return [msgId] return [msgId]
if depth > 200: if depth > 100:
return [] return []
finishes = [] finishes = []
@ -1691,6 +1693,7 @@ class Story(object):
returns message ids returns message ids
""" """
print(msg.id, self.strands)
if msg.id in self.strands: if msg.id in self.strands:
return self.strands[msg.id] return self.strands[msg.id]
@ -1710,22 +1713,22 @@ class Story(object):
# TODO: should the direction have at least a timeout condition set, or not perse? # TODO: should the direction have at least a timeout condition set, or not perse?
return self.directionsPerMsg[msg.id][0] return self.directionsPerMsg[msg.id][0]
@classmethod @classmethod
def getStateDir(self): def getStateDir(self):
return "/tmp" return "/tmp"
# day = time.strftime("%Y%m%d") # day = time.strftime("%Y%m%d")
# t = time.strftime("%H:%M:%S") # t = time.strftime("%H:%M:%S")
# #
# self.out_folder = os.path.join(self.main_folder, day, f"{self.hv_id}", t) # self.out_folder = os.path.join(self.main_folder, day, f"{self.hv_id}", t)
# if not os.path.exists(self.out_folder): # if not os.path.exists(self.out_folder):
# self.logger.debug(f"Create directory {self.out_folder}") # self.logger.debug(f"Create directory {self.out_folder}")
# self.target_folder = os.makedirs(self.out_folder, exist_ok=True) # self.target_folder = os.makedirs(self.out_folder, exist_ok=True)
@classmethod @classmethod
def getStateFilename(cls, hv_id): def getStateFilename(cls, hv_id):
return os.path.join(cls.getStateDir(), f"hugvey{hv_id}") return os.path.join(cls.getStateDir(), f"hugvey{hv_id}")
def storeState(self): def storeState(self):
# TODO: stop stopwatch # TODO: stop stopwatch
fn = self.getStateFilename(self.hugvey.id) fn = self.getStateFilename(self.hugvey.id)
@ -1735,49 +1738,48 @@ class Story(object):
pickle.dump(self, fp) pickle.dump(self, fp)
# write atomic to disk: flush, close, rename # write atomic to disk: flush, close, rename
fp.flush() fp.flush()
os.fsync(fp.fileno()) os.fsync(fp.fileno())
os.rename(tmpfn, fn) os.rename(tmpfn, fn)
self.logger.debug(f"saved state to {fn}") self.logger.debug(f"saved state to {fn}")
def hasSavedState(self): def hasSavedState(self):
return self.hugveyHasSavedState(self.hugvey.id) return self.hugveyHasSavedState(self.hugvey.id)
@classmethod @classmethod
def hugveyHasSavedState(cls, hv_id): def hugveyHasSavedState(cls, hv_id):
return os.path.exists(cls.getStateFilename(hv_id)) return os.path.exists(cls.getStateFilename(hv_id))
@classmethod @classmethod
def loadStoryFromState(cls, hugvey_state): def loadStoryFromState(cls, hugvey_state):
# restart stopwatch # restart stopwatch
with open(cls.getStateFilename(hugvey_state.id), 'rb') as fp: with open(cls.getStateFilename(hugvey_state.id), 'rb') as fp:
story = pickle.load(fp) story = pickle.load(fp)
story.hugvey = hugvey_state story.hugvey = hugvey_state
story.logger = mainLogger.getChild(f"{story.hugvey.id}").getChild("story") story.logger = mainLogger.getChild(f"{story.hugvey.id}").getChild("story")
return story return story
# TODO: take running state etc. # TODO: take running state etc.
@classmethod @classmethod
def clearSavedState(cls, hv_id): def clearSavedState(cls, hv_id):
fn = cls.getStateFilename(hv_id) fn = cls.getStateFilename(hv_id)
if os.path.exists(fn): if os.path.exists(fn):
os.unlink(fn) os.unlink(fn)
mainLogger.info(f"Removed state: {fn}") mainLogger.info(f"Removed state: {fn}")
# #
def __getstate__(self): def __getstate__(self):
# Copy the object's state from self.__dict__ which contains # Copy the object's state from self.__dict__ which contains
# all our instance attributes. Always use the dict.copy() # all our instance attributes. Always use the dict.copy()
# method to avoid modifying the original state. # method to avoid modifying the original state.
state = self.__dict__.copy() state = self.__dict__.copy()
# Remove the unpicklable entries. # Remove the unpicklable entries.
del state['hugvey'] del state['hugvey']
del state['logger'] del state['logger']
# del state['isRunning'] # del state['isRunning']
return state return state
def __setstate__(self, state): def __setstate__(self, state):
self.__dict__.update(state) self.__dict__.update(state)

View file

@ -15,6 +15,16 @@ echo "blacklist snd_bcm2835" > /etc/modprobe.d/internalsnd-blacklist.conf
echo "d /var/log/supervisor 0777 root root" > /etc/tmpfiles.d/supervisor.conf echo "d /var/log/supervisor 0777 root root" > /etc/tmpfiles.d/supervisor.conf
cp installation/fstab /etc/fstab cp installation/fstab /etc/fstab
# setup pulseaudio as system daemon and grant access: https://rudd-o.com/linux-and-free-software/how-to-make-pulseaudio-run-once-at-boot-for-all-your-users
cp installation/pulseaudio.service /etc/systemd/system/pulseaudio.service
# disable autospawn
cp installation/pulseaudio-client.conf /etc/pulse/client.conf
# put tsched to 0: https://wiki.archlinux.org/index.php/PulseAudio/Troubleshooting#Sound_stuttering_when_streaming_over_network
cp installation/pulse-default.pa /etc/pulse/default.pa
systemctl --system enable pulseaudio.service
systemctl --system start pulseaudio.service
usermod -a -G pulse-access pi
# Added chown=pi:pi # Added chown=pi:pi
cp installation/supervisord.conf /etc/supervisor/supervisord.conf cp installation/supervisord.conf /etc/supervisor/supervisord.conf
ln -s /home/pi/hugvey/supervisor.conf /etc/supervisor/conf.d/hugvey.conf ln -s /home/pi/hugvey/supervisor.conf /etc/supervisor/conf.d/hugvey.conf

View file

@ -0,0 +1,141 @@
#!/usr/bin/pulseaudio -nF
#
# This file is part of PulseAudio.
#
# PulseAudio is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# PulseAudio 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 Lesser General Public License
# along with PulseAudio; if not, see <http://www.gnu.org/licenses/>.
# This startup script is used only if PulseAudio is started per-user
# (i.e. not in system mode)
.fail
### Automatically restore the volume of streams and devices
load-module module-device-restore
load-module module-stream-restore
load-module module-card-restore
### Automatically augment property information from .desktop files
### stored in /usr/share/application
load-module module-augment-properties
### Should be after module-*-restore but before module-*-detect
load-module module-switch-on-port-available
### Load audio drivers statically
### (it's probably better to not load these drivers manually, but instead
### use module-udev-detect -- see below -- for doing this automatically)
#load-module module-alsa-sink
#load-module module-alsa-source device=hw:1,0
#load-module module-oss device="/dev/dsp" sink_name=output source_name=input
#load-module module-oss-mmap device="/dev/dsp" sink_name=output source_name=input
#load-module module-null-sink
#load-module module-pipe-sink
### Automatically load driver modules depending on the hardware available
.ifexists module-udev-detect.so
# HUGVEY: put tsched to 0: https://wiki.archlinux.org/index.php/PulseAudio/Troubleshooting#Sound_stuttering_when_streaming_over_network
load-module module-udev-detect tsched=0
.else
### Use the static hardware detection module (for systems that lack udev support)
load-module module-detect
.endif
### Automatically connect sink and source if JACK server is present
.ifexists module-jackdbus-detect.so
.nofail
load-module module-jackdbus-detect channels=2
.fail
.endif
### Automatically load driver modules for Bluetooth hardware
.ifexists module-bluetooth-policy.so
load-module module-bluetooth-policy
.endif
.ifexists module-bluetooth-discover.so
load-module module-bluetooth-discover
.endif
### Load several protocols
.ifexists module-esound-protocol-unix.so
load-module module-esound-protocol-unix
.endif
load-module module-native-protocol-unix
### Network access (may be configured with paprefs, so leave this commented
### here if you plan to use paprefs)
#load-module module-esound-protocol-tcp
#load-module module-native-protocol-tcp
#load-module module-zeroconf-publish
### Load the RTP receiver module (also configured via paprefs, see above)
#load-module module-rtp-recv
### Load the RTP sender module (also configured via paprefs, see above)
#load-module module-null-sink sink_name=rtp format=s16be channels=2 rate=44100 sink_properties="device.description='RTP Multicast Sink'"
#load-module module-rtp-send source=rtp.monitor
### Load additional modules from GConf settings. This can be configured with the paprefs tool.
### Please keep in mind that the modules configured by paprefs might conflict with manually
### loaded modules.
.ifexists module-gconf.so
.nofail
load-module module-gconf
.fail
.endif
### Automatically restore the default sink/source when changed by the user
### during runtime
### NOTE: This should be loaded as early as possible so that subsequent modules
### that look up the default sink/source get the right value
load-module module-default-device-restore
### Automatically move streams to the default sink if the sink they are
### connected to dies, similar for sources
load-module module-rescue-streams
### Make sure we always have a sink around, even if it is a null sink.
load-module module-always-sink
### Honour intended role device property
load-module module-intended-roles
### Automatically suspend sinks/sources that become idle for too long
load-module module-suspend-on-idle
### If autoexit on idle is enabled we want to make sure we only quit
### when no local session needs us anymore.
.ifexists module-console-kit.so
load-module module-console-kit
.endif
.ifexists module-systemd-login.so
load-module module-systemd-login
.endif
### Enable positioned event sounds
load-module module-position-event-sounds
### Cork music/video streams when a phone stream is active
load-module module-role-cork
### Modules to allow autoloading of filters (such as echo cancellation)
### on demand. module-filter-heuristics tries to determine what filters
### make sense, and module-filter-apply does the heavy-lifting of
### loading modules and rerouting streams.
load-module module-filter-heuristics
load-module module-filter-apply
### Make some devices default
#set-default-sink output
#set-default-source input

View file

@ -0,0 +1,18 @@
# disable autospawn because we're in system wide mode
; default-sink =
; default-source =
; default-server =
; default-dbus-server =
autospawn = no
; daemon-binary = /usr/bin/pulseaudio
; extra-arguments = --log-target=syslog
; cookie-file =
; enable-shm = yes
; shm-size-bytes = 0 # setting this 0 will use the system-default, usually 64 MiB
; auto-connect-localhost = no
; auto-connect-display = no

View file

@ -0,0 +1,9 @@
[Unit]
Description=PulseAudio system server
[Service]
Type=notify
ExecStart=/usr/bin/pulseaudio --daemonize=no --system --realtime --log-target=journal
[Install]
WantedBy=multi-user.target

View file

@ -1,51 +1,52 @@
#N canvas 200 136 660 592 10; #N canvas 976 211 660 592 10;
#X obj 131 277 dac~; #X obj 131 277 dac~;
#X obj 131 227 readsf~; #X obj 131 227 readsf~;
#X obj 209 209 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144 #X obj 209 209 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144
-1 -1; -1 -1;
#X text 229 206 (re-)start loop; #X text 229 206 (re-)start loop;
#X msg 132 170 open /home/a/projects/pd-play/testaudio.wav \, 1;
#X obj 208 459 netsend -u -b; #X obj 208 459 netsend -u -b;
#X obj 208 481 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 0 #X obj 208 481 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 1
1; 1;
#X msg 355 464 disconnect; #X msg 355 464 disconnect;
#X obj 208 393 list prepend send; #X obj 208 393 list prepend send;
#X obj 208 415 list trim; #X obj 208 415 list trim;
#X msg 357 434 connect localhost 5555;
#X obj 208 345 oscformat /trigger;
#X msg 51 193 0; #X msg 51 193 0;
#X obj 227 83 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144 -1 #X obj 227 83 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144 -1
-1; -1;
#X text 40 173 stop loop; #X text 40 173 stop loop;
#X msg 210 292 1; #X msg 208 320 1;
#X obj 318 84 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144 -1 #X obj 318 84 bng 15 250 50 0 empty empty empty 17 7 0 10 -262144 -1
-1; -1;
#X text 217 60 START; #X text 217 60 START;
#X obj 436 84 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 0 #X obj 436 84 tgl 15 0 empty empty empty 17 7 0 10 -262144 -1 -1 1
1; 1;
#X text 309 61 !STOP!; #X text 309 61 !STOP!;
#X text 422 60 Playing indicator; #X text 422 60 Playing indicator;
#X msg 261 118 1; #X msg 261 118 1;
#X msg 330 117 0; #X msg 330 117 0;
#X obj 208 345 oscformat /loop;
#X msg 356 434 connect 192.168.1.174 7400;
#X msg 132 170 open /mnt/stash/hugvey/score38_loop_40s_extra.wav \,
1;
#X connect 1 0 0 0; #X connect 1 0 0 0;
#X connect 1 0 0 1; #X connect 1 0 0 1;
#X connect 1 1 2 0; #X connect 1 1 2 0;
#X connect 2 0 4 0; #X connect 2 0 22 0;
#X connect 2 0 15 0; #X connect 2 0 12 0;
#X connect 4 0 1 0; #X connect 4 0 5 0;
#X connect 5 0 6 0; #X connect 6 0 4 0;
#X connect 7 0 5 0; #X connect 7 0 8 0;
#X connect 8 0 9 0; #X connect 8 0 4 0;
#X connect 9 0 5 0; #X connect 9 0 1 0;
#X connect 10 0 5 0; #X connect 10 0 21 0;
#X connect 11 0 8 0; #X connect 10 0 2 0;
#X connect 12 0 1 0; #X connect 10 0 18 0;
#X connect 13 0 10 0; #X connect 12 0 20 0;
#X connect 13 0 2 0; #X connect 13 0 9 0;
#X connect 13 0 21 0; #X connect 13 0 6 0;
#X connect 15 0 11 0; #X connect 13 0 19 0;
#X connect 16 0 12 0; #X connect 18 0 15 0;
#X connect 16 0 7 0; #X connect 19 0 15 0;
#X connect 16 0 22 0; #X connect 20 0 7 0;
#X connect 21 0 18 0; #X connect 21 0 4 0;
#X connect 22 0 18 0; #X connect 22 0 1 0;

View file

@ -28,7 +28,7 @@ class Panopticon {
}, },
loadNarrative: function( code, file ) { loadNarrative: function( code, file ) {
panopticon.hugveys.selectedId = null; panopticon.hugveys.selectedId = null;
if(panopticon.hasGraph) { if(panopticon.hasGraph) {
return panopticon.loadNarrative( code, file ); return panopticon.loadNarrative( code, file );
} }
@ -79,16 +79,16 @@ class Panopticon {
this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: false, reconnectInterval: 3000 } ); this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: false, reconnectInterval: 3000 } );
if(this.hasGraph) { if(this.hasGraph) {
this.graph = new Graph(); this.graph = new Graph();
} }
this.socket.addEventListener( 'open', ( e ) => { this.socket.addEventListener( 'open', ( e ) => {
this.send( { action: 'init' } ); this.send( { action: 'init' } );
} ); } );
// request close before unloading // request close before unloading
window.addEventListener('beforeunload', function(){ window.addEventListener('beforeunload', function(){
panopticon.socket.close(); panopticon.socket.close();
@ -102,7 +102,7 @@ class Panopticon {
console.log("Websocket connected") console.log("Websocket connected")
return; return;
} }
let msg = JSON.parse( e.data ); let msg = JSON.parse( e.data );
if ( typeof msg['alert'] !== 'undefined' ) { if ( typeof msg['alert'] !== 'undefined' ) {
alert( msg['alert'] ); alert( msg['alert'] );
@ -112,9 +112,9 @@ class Panopticon {
console.error( "not a valid message: " + e.data ); console.error( "not a valid message: " + e.data );
return; return;
} }
console.debug(msg); console.debug(msg);
switch ( msg['action'] ) { switch ( msg['action'] ) {
case 'status': case 'status':
@ -134,28 +134,28 @@ class Panopticon {
} }
} ); } );
} }
updateSelectedHugvey() { updateSelectedHugvey() {
let hv = null; let hv = null;
if(this.hugveys.selectedId) { if(this.hugveys.selectedId) {
hv = this.getHugvey(this.hugveys.selectedId); hv = this.getHugvey(this.hugveys.selectedId);
if(this.hasGraph) { if(this.hasGraph) {
if(hv.language && this.graph.language_code != hv.language) { if(hv.language && this.graph.language_code != hv.language) {
this.loadNarrative(hv.language); this.loadNarrative(hv.language);
} }
} }
// let varEl = document.getElementById("variables"); // let varEl = document.getElementById("variables");
// varEl.innerHTML = ""; // varEl.innerHTML = "";
} }
if(this.hasGraph) { if(this.hasGraph) {
this.graph.updateHugveyStatus(hv); this.graph.updateHugveyStatus(hv);
} }
} }
getHugvey(id) { getHugvey(id) {
for(let hv of this.hugveys.hugveys) { for(let hv of this.hugveys.hugveys) {
if(hv.id == id) { if(hv.id == id) {
@ -206,7 +206,7 @@ class Panopticon {
let graph = this.graph; let graph = this.graph;
req.addEventListener( "load", function( e ) { req.addEventListener( "load", function( e ) {
graph.loadData( JSON.parse( this.response ), code ); graph.loadData( JSON.parse( this.response ), code );
// console.log(, e); // console.log(, e);
} ); } );
req.open( "GET", "/local/" + file ); req.open( "GET", "/local/" + file );
req.send(); req.send();
@ -236,7 +236,7 @@ class Panopticon {
console.log("Light", hv_id, light_id); console.log("Light", hv_id, light_id);
this.send( { action: 'change_light', hugvey: hv_id, light_id: light_id } ); this.send( { action: 'change_light', hugvey: hv_id, light_id: light_id } );
} }
playFromSelected(msg_id, reloadStory) { playFromSelected(msg_id, reloadStory) {
if(!this.hugveys.selectedId) { if(!this.hugveys.selectedId) {
alert('No hugvey selected'); alert('No hugvey selected');
@ -337,17 +337,17 @@ class Graph {
// used eg. after a condition creation. // used eg. after a condition creation.
this.showMsg( this.selectedMsg ); this.showMsg( this.selectedMsg );
} }
getAudioUrlForMsg(msg) { getAudioUrlForMsg(msg) {
let isVariable = msg['text'].includes('$') ? '1' : '0'; let isVariable = msg['text'].includes('$') ? '1' : '0';
let lang = panopticon.graph.language_code; let lang = panopticon.graph.language_code;
return `http://localhost:8888/voice?text=${encodeURIComponent(msg['text'])}&variable=${isVariable}&lang=${lang}&filename=0`; return `http://localhost:8888/voice?text=${encodeURIComponent(msg['text'])}&variable=${isVariable}&lang=${lang}&filename=0`;
} }
getConfig() { getConfig() {
} }
getNumericId(prefix) { getNumericId(prefix) {
let id, i = 0; let id, i = 0;
let hasId= function(a, id) { let hasId= function(a, id) {
@ -362,10 +362,10 @@ class Graph {
id = prefix + i; id = prefix + i;
i++; i++;
} while(hasId(this.data, id)) } while(hasId(this.data, id))
return id; return id;
} }
createDiversion(type) { createDiversion(type) {
let div = { let div = {
"@id": this.getNumericId(this.language_code.substring( 0, 2 ) + `-div-${type}#`), "@id": this.getNumericId(this.language_code.substring( 0, 2 ) + `-div-${type}#`),
@ -373,7 +373,7 @@ class Graph {
'type': type, 'type': type,
'params': {} 'params': {}
} }
if(type == 'no_response') { if(type == 'no_response') {
div['params']['consecutiveSilences'] = 3; div['params']['consecutiveSilences'] = 3;
div['params']['timesOccured'] = 0; div['params']['timesOccured'] = 0;
@ -399,33 +399,33 @@ class Graph {
div['params']['msgId'] = ""; div['params']['msgId'] = "";
} }
else if(type == 'repeat') { else if(type == 'repeat') {
div['params']['regex'] = "can you repeat that\\?"; div['params']['regex'] = "can you repeat that\\?";
} else { } else {
console.log("invalid type", type); console.log("invalid type", type);
alert('invalid type for diversion'); alert('invalid type for diversion');
} }
if(type != 'repeat' && type != 'interrupt') { if(type != 'repeat' && type != 'interrupt') {
div['params']['notAfterMsgId'] = ""; div['params']['notAfterMsgId'] = "";
} }
this.data.push( div ); this.data.push( div );
this.updateFromData(); this.updateFromData();
this.build(); this.build();
this.showDiversions(); this.showDiversions();
return msg; return msg;
} }
deleteDiversion(div) { deleteDiversion(div) {
this._rmNode( div ); this._rmNode( div );
this.showDiversions( ); this.showDiversions( );
} }
showDiversions( ) { showDiversions( ) {
let msgEl = document.getElementById( 'msg' ); let msgEl = document.getElementById( 'msg' );
msgEl.innerHTML = ""; msgEl.innerHTML = "";
let divsNoResponse =[], divsRepeat = [], divsReplyContains = [], divsTimeouts = [], divsInterrupts = []; let divsNoResponse =[], divsRepeat = [], divsReplyContains = [], divsTimeouts = [], divsInterrupts = [];
for(let div of this.diversions) { for(let div of this.diversions) {
@ -448,7 +448,7 @@ class Graph {
}}, ...notMsgOptions) }}, ...notMsgOptions)
); );
} }
if(div['type'] == 'no_response') { if(div['type'] == 'no_response') {
let returnAttrs = { let returnAttrs = {
'type': 'checkbox', 'type': 'checkbox',
@ -468,7 +468,7 @@ class Graph {
} }
msgOptions.push(crel('option', optionParams , startMsg['@id'])); msgOptions.push(crel('option', optionParams , startMsg['@id']));
} }
divsNoResponse.push(crel( divsNoResponse.push(crel(
'div', { 'div', {
'class': 'diversion', 'class': 'diversion',
@ -539,7 +539,7 @@ class Graph {
} }
msgOptions.push(crel('option', optionParams , startMsg['@id'])); msgOptions.push(crel('option', optionParams , startMsg['@id']));
} }
divsReplyContains.push(crel( divsReplyContains.push(crel(
'div', { 'div', {
'class': 'diversion', 'class': 'diversion',
@ -606,7 +606,7 @@ class Graph {
), ),
notAfterMsgIdEl notAfterMsgIdEl
)); ));
} }
if(div['type'] == 'timeout') { if(div['type'] == 'timeout') {
let returnAttrs = { let returnAttrs = {
'type': 'checkbox', 'type': 'checkbox',
@ -617,7 +617,7 @@ class Graph {
if(div['params']['returnAfterStrand']) { if(div['params']['returnAfterStrand']) {
returnAttrs['checked'] = 'checked'; returnAttrs['checked'] = 'checked';
} }
let totalOrLocalAttrs = { let totalOrLocalAttrs = {
'type': 'checkbox', 'type': 'checkbox',
'on': { 'on': {
@ -627,7 +627,7 @@ class Graph {
if(div['params']['fromLastMessage']) { if(div['params']['fromLastMessage']) {
totalOrLocalAttrs['checked'] = 'checked'; totalOrLocalAttrs['checked'] = 'checked';
} }
let msgOptions = [crel('option',"")]; let msgOptions = [crel('option',"")];
let starts = this.messages.filter( m => m.hasOwnProperty('start') && m['start'] == true); let starts = this.messages.filter( m => m.hasOwnProperty('start') && m['start'] == true);
for(let startMsg of starts) { for(let startMsg of starts) {
@ -637,7 +637,7 @@ class Graph {
} }
msgOptions.push(crel('option', optionParams , startMsg['@id'])); msgOptions.push(crel('option', optionParams , startMsg['@id']));
} }
divsTimeouts.push(crel( divsTimeouts.push(crel(
'div', { 'div', {
'class': 'diversion', 'class': 'diversion',
@ -734,7 +734,7 @@ class Graph {
} }
msgOptions.push(crel('option', optionParams , startMsg['@id'])); msgOptions.push(crel('option', optionParams , startMsg['@id']));
} }
divsInterrupts.push(crel( divsInterrupts.push(crel(
'div', {'class': 'diversion'}, 'div', {'class': 'diversion'},
crel('h3', div['@id']), crel('h3', div['@id']),
@ -753,9 +753,9 @@ class Graph {
)); ));
} }
} }
console.log(divsReplyContains, divsNoResponse, divsRepeat, divsTimeouts, divsInterrupts); console.log(divsReplyContains, divsNoResponse, divsRepeat, divsTimeouts, divsInterrupts);
let divEl = crel( let divEl = crel(
'div', 'div',
{ {
@ -797,7 +797,7 @@ class Graph {
'on': { 'on': {
'click': (e) => this.createDiversion('repeat') 'click': (e) => this.createDiversion('repeat')
} }
}, },
'New case for repeat' 'New case for repeat'
) )
), ),
@ -810,7 +810,7 @@ class Graph {
'on': { 'on': {
'click': (e) => this.createDiversion('timeout') 'click': (e) => this.createDiversion('timeout')
} }
}, },
'New case for timeout' 'New case for timeout'
) )
) )
@ -824,12 +824,12 @@ class Graph {
// 'on': { // 'on': {
// 'click': (e) => this.createDiversion('interrupt') // 'click': (e) => this.createDiversion('interrupt')
// } // }
// }, // },
// 'New case for Interrupt' // 'New case for Interrupt'
// ) // )
// ) // )
); );
msgEl.appendChild(divEl); msgEl.appendChild(divEl);
} }
@ -904,19 +904,19 @@ class Graph {
}) })
) )
); );
document.getElementById("interface").appendChild(configEl); document.getElementById("interface").appendChild(configEl);
} }
showMsg( msg ) { showMsg( msg ) {
let msgEl = document.getElementById( 'msg' ); let msgEl = document.getElementById( 'msg' );
msgEl.innerHTML = ""; msgEl.innerHTML = "";
if(msg == null){ if(msg == null){
return; return;
} }
let startAttributes = { let startAttributes = {
'name': msg['@id'] + '-start', 'name': msg['@id'] + '-start',
// 'readonly': 'readonly', // 'readonly': 'readonly',
@ -939,7 +939,7 @@ class Graph {
if ( msg['beginning'] == true ) { if ( msg['beginning'] == true ) {
beginningAttributes['checked'] = 'checked'; beginningAttributes['checked'] = 'checked';
} }
// chapter marker: // chapter marker:
let chapterAttributes = { let chapterAttributes = {
'name': msg['@id'] + '-chapterStart', 'name': msg['@id'] + '-chapterStart',
@ -952,14 +952,14 @@ class Graph {
if ( typeof msg['chapterStart'] !== 'undefined' && msg['chapterStart'] == true ) { if ( typeof msg['chapterStart'] !== 'undefined' && msg['chapterStart'] == true ) {
chapterAttributes['checked'] = 'checked'; chapterAttributes['checked'] = 'checked';
} }
let params = {}; let params = {};
if(msg.hasOwnProperty('params')) { if(msg.hasOwnProperty('params')) {
params = msg['params']; params = msg['params'];
} else { } else {
msg['params'] = {}; msg['params'] = {};
} }
let audioSrcEl = crel('source', {'src': msg['audio'] ? msg['audio']['file'] : this.getAudioUrlForMsg(msg)}); let audioSrcEl = crel('source', {'src': msg['audio'] ? msg['audio']['file'] : this.getAudioUrlForMsg(msg)});
let audioSpan = crel( let audioSpan = crel(
'span', 'span',
@ -1105,7 +1105,7 @@ class Graph {
} }
} ) } )
), ),
// color for beter overview // color for beter overview
crel( 'label', crel( 'label',
@ -1123,9 +1123,9 @@ class Graph {
) )
); );
msgEl.appendChild( msgInfoEl ); msgEl.appendChild( msgInfoEl );
if(panopticon.hugveys.selectedId) { if(panopticon.hugveys.selectedId) {
msgEl.appendChild(crel( msgEl.appendChild(crel(
'div', 'div',
@ -1161,9 +1161,9 @@ class Graph {
"Continue on #" + panopticon.hugveys.selectedId "Continue on #" + panopticon.hugveys.selectedId
) )
)); ));
} }
// let directionHEl = document.createElement('h2'); // let directionHEl = document.createElement('h2');
// directionHEl.innerHTML = "Directions"; // directionHEl.innerHTML = "Directions";
@ -1220,13 +1220,13 @@ class Graph {
'on': { 'on': {
'click': ( e ) => { 'click': ( e ) => {
if(confirm("Do you want to remove this direction and its conditions?")) { if(confirm("Do you want to remove this direction and its conditions?")) {
g.rmDirection( direction ); g.rmDirection( direction );
} }
} }
} }
}, 'disconnect') }, 'disconnect')
); );
for ( let conditionId of direction['conditions'] ) { for ( let conditionId of direction['conditions'] ) {
let condition = this.getNodeById( conditionId ); let condition = this.getNodeById( conditionId );
directionEl.appendChild( this.getEditConditionFormEl( condition, direction ) ); directionEl.appendChild( this.getEditConditionFormEl( condition, direction ) );
@ -1246,7 +1246,7 @@ class Graph {
'click': ( e ) => { 'click': ( e ) => {
if(confirm("Do you want to remove this condition?")) { if(confirm("Do you want to remove this condition?")) {
// console.log('remove condition for direction', condition, direction); // console.log('remove condition for direction', condition, direction);
panopticon.graph.rmCondition( condition, direction ); panopticon.graph.rmCondition( condition, direction );
} }
} }
} }
@ -1264,7 +1264,7 @@ class Graph {
} ); } );
labelLabel.appendChild( labelInput ); labelLabel.appendChild( labelInput );
conditionEl.appendChild( labelLabel ); conditionEl.appendChild( labelLabel );
// for ( let v in condition['vars'] ) { // for ( let v in condition['vars'] ) {
// let varLabel = document.createElement( 'label' ); // let varLabel = document.createElement( 'label' );
@ -1309,14 +1309,14 @@ class Graph {
} }
}; };
} }
getConditionInputsForType( type, conditionId, values ) { getConditionInputsForType( type, conditionId, values ) {
let inputs = []; let inputs = [];
let vars = this.getConditionTypes()[type]; let vars = this.getConditionTypes()[type];
for ( let v in vars ) { for ( let v in vars ) {
let attr = vars[v]; let attr = vars[v];
let inputType = attr.hasOwnProperty('tag') ? attr['tag'] : 'input'; let inputType = attr.hasOwnProperty('tag') ? attr['tag'] : 'input';
attr['name'] = typeof conditionId == 'undefined' ? v : `${conditionId}-vars.${v}`; attr['name'] = typeof conditionId == 'undefined' ? v : `${conditionId}-vars.${v}`;
if(typeof values != 'undefined') { if(typeof values != 'undefined') {
let value = this._getValueForPath(v, values); let value = this._getValueForPath(v, values);
@ -1326,12 +1326,12 @@ class Graph {
} }
attr['value'] = typeof value == 'undefined' ? "": value; attr['value'] = typeof value == 'undefined' ? "": value;
attr['on'] = { attr['on'] = {
'change': this.getEditEventListener() 'change': this.getEditEventListener()
} ; } ;
} else { } else {
// console.log(attr); // console.log(attr);
} }
inputs.push( inputs.push(
crel( 'label', crel( 'label',
crel( 'span', { crel( 'span', {
@ -1352,7 +1352,7 @@ class Graph {
conditionForm.appendChild(i); conditionForm.appendChild(i);
} }
} }
_getValueForPath(path, vars) { _getValueForPath(path, vars) {
path = path.split( '.' ); // use vars.test to set ['vars']['test'] = value path = path.split( '.' ); // use vars.test to set ['vars']['test'] = value
let v = vars; let v = vars;
@ -1372,7 +1372,7 @@ class Graph {
} }
return result; return result;
} }
/** /**
* Save an array path (string) with a value to an object. Used to turn * Save an array path (string) with a value to an object. Used to turn
* strings into nested arrays * strings into nested arrays
@ -1421,7 +1421,7 @@ class Graph {
form.delete( 'type' ); form.delete( 'type' );
let label = form.get( 'label' ); let label = form.get( 'label' );
form.delete( 'label' ); form.delete( 'label' );
// checkboxes to true/false // checkboxes to true/false
let defs = g.getConditionTypes()[type]; let defs = g.getConditionTypes()[type];
// console.log(defs); // console.log(defs);
@ -1432,13 +1432,13 @@ class Graph {
form.set(field, form.has(field)); form.set(field, form.has(field));
} }
} }
let vars = {}; let vars = {};
for ( var pair of form.entries() ) { for ( var pair of form.entries() ) {
// FormData only has strings & blobs, we want booleans: // FormData only has strings & blobs, we want booleans:
if(pair[1] === 'true') pair[1] = true; if(pair[1] === 'true') pair[1] = true;
if(pair[1] === 'false') pair[1] = false; if(pair[1] === 'false') pair[1] = false;
vars = g._formPathToVars(pair[0], pair[1], vars); vars = g._formPathToVars(pair[0], pair[1], vars);
} }
// TODO: checkboxes // TODO: checkboxes
@ -1481,7 +1481,7 @@ class Graph {
return addConditionEl; return addConditionEl;
} }
/** /**
* remove condition from the graph or merely from the given direction * remove condition from the graph or merely from the given direction
* @param {any} condition The condition to remove * @param {any} condition The condition to remove
@ -1496,7 +1496,7 @@ class Graph {
if(pos > -1) { if(pos > -1) {
direction['conditions'].splice(pos, 1); direction['conditions'].splice(pos, 1);
} }
for(let dir of this.directions) { for(let dir of this.directions) {
// console.log('check if condition exists for dir', dir) // console.log('check if condition exists for dir', dir)
if(dir['conditions'].indexOf(id) > -1) { if(dir['conditions'].indexOf(id) > -1) {
@ -1547,14 +1547,14 @@ class Graph {
"afterrunTime": 0.5, "afterrunTime": 0.5,
} }
this.data.push( msg ); this.data.push( msg );
console.log("skip or not to skip?", skipRebuild); console.log("skip or not to skip?", skipRebuild);
if(typeof skipRebuild == 'undefined' || !skipRebuild) { if(typeof skipRebuild == 'undefined' || !skipRebuild) {
this.updateFromData(); this.updateFromData();
this.build(); this.build();
this.selectMsg(msg); this.selectMsg(msg);
} }
return msg; return msg;
} }
@ -1611,7 +1611,7 @@ class Graph {
"conditions": [] "conditions": []
} }
this.data.push( dir ); this.data.push( dir );
let skipDistances; let skipDistances;
// orphaned target and source has no other destinations. We can copy the vertical position: // orphaned target and source has no other destinations. We can copy the vertical position:
if(this.getDirectionsFrom( source ).length < 1 && this.getDirectionsFrom( target ).length < 1 && this.getDirectionsTo( target ).length < 1) { if(this.getDirectionsFrom( source ).length < 1 && this.getDirectionsFrom( target ).length < 1 && this.getDirectionsTo( target ).length < 1) {
@ -1620,11 +1620,11 @@ class Graph {
let d = [distance[0] + 1, distance[1]]; let d = [distance[0] + 1, distance[1]];
// create a distance based on source's position // create a distance based on source's position
// this saves us from running the slow calculateDistancesFromStart // this saves us from running the slow calculateDistancesFromStart
this.distances[target['@id']] = d; this.distances[target['@id']] = d;
} else { } else {
skipDistances = false; skipDistances = false;
} }
this.updateFromData(skipDistances); this.updateFromData(skipDistances);
this.build(); this.build();
return dir; return dir;
@ -1639,18 +1639,18 @@ class Graph {
this.addMsg(); this.addMsg();
this.build(); this.build();
} }
createConnectedMsg(sourceMsg) { createConnectedMsg(sourceMsg) {
console.time('createConnected'); console.time('createConnected');
console.time("Add"); console.time("Add");
let newMsg = this.addMsg(true); // skipRebuild = true, as addDirection() already rebuilds the graph let newMsg = this.addMsg(true); // skipRebuild = true, as addDirection() already rebuilds the graph
this.getNodeById(newMsg['@id']).y = this.getNodeById(sourceMsg['@id']).y; this.getNodeById(newMsg['@id']).y = this.getNodeById(sourceMsg['@id']).y;
if(this.getNodeById(sourceMsg['@id']).hasOwnProperty('color')){ if(this.getNodeById(sourceMsg['@id']).hasOwnProperty('color')){
this.getNodeById(newMsg['@id']).color = this.getNodeById(sourceMsg['@id']).color this.getNodeById(newMsg['@id']).color = this.getNodeById(sourceMsg['@id']).color
} }
console.timeEnd("Add"); console.timeEnd("Add");
console.time("direction"); console.time("direction");
this.addDirection(sourceMsg, newMsg); this.addDirection(sourceMsg, newMsg);
console.timeEnd("direction"); console.timeEnd("direction");
@ -1677,15 +1677,15 @@ class Graph {
let graph = this; let graph = this;
let el = function( e ) { let el = function( e ) {
console.info("Changed", e); console.info("Changed", e);
let parts = e.srcElement.name.split( '-' ); let parts = e.target.name.split( '-' );
let field = parts.pop(); let field = parts.pop();
let id = parts.join('-'); let id = parts.join('-');
let node = graph.getNodeById( id ); let node = graph.getNodeById( id );
let path = field.split( '.' ); // use vars.test to set ['vars']['test'] = value let path = field.split( '.' ); // use vars.test to set ['vars']['test'] = value
var res = node; var res = node;
let value = e.srcElement.value let value = e.target.value
if(e.srcElement.type == 'checkbox') { if(e.target.type == 'checkbox') {
value = e.srcElement.checked; value = e.target.checked;
} }
for ( var i = 0; i < path.length; i++ ) { for ( var i = 0; i < path.length; i++ ) {
if ( i == ( path.length - 1 ) ) { if ( i == ( path.length - 1 ) ) {
@ -1698,7 +1698,7 @@ class Graph {
// node[field] = e.srcElement.value; // node[field] = e.srcElement.value;
graph.build(); graph.build();
if(typeof callback !== 'undefined'){ if(typeof callback !== 'undefined'){
callback(); callback();
} }
@ -1769,11 +1769,11 @@ class Graph {
console.info("Save json", formData ); console.info("Save json", formData );
var request = new XMLHttpRequest(); var request = new XMLHttpRequest();
request.open( "POST", "http://localhost:8888/upload" ); request.open( "POST", "http://localhost:8888/upload" );
if(callback) { if(callback) {
request.addEventListener( "load", callback); request.addEventListener( "load", callback);
} }
request.send( formData ); request.send( formData );
} }
@ -1789,13 +1789,13 @@ class Graph {
this.directions = this.data.filter(( node ) => node['@type'] == 'Direction' ); this.directions = this.data.filter(( node ) => node['@type'] == 'Direction' );
this.conditions = this.data.filter(( node ) => node['@type'] == 'Condition' ); this.conditions = this.data.filter(( node ) => node['@type'] == 'Condition' );
this.diversions = this.data.filter(( node ) => node['@type'] == 'Diversion' ); this.diversions = this.data.filter(( node ) => node['@type'] == 'Diversion' );
let configurations = this.data.filter(( node ) => node['@type'] == 'Configuration' ); let configurations = this.data.filter(( node ) => node['@type'] == 'Configuration' );
this.configuration = configurations.length > 0 ? configurations[0] : { this.configuration = configurations.length > 0 ? configurations[0] : {
"@id": "config", "@id": "config",
"@type": "Configuration" "@type": "Configuration"
}; };
document.getElementById('current_lang').innerHTML = ""; document.getElementById('current_lang').innerHTML = "";
document.getElementById('current_lang').appendChild(crel('span', { document.getElementById('current_lang').appendChild(crel('span', {
'class': 'flag-icon ' + this.language_code 'class': 'flag-icon ' + this.language_code
@ -1803,7 +1803,7 @@ class Graph {
let storyEl = document.getElementById('story'); let storyEl = document.getElementById('story');
storyEl.classList.remove(... panopticon.languages.map((l) => l['code'])) storyEl.classList.remove(... panopticon.languages.map((l) => l['code']))
storyEl.classList.add(this.language_code); storyEl.classList.add(this.language_code);
if(typeof skipDistances == 'undefined' || !skipDistances) { if(typeof skipDistances == 'undefined' || !skipDistances) {
this.distances = this.calculateDistancesFromStart(); this.distances = this.calculateDistancesFromStart();
} }
@ -1811,7 +1811,7 @@ class Graph {
// save state; // save state;
this.saveState(); this.saveState();
} }
updateHugveyStatus(hv) { updateHugveyStatus(hv) {
let els = document.getElementsByClassName('beenHit'); let els = document.getElementsByClassName('beenHit');
while(els.length > 0) { while(els.length > 0) {
@ -1820,11 +1820,11 @@ class Graph {
if(!hv || typeof hv['history'] == 'undefined') { if(!hv || typeof hv['history'] == 'undefined') {
return; return;
} }
if(hv['history'].hasOwnProperty('messages')){ if(hv['history'].hasOwnProperty('messages')){
for(let msg of hv['history']['messages']) { for(let msg of hv['history']['messages']) {
document.getElementById(msg[0]['id']).classList.add('beenHit'); document.getElementById(msg[0]['id']).classList.add('beenHit');
} }
} }
if(hv['history'].hasOwnProperty('directions')){ if(hv['history'].hasOwnProperty('directions')){
for(let msg of hv['history']['directions']) { for(let msg of hv['history']['directions']) {
@ -1857,7 +1857,7 @@ class Graph {
return fx; return fx;
}).strength(50)) }).strength(50))
.force( "forceY", d3.forceY(function(m){ .force( "forceY", d3.forceY(function(m){
// if(panopticon.graph.distances[m['@id']] !== null ) // if(panopticon.graph.distances[m['@id']] !== null )
// console.log(panopticon.graph.distances[m['@id']][1]); // console.log(panopticon.graph.distances[m['@id']][1]);
let fy = panopticon.graph.distances[m['@id']] !== null ? panopticon.graph.distances[m['@id']][1] * panopticon.graph.nodeSize * 3: 0 let fy = panopticon.graph.distances[m['@id']] !== null ? panopticon.graph.distances[m['@id']][1] * panopticon.graph.nodeSize * 3: 0
// console.log('fx', m['@id'], panopticon.graph.distances[m['@id']], fx); // console.log('fx', m['@id'], panopticon.graph.distances[m['@id']], fx);
@ -1872,7 +1872,7 @@ class Graph {
.selectAll( "g" ) .selectAll( "g" )
.data( this.messages, n => n['@id'] ) .data( this.messages, n => n['@id'] )
; ;
// Update existing nodes // Update existing nodes
let newNode = node.enter(); let newNode = node.enter();
@ -1887,7 +1887,7 @@ class Graph {
.attr( 'r', this.nodeSize ) .attr( 'r', this.nodeSize )
// .text(d => d.id) // .text(d => d.id)
; ;
let textId = newNodeG.append( "text" ).attr( 'class', 'msg_id' ); let textId = newNodeG.append( "text" ).attr( 'class', 'msg_id' );
let textContent = newNodeG.append( "text" ).attr( 'class', 'msg_txt' ); let textContent = newNodeG.append( "text" ).attr( 'class', 'msg_txt' );
let statusIcon = newNodeG.append( "image" ) let statusIcon = newNodeG.append( "image" )
@ -1901,7 +1901,7 @@ class Graph {
// remove // remove
node.exit().remove(); node.exit().remove();
node = node.merge( newNodeG ); node = node.merge( newNodeG );
// for all existing nodes: // for all existing nodes:
node.attr( 'class', msg => { node.attr( 'class', msg => {
@ -1916,9 +1916,9 @@ class Graph {
return classes.join( ' ' ); return classes.join( ' ' );
} ) } )
.on(".drag", null) .on(".drag", null)
.call( .call(
d3.drag( this.simulation ) d3.drag( this.simulation )
.on("start", function(d){ .on("start", function(d){
if (!d3.event.active) panopticon.graph.simulation.alphaTarget(0.3).restart(); if (!d3.event.active) panopticon.graph.simulation.alphaTarget(0.3).restart();
@ -1935,9 +1935,9 @@ class Graph {
d.fy = null; d.fy = null;
}) })
// .container(document.getElementById('container')) // .container(document.getElementById('container'))
); );
node.select('circle').attr('style', (d) => 'fill: ' + (d.hasOwnProperty('color') ? d['color'] : '#77618e')); node.select('circle').attr('style', (d) => 'fill: ' + (d.hasOwnProperty('color') ? d['color'] : '#77618e'));
let link = this.linkG let link = this.linkG
@ -1947,7 +1947,7 @@ class Graph {
let newLink = link.enter() let newLink = link.enter()
.append( "line" ) .append( "line" )
; ;
//remove //remove
link.exit().remove(); link.exit().remove();
link = link.merge( newLink ); link = link.merge( newLink );
@ -2032,7 +2032,7 @@ class Graph {
} }
return this.svg.node(); return this.svg.node();
} }
calculateDistancesFromStart() { calculateDistancesFromStart() {
console.time('calculateDistancesFromStart'); console.time('calculateDistancesFromStart');
let starts = this.messages.filter( m => m.hasOwnProperty('start') && m['start'] == true); let starts = this.messages.filter( m => m.hasOwnProperty('start') && m['start'] == true);
@ -2040,26 +2040,26 @@ class Graph {
console.error("No start set"); console.error("No start set");
return; return;
} }
//initiate distances //initiate distances
let distances = {}; let distances = {};
for(let msg of this.messages) { for(let msg of this.messages) {
// distances[msg['@id']] = msg === startMsg ? 0 : null; // distances[msg['@id']] = msg === startMsg ? 0 : null;
distances[msg['@id']] = null; distances[msg['@id']] = null;
} }
let targetsPerMsg = {}; let targetsPerMsg = {};
let sourcesPerMsg = {}; let sourcesPerMsg = {};
// console.log("dir", this.directions); // console.log("dir", this.directions);
for(let direction of this.directions) { for(let direction of this.directions) {
let from = typeof direction['source'] == "string" ? direction['source'] : direction['source']['@id']; let from = typeof direction['source'] == "string" ? direction['source'] : direction['source']['@id'];
let to = typeof direction['target'] == "string" ? direction['target'] : direction['target']['@id']; let to = typeof direction['target'] == "string" ? direction['target'] : direction['target']['@id'];
if(!targetsPerMsg.hasOwnProperty(from)) { if(!targetsPerMsg.hasOwnProperty(from)) {
targetsPerMsg[from] = []; targetsPerMsg[from] = [];
} }
targetsPerMsg[from].push(to); targetsPerMsg[from].push(to);
if(!sourcesPerMsg.hasOwnProperty(to)) { if(!sourcesPerMsg.hasOwnProperty(to)) {
sourcesPerMsg[to] = []; sourcesPerMsg[to] = [];
@ -2074,21 +2074,21 @@ class Graph {
// end of trail // end of trail
return yPos; return yPos;
} }
let i = 0, y =0; let i = 0, y =0;
for(let childMsgId of msgsPerMsg[msgId]) { for(let childMsgId of msgsPerMsg[msgId]) {
if(distances[childMsgId] !== null){ if(distances[childMsgId] !== null){
continue; continue;
} }
if(distances[childMsgId] === null || (goingDown && distances[childMsgId][0] > depth)) { if(distances[childMsgId] === null || (goingDown && distances[childMsgId][0] > depth)) {
if(distances[childMsgId] === null) { if(distances[childMsgId] === null) {
if(i > 0){ if(i > 0){
yPos++; yPos++;
} }
i++; i++;
console.log('set for id', childMsgId, goingDown, depth, yPos); console.log('set for id', childMsgId, goingDown, depth, yPos);
distances[childMsgId] = [depth, yPos]; distances[childMsgId] = [depth, yPos];
@ -2106,7 +2106,7 @@ class Graph {
if(distances[childMsgId] === null) { if(distances[childMsgId] === null) {
distances[childMsgId] = [depth, yPos]; distances[childMsgId] = [depth, yPos];
} }
// console.log('a', depth); // console.log('a', depth);
yPos = traverseMsg(childMsgId, depth - 1, goingDown, yPos); yPos = traverseMsg(childMsgId, depth - 1, goingDown, yPos);
} else { } else {
@ -2114,7 +2114,7 @@ class Graph {
} }
} }
// if( i == 0 && y == 1) { // if( i == 0 && y == 1) {
// // we reached an item that branches back into the tree // // we reached an item that branches back into the tree
// return yPos -1; // return yPos -1;
@ -2122,7 +2122,7 @@ class Graph {
// console.log('yPos',msgId,yPos); // console.log('yPos',msgId,yPos);
return yPos; return yPos;
} }
let yPos = 0; let yPos = 0;
console.time('step1'); console.time('step1');
for(let startMsg of starts) { for(let startMsg of starts) {
@ -2148,7 +2148,7 @@ class Graph {
console.timeEnd('polish: '+ msgId); console.timeEnd('polish: '+ msgId);
} }
console.timeEnd('step2'); console.timeEnd('step2');
// let additionalsDepth = 0; // let additionalsDepth = 0;
//// now the secondary strands: //// now the secondary strands:
// for(let msgId in distances) { // for(let msgId in distances) {
@ -2158,11 +2158,11 @@ class Graph {
// } // }
// distances[msgId] = additionalsDepth; // distances[msgId] = additionalsDepth;
// traverseMsg(msgId, additionalsDepth+1, true); // traverseMsg(msgId, additionalsDepth+1, true);
// //
// } // }
console.timeEnd("calculateDistancesFromStart"); console.timeEnd("calculateDistancesFromStart");
return distances; return distances;
} }
} }
// //
// //