Running a very basic Story for the hugveys
This commit is contained in:
parent
f5f08fc103
commit
1ea85dd490
13 changed files with 12828 additions and 164 deletions
|
@ -17,13 +17,33 @@ from hugvey.story import Story
|
||||||
from hugvey.voice.google import GoogleVoiceClient
|
from hugvey.voice.google import GoogleVoiceClient
|
||||||
from hugvey.voice.player import Player
|
from hugvey.voice.player import Player
|
||||||
from hugvey.voice.streamer import AudioStreamer
|
from hugvey.voice.streamer import AudioStreamer
|
||||||
|
import queue
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger("command")
|
logger = logging.getLogger("command")
|
||||||
|
|
||||||
|
# def exceptionEmitter(a):
|
||||||
|
# print(a)
|
||||||
|
# def decorate(func):
|
||||||
|
# print('decorate')
|
||||||
|
# async def call(*args, **kwargs):
|
||||||
|
# print('call')
|
||||||
|
# # pre(func, *args, **kwargs)
|
||||||
|
# try:
|
||||||
|
# result = await func(*args, **kwargs)
|
||||||
|
# except Exception as e:
|
||||||
|
# logger.critical(e, "in", func)
|
||||||
|
# raise e
|
||||||
|
# # post(func, *args, **kwargs)
|
||||||
|
# return result
|
||||||
|
# return call
|
||||||
|
# return decorate
|
||||||
|
|
||||||
|
|
||||||
class CentralCommand(object):
|
class CentralCommand(object):
|
||||||
"""docstring for CentralCommand."""
|
"""docstring for CentralCommand."""
|
||||||
def __init__(self, debug_mode = False):
|
|
||||||
|
def __init__(self, debug_mode=False):
|
||||||
self.debug = debug_mode
|
self.debug = debug_mode
|
||||||
self.eventQueue = asyncio.Queue()
|
self.eventQueue = asyncio.Queue()
|
||||||
self.commandQueue = asyncio.Queue()
|
self.commandQueue = asyncio.Queue()
|
||||||
|
@ -31,7 +51,7 @@ class CentralCommand(object):
|
||||||
self.hugveys = {}
|
self.hugveys = {}
|
||||||
self.ctx = Context.instance()
|
self.ctx = Context.instance()
|
||||||
self.hugveyLock = asyncio.Lock()
|
self.hugveyLock = asyncio.Lock()
|
||||||
|
self.start_time = time.time()
|
||||||
|
|
||||||
def loadConfig(self, filename):
|
def loadConfig(self, filename):
|
||||||
if hasattr(self, 'config'):
|
if hasattr(self, 'config'):
|
||||||
|
@ -41,7 +61,7 @@ class CentralCommand(object):
|
||||||
logger.debug('Load config from {}'.format(filename))
|
logger.debug('Load config from {}'.format(filename))
|
||||||
self.config = yaml.safe_load(fp)
|
self.config = yaml.safe_load(fp)
|
||||||
|
|
||||||
self.hugvey_ids = [i+1 for i in range(self.config['hugveys'])]
|
self.hugvey_ids = [i + 1 for i in range(self.config['hugveys'])]
|
||||||
|
|
||||||
# load languages:
|
# load languages:
|
||||||
self.languages = {}
|
self.languages = {}
|
||||||
|
@ -52,6 +72,33 @@ class CentralCommand(object):
|
||||||
|
|
||||||
self.panopticon = Panopticon(self, self.config)
|
self.panopticon = Panopticon(self, self.config)
|
||||||
|
|
||||||
|
def getHugveyStatus(self, hv_id):
|
||||||
|
status = {'id': hv_id}
|
||||||
|
if not hv_id in self.hugveys:
|
||||||
|
status['status'] = 'off'
|
||||||
|
return status
|
||||||
|
|
||||||
|
hv = self.hugveys[hv_id]
|
||||||
|
status['status'] = 'running' if hv.isRunning.is_set() else 'paused'
|
||||||
|
status['language'] = hv.language_code
|
||||||
|
status['msg'] = hv.story.currentMessage.id
|
||||||
|
status['counts'] = hv.story.getStoryCounts()
|
||||||
|
status['finished'] = hv.story.isFinished()
|
||||||
|
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
|
def getStatusSummary(self):
|
||||||
|
status = {
|
||||||
|
'uptime': time.time() - self.start_time,
|
||||||
|
'languages': self.config['languages'],
|
||||||
|
'hugveys': [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for hv_id in self.hugvey_ids:
|
||||||
|
status['hugveys'].append(self.getHugveyStatus(hv_id))
|
||||||
|
|
||||||
|
return status
|
||||||
|
|
||||||
def commandHugvey(self, hv_id, msg):
|
def commandHugvey(self, hv_id, msg):
|
||||||
"""
|
"""
|
||||||
|
@ -59,8 +106,9 @@ class CentralCommand(object):
|
||||||
"""
|
"""
|
||||||
if threading.current_thread().getName() != 'MainThread':
|
if threading.current_thread().getName() != 'MainThread':
|
||||||
# Threading nightmares! Adding to queue from other thread/loop (not sure which is the isse)
|
# Threading nightmares! Adding to queue from other thread/loop (not sure which is the isse)
|
||||||
# won't trigger asyncios queue.get() so we have to do this thread safe, in the right loop
|
# won't trigger asyncios queue.get() so we have to do this thread
|
||||||
self.loop.call_soon_threadsafe( self._queueCommand, hv_id, msg )
|
# safe, in the right loop
|
||||||
|
self.loop.call_soon_threadsafe(self._queueCommand, hv_id, msg)
|
||||||
else:
|
else:
|
||||||
self._queueCommand(hv_id, msg)
|
self._queueCommand(hv_id, msg)
|
||||||
|
|
||||||
|
@ -89,8 +137,10 @@ class CentralCommand(object):
|
||||||
|
|
||||||
# sleep to allow pending connections to connect
|
# sleep to allow pending connections to connect
|
||||||
await asyncio.sleep(1)
|
await asyncio.sleep(1)
|
||||||
logger.info("Ready to publish commands on: {}".format(self.config['events']['cmd_address']))
|
logger.info("Ready to publish commands on: {}".format(
|
||||||
logger.debug('Already {} items in queue'.format(self.commandQueue.qsize()))
|
self.config['events']['cmd_address']))
|
||||||
|
logger.debug('Already {} items in queue'.format(
|
||||||
|
self.commandQueue.qsize()))
|
||||||
|
|
||||||
while self.isRunning.is_set():
|
while self.isRunning.is_set():
|
||||||
hv_id, cmd = await self.commandQueue.get()
|
hv_id, cmd = await self.commandQueue.get()
|
||||||
|
@ -109,64 +159,72 @@ class CentralCommand(object):
|
||||||
'host': socket.gethostname(),
|
'host': socket.gethostname(),
|
||||||
'ip': self.getIp(),
|
'ip': self.getIp(),
|
||||||
'''
|
'''
|
||||||
async with self.hugveyLock: # lock to prevent duplicates on creation
|
async with self.hugveyLock: # lock to prevent duplicates on creation
|
||||||
if not hugvey_id in self.hugveys:
|
if not hugvey_id in self.hugveys:
|
||||||
logger.info(f'Instantiate hugvey #{hugvey_id}')
|
logger.info(f'Instantiate hugvey #{hugvey_id}')
|
||||||
|
print('a')
|
||||||
h = HugveyState(hugvey_id, self)
|
h = HugveyState(hugvey_id, self)
|
||||||
h.config(msg['host'],msg['ip'])
|
print('a')
|
||||||
|
h.config(msg['host'], msg['ip'])
|
||||||
|
print('b')
|
||||||
self.hugveys[hugvey_id] = h
|
self.hugveys[hugvey_id] = h
|
||||||
thread = threading.Thread(target=h.start, name=f"hugvey#{hugvey_id}")
|
thread = threading.Thread(
|
||||||
|
target=h.start, name=f"hugvey#{hugvey_id}")
|
||||||
thread.start()
|
thread.start()
|
||||||
|
print('c')
|
||||||
else:
|
else:
|
||||||
logger.info(f'Reconfigure hugvey #{hugvey_id}')
|
logger.info(f'Reconfigure hugvey #{hugvey_id}')
|
||||||
# (re)configure exisitng hugveys
|
# (re)configure exisitng hugveys
|
||||||
h.config(msg['host'],msg['ip'])
|
h.config(msg['host'], msg['ip'])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async def eventListener(self):
|
async def eventListener(self):
|
||||||
s = self.ctx.socket(zmq.SUB)
|
s = self.ctx.socket(zmq.SUB)
|
||||||
s.bind(self.config['events']['listen_address'])
|
s.bind(self.config['events']['listen_address'])
|
||||||
logger.info("Listen for events on: {}".format(self.config['events']['listen_address']))
|
logger.info("Listen for events on: {}".format(
|
||||||
|
self.config['events']['listen_address']))
|
||||||
|
|
||||||
for id in self.hugvey_ids:
|
for id in self.hugvey_ids:
|
||||||
s.subscribe(getTopic(id))
|
s.subscribe(getTopic(id))
|
||||||
|
|
||||||
while self.isRunning.is_set():
|
while self.isRunning.is_set():
|
||||||
hugvey_id, msg = await zmqReceive(s)
|
try:
|
||||||
|
hugvey_id, msg = await zmqReceive(s)
|
||||||
|
|
||||||
if hugvey_id not in self.hugvey_ids:
|
if hugvey_id not in self.hugvey_ids:
|
||||||
logger.critical("Message from alien Hugvey: {}".format(hugvey_id))
|
logger.critical(
|
||||||
continue
|
"Message from alien Hugvey: {}".format(hugvey_id))
|
||||||
elif hugvey_id not in self.hugveys:
|
continue
|
||||||
if msg['event'] == 'connection':
|
elif hugvey_id not in self.hugveys:
|
||||||
# Create a hugvey
|
if msg['event'] == 'connection':
|
||||||
await self.instantiateHugvey(hugvey_id, msg)
|
# Create a hugvey
|
||||||
|
await self.instantiateHugvey(hugvey_id, msg)
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
"Message from uninstantiated Hugvey {}".format(hugvey_id))
|
||||||
|
logger.debug("Message contains: {}".format(msg))
|
||||||
|
continue
|
||||||
else:
|
else:
|
||||||
logger.warning("Message from uninstantiated Hugvey {}".format(hugvey_id))
|
await self.hugveys[hugvey_id].eventQueue.put(msg)
|
||||||
logger.debug("Message contains: {}".format(msg))
|
except Exception as e:
|
||||||
continue
|
logger.critical(f"Exception while running event loop:")
|
||||||
else:
|
logger.exception(e)
|
||||||
await self.hugveys[hugvey_id].eventQueue.put(msg)
|
|
||||||
pass
|
|
||||||
|
|
||||||
# def getPanopticon(self):
|
|
||||||
# self.panopticon =
|
|
||||||
|
|
||||||
def start(self):
|
def start(self):
|
||||||
self.isRunning.set()
|
self.isRunning.set()
|
||||||
self.loop = asyncio.get_event_loop()
|
self.loop = asyncio.get_event_loop()
|
||||||
# self.panopticon_loop = asyncio.new_event_loop()
|
# self.panopticon_loop = asyncio.new_event_loop()
|
||||||
|
|
||||||
self.tasks = {} # collect tasks so we can cancel in case of error
|
self.tasks = {} # collect tasks so we can cancel in case of error
|
||||||
self.tasks['eventListener'] = self.loop.create_task(self.eventListener())
|
self.tasks['eventListener'] = self.loop.create_task(
|
||||||
self.tasks['commandSender'] = self.loop.create_task(self.commandSender())
|
self.eventListener())
|
||||||
|
self.tasks['commandSender'] = self.loop.create_task(
|
||||||
|
self.commandSender())
|
||||||
|
|
||||||
print(threading.current_thread())
|
|
||||||
# we want the web interface in a separate thread
|
# we want the web interface in a separate thread
|
||||||
self.panopticon_thread = threading.Thread(target=self.panopticon.start, name="Panopticon")
|
self.panopticon_thread = threading.Thread(
|
||||||
|
target=self.panopticon.start, name="Panopticon")
|
||||||
self.panopticon_thread.start()
|
self.panopticon_thread.start()
|
||||||
print(threading.current_thread())
|
|
||||||
|
|
||||||
self.loop.run_forever()
|
self.loop.run_forever()
|
||||||
|
|
||||||
|
@ -174,17 +232,19 @@ class CentralCommand(object):
|
||||||
self.isRunning.clear()
|
self.isRunning.clear()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HugveyState(object):
|
class HugveyState(object):
|
||||||
"""Represents the state of a Hugvey client on the server.
|
"""Represents the state of a Hugvey client on the server.
|
||||||
Manages server connections & voice parsing etc.
|
Manages server connections & voice parsing etc.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, id: int, command: CentralCommand):
|
def __init__(self, id: int, command: CentralCommand):
|
||||||
|
|
||||||
self.id = id
|
self.id = id
|
||||||
self.command = command
|
self.command = command
|
||||||
self.logger = logging.getLogger(f"hugvey{self.id}")
|
self.logger = logging.getLogger(f"hugvey{self.id}")
|
||||||
self.loop = asyncio.new_event_loop()
|
self.loop = asyncio.new_event_loop()
|
||||||
self.isConfigured = False
|
self.isConfigured = False
|
||||||
|
self.isRunning = threading.Event()
|
||||||
self.eventQueue = None
|
self.eventQueue = None
|
||||||
self.language_code = 'en-GB'
|
self.language_code = 'en-GB'
|
||||||
self.story = Story(self)
|
self.story = Story(self)
|
||||||
|
@ -193,7 +253,8 @@ class HugveyState(object):
|
||||||
def config(self, hostname, ip):
|
def config(self, hostname, ip):
|
||||||
self.ip = ip
|
self.ip = ip
|
||||||
self.hostname = hostname
|
self.hostname = hostname
|
||||||
self.logger.info(f"Hugvey {self.id} at {self.ip}, host: {self.hostname}")
|
self.logger.info(
|
||||||
|
f"Hugvey {self.id} at {self.ip}, host: {self.hostname}")
|
||||||
|
|
||||||
if self.isConfigured == True:
|
if self.isConfigured == True:
|
||||||
# a reconfiguration/reconnection
|
# a reconfiguration/reconnection
|
||||||
|
@ -212,15 +273,13 @@ class HugveyState(object):
|
||||||
"""
|
"""
|
||||||
Start the tasks
|
Start the tasks
|
||||||
"""
|
"""
|
||||||
# stop on isRunning.is_set() or wait()
|
|
||||||
# self.loop.create_task(self.processAudio())
|
|
||||||
tasks = asyncio.gather(
|
tasks = asyncio.gather(
|
||||||
self.catchException(self.processAudio()),
|
self.catchException(self.processAudio()),
|
||||||
self.catchException(self.handleEvents()),
|
self.catchException(self.handleEvents()),
|
||||||
self.catchException(self.playStory()),
|
self.catchException(self.playStory()),
|
||||||
loop=self.loop)
|
loop=self.loop)
|
||||||
self.loop.run_until_complete(tasks)
|
self.loop.run_until_complete(tasks)
|
||||||
# asyncio.run_coroutine_threadsafe(self._start(), self.loop)
|
self.isRunning.set()
|
||||||
|
|
||||||
async def catchException(self, awaitable):
|
async def catchException(self, awaitable):
|
||||||
try:
|
try:
|
||||||
|
@ -243,14 +302,21 @@ class HugveyState(object):
|
||||||
self.story.events.append(msg)
|
self.story.events.append(msg)
|
||||||
|
|
||||||
async def handleEvents(self):
|
async def handleEvents(self):
|
||||||
self.eventQueue = asyncio.Queue() # start event queue here, to avoid loop issues
|
self.eventQueue = asyncio.Queue() # start event queue here, to avoid loop issues
|
||||||
while self.command.isRunning.is_set():
|
while self.command.isRunning.is_set():
|
||||||
event = await self.eventQueue.get()
|
event = await self.eventQueue.get()
|
||||||
self.logger.info("Received: {}".format(event))
|
self.logger.info("Received: {}".format(event))
|
||||||
|
|
||||||
if event['event'] =='language':
|
if event['event'] == 'language':
|
||||||
self.setLanguage(event['code'])
|
self.setLanguage(event['code'])
|
||||||
|
|
||||||
|
if event['event'] == 'pause':
|
||||||
|
self.pause()
|
||||||
|
if event['event'] == 'restart':
|
||||||
|
self.restart()
|
||||||
|
if event['event'] == 'resume':
|
||||||
|
self.resume()
|
||||||
|
|
||||||
self.eventQueue = None
|
self.eventQueue = None
|
||||||
|
|
||||||
def setLanguage(self, language_code):
|
def setLanguage(self, language_code):
|
||||||
|
@ -263,6 +329,21 @@ class HugveyState(object):
|
||||||
self.story.reset()
|
self.story.reset()
|
||||||
self.story.setStoryData(self.command.languages[language_code])
|
self.story.setStoryData(self.command.languages[language_code])
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
self.google.pause()
|
||||||
|
self.story.pause()
|
||||||
|
self.isRunning.clear()
|
||||||
|
|
||||||
|
def resume(self):
|
||||||
|
self.google.resume()
|
||||||
|
self.story.resume()
|
||||||
|
self.isRunning.set()
|
||||||
|
|
||||||
|
def restart(self):
|
||||||
|
self.story.reset()
|
||||||
|
self.resume()
|
||||||
|
self.isRunning.set()
|
||||||
|
|
||||||
async def playStory(self):
|
async def playStory(self):
|
||||||
await self.story.start()
|
await self.story.start()
|
||||||
|
|
||||||
|
@ -278,7 +359,8 @@ class HugveyState(object):
|
||||||
|
|
||||||
if self.command.debug:
|
if self.command.debug:
|
||||||
self.logger.warn("Debug on: Connecting Audio player")
|
self.logger.warn("Debug on: Connecting Audio player")
|
||||||
self.player = Player(self.command.config['voice']['src_rate'], self.command.config['voice']['out_rate'])
|
self.player = Player(
|
||||||
|
self.command.config['voice']['src_rate'], self.command.config['voice']['out_rate'])
|
||||||
streamer.addConsumer(self.player)
|
streamer.addConsumer(self.player)
|
||||||
|
|
||||||
self.logger.info("Start Speech")
|
self.logger.info("Start Speech")
|
||||||
|
@ -287,7 +369,7 @@ class HugveyState(object):
|
||||||
src_rate=self.command.config['voice']['src_rate'],
|
src_rate=self.command.config['voice']['src_rate'],
|
||||||
credential_file=self.command.config['voice']['google_credentials'],
|
credential_file=self.command.config['voice']['google_credentials'],
|
||||||
language_code=self.language_code
|
language_code=self.language_code
|
||||||
)
|
)
|
||||||
streamer.addConsumer(self.google)
|
streamer.addConsumer(self.google)
|
||||||
|
|
||||||
await streamer.run()
|
await streamer.run()
|
||||||
|
|
|
@ -11,28 +11,73 @@ import tornado.ioloop
|
||||||
import os
|
import os
|
||||||
from pytz.reference import Central
|
from pytz.reference import Central
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
|
|
||||||
logger = logging.getLogger("panopticon")
|
logger = logging.getLogger("panopticon")
|
||||||
|
|
||||||
web_dir = os.path.join(os.path.split(__file__)[0], '..','www')
|
web_dir = os.path.join(os.path.split(__file__)[0], '..', 'www')
|
||||||
print(web_dir)
|
print(web_dir)
|
||||||
|
|
||||||
class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
|
||||||
connections = set()
|
|
||||||
|
|
||||||
# the client connected
|
def getWebSocketHandler(central_command):
|
||||||
def open(self):
|
class WebSocketHandler(tornado.websocket.WebSocketHandler):
|
||||||
self.connections.add(self)
|
connections = set()
|
||||||
print("New client connected")
|
|
||||||
|
|
||||||
# the client sent the message
|
# the client connected
|
||||||
def on_message(self, message):
|
def open(self):
|
||||||
[con.write_message(message) for con in self.connections]
|
self.connections.add(self)
|
||||||
|
logger.info("New client connected")
|
||||||
|
|
||||||
# client disconnected
|
# the client sent the message
|
||||||
def on_close(self):
|
def on_message(self, message):
|
||||||
self.connections.remove(self)
|
try:
|
||||||
print("Client disconnected")
|
msg = json.loads(message)
|
||||||
|
if msg['action'] == 'init':
|
||||||
|
self.msgInit()
|
||||||
|
if msg['action'] == 'get_status':
|
||||||
|
self.msgStatus()
|
||||||
|
if msg['action'] == 'resume':
|
||||||
|
self.msgResume(msg['hugvey'])
|
||||||
|
if msg['action'] == 'pause':
|
||||||
|
self.msgPause(msg['hugvey'])
|
||||||
|
if msg['action'] == 'restart':
|
||||||
|
self.msgRestart(msg['hugvey'])
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.send({'alert': 'Invalid request: {}'.format(e)})
|
||||||
|
|
||||||
|
def send(self, message):
|
||||||
|
j = json.dumps(message)
|
||||||
|
[con.write_message(j) for con in self.connections]
|
||||||
|
|
||||||
|
# client disconnected
|
||||||
|
def on_close(self):
|
||||||
|
self.connections.remove(self)
|
||||||
|
logger.info("Client disconnected")
|
||||||
|
|
||||||
|
def getStatusMsg(self):
|
||||||
|
msg = central_command.getStatusSummary()
|
||||||
|
msg['action'] = 'status'
|
||||||
|
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def msgStatus(self):
|
||||||
|
self.send(self.getStatusMsg())
|
||||||
|
|
||||||
|
def msgInit(self):
|
||||||
|
msg = self.getStatusMsg()
|
||||||
|
self.send(msg)
|
||||||
|
|
||||||
|
def msgResume(self, hv_id):
|
||||||
|
central_command.hugveys[hv_id].eventQueue.put({'event': 'resume'})
|
||||||
|
|
||||||
|
def msgPause(self, hv_id):
|
||||||
|
central_command.hugveys[hv_id].eventQueue.put({'event': 'pause'})
|
||||||
|
|
||||||
|
def msgRestart(self, hv_id):
|
||||||
|
central_command.hugveys[hv_id].eventQueue.put({'event': 'restart'})
|
||||||
|
|
||||||
|
return WebSocketHandler
|
||||||
|
|
||||||
|
|
||||||
class Panopticon(object):
|
class Panopticon(object):
|
||||||
|
@ -40,12 +85,13 @@ class Panopticon(object):
|
||||||
self.command = central_command
|
self.command = central_command
|
||||||
self.config = config
|
self.config = config
|
||||||
self.application = tornado.web.Application([
|
self.application = tornado.web.Application([
|
||||||
(r"/ws", WebSocketHandler),
|
(r"/ws", getWebSocketHandler(self.command)),
|
||||||
(r"/uploads/(.*)", tornado.web.StaticFileHandler, {"path": config['web']['files_dir']}),
|
(r"/uploads/(.*)", tornado.web.StaticFileHandler,
|
||||||
(r"/(.*)", tornado.web.StaticFileHandler, {"path": web_dir, "default_filename": 'index.html'}),
|
{"path": config['web']['files_dir']}),
|
||||||
|
(r"/(.*)", tornado.web.StaticFileHandler,
|
||||||
|
{"path": web_dir, "default_filename": 'index.html'}),
|
||||||
], debug=True)
|
], debug=True)
|
||||||
|
|
||||||
|
|
||||||
self.application.listen(config['web']['port'])
|
self.application.listen(config['web']['port'])
|
||||||
# self.loop.configure(evt_loop)
|
# self.loop.configure(evt_loop)
|
||||||
|
|
||||||
|
|
165
hugvey/story.py
165
hugvey/story.py
|
@ -7,6 +7,7 @@ import asyncio
|
||||||
|
|
||||||
logger = logging.getLogger("narrative")
|
logger = logging.getLogger("narrative")
|
||||||
|
|
||||||
|
|
||||||
class Message(object):
|
class Message(object):
|
||||||
def __init__(self, id, text):
|
def __init__(self, id, text):
|
||||||
self.id = id
|
self.id = id
|
||||||
|
@ -16,9 +17,9 @@ class Message(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def initFromJson(message, data, story):
|
def initFromJson(message, data, story):
|
||||||
msg = message(data['@id'], data['text'])
|
msg = message(data['@id'], data['text'])
|
||||||
msg.isStart = data['start'] if 'start' in data else False
|
msg.isStart = data['start'] if 'start' in data else False
|
||||||
return msg;
|
return msg
|
||||||
|
|
||||||
def setReply(self, text):
|
def setReply(self, text):
|
||||||
self.reply = text
|
self.reply = text
|
||||||
|
@ -28,7 +29,8 @@ class Message(object):
|
||||||
|
|
||||||
def getReply(self):
|
def getReply(self):
|
||||||
if self.reply is None:
|
if self.reply is None:
|
||||||
raise Exception("Getting reply while there is none! {0}".format(self.id))
|
raise Exception(
|
||||||
|
"Getting reply while there is none! {0}".format(self.id))
|
||||||
|
|
||||||
return self.reply
|
return self.reply
|
||||||
|
|
||||||
|
@ -38,6 +40,7 @@ class Condition(object):
|
||||||
A condition, basic conditions are built in, custom condition can be given by
|
A condition, basic conditions are built in, custom condition can be given by
|
||||||
providing a custom method.
|
providing a custom method.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, id):
|
def __init__(self, id):
|
||||||
self.id = id
|
self.id = id
|
||||||
self.method = None
|
self.method = None
|
||||||
|
@ -45,7 +48,7 @@ class Condition(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def initFromJson(conditionClass, data, story):
|
def initFromJson(conditionClass, data, story):
|
||||||
condition = conditionClass(data['@id'])
|
condition = conditionClass(data['@id'])
|
||||||
# TODO: should Condition be subclassed?
|
# TODO: should Condition be subclassed?
|
||||||
if data['type'] == "replyContains":
|
if data['type'] == "replyContains":
|
||||||
condition.method = condition._hasMetReplyContains
|
condition.method = condition._hasMetReplyContains
|
||||||
|
@ -55,7 +58,7 @@ class Condition(object):
|
||||||
if 'vars' in data:
|
if 'vars' in data:
|
||||||
condition.vars = data['vars']
|
condition.vars = data['vars']
|
||||||
|
|
||||||
return condition;
|
return condition
|
||||||
|
|
||||||
def isMet(self, story):
|
def isMet(self, story):
|
||||||
"""
|
"""
|
||||||
|
@ -98,6 +101,7 @@ class Direction(object):
|
||||||
"""
|
"""
|
||||||
A condition based edge in the story graph
|
A condition based edge in the story graph
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, id, msgFrom: Message, msgTo: Message):
|
def __init__(self, id, msgFrom: Message, msgTo: Message):
|
||||||
self.id = id
|
self.id = id
|
||||||
self.msgFrom = msgFrom
|
self.msgFrom = msgFrom
|
||||||
|
@ -111,18 +115,19 @@ class Direction(object):
|
||||||
def initFromJson(direction, data, story):
|
def initFromJson(direction, data, story):
|
||||||
msgFrom = story.get(data['source'])
|
msgFrom = story.get(data['source'])
|
||||||
msgTo = story.get(data['target'])
|
msgTo = story.get(data['target'])
|
||||||
direction = direction(data['@id'], msgFrom, msgTo)
|
direction = direction(data['@id'], msgFrom, msgTo)
|
||||||
if 'conditions' in data:
|
if 'conditions' in data:
|
||||||
for conditionId in data['conditions']:
|
for conditionId in data['conditions']:
|
||||||
c = story.get(conditionId)
|
c = story.get(conditionId)
|
||||||
direction.addCondition(c)
|
direction.addCondition(c)
|
||||||
return direction;
|
return direction
|
||||||
|
|
||||||
|
|
||||||
class Interruption(object):
|
class Interruption(object):
|
||||||
"""
|
"""
|
||||||
An Interruption. Used to catch events outside of story flow.
|
An Interruption. Used to catch events outside of story flow.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, id):
|
def __init__(self, id):
|
||||||
self.id = id
|
self.id = id
|
||||||
self.conditions = []
|
self.conditions = []
|
||||||
|
@ -132,12 +137,13 @@ class Interruption(object):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def initFromJson(interruptionClass, data, story):
|
def initFromJson(interruptionClass, data, story):
|
||||||
interrupt = interruptionClass(data['@id'])
|
interrupt = interruptionClass(data['@id'])
|
||||||
if 'conditions' in data:
|
if 'conditions' in data:
|
||||||
for conditionId in data['conditions']:
|
for conditionId in data['conditions']:
|
||||||
c = story.get(conditionId)
|
c = story.get(conditionId)
|
||||||
interrupt.addCondition(c)
|
interrupt.addCondition(c)
|
||||||
return interrupt;
|
return interrupt
|
||||||
|
|
||||||
|
|
||||||
storyClasses = {
|
storyClasses = {
|
||||||
'Msg': Message,
|
'Msg': Message,
|
||||||
|
@ -146,18 +152,87 @@ storyClasses = {
|
||||||
'Interruption': Interruption,
|
'Interruption': Interruption,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class Stopwatch(object):
|
||||||
|
"""
|
||||||
|
Keep track of elapsed time. Use multiple markers, but a single pause/resume button
|
||||||
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
self.isRunning = asyncio.Event()
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def getElapsed(self, since_mark='start'):
|
||||||
|
t = time.time()
|
||||||
|
if self.paused_at != 0:
|
||||||
|
pause_duration = t - self.paused_at
|
||||||
|
else:
|
||||||
|
pause_duration = 0
|
||||||
|
return t - self.marks[since_mark] - pause_duration
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
self.paused_at = time.time()
|
||||||
|
self.isRunning.clear()
|
||||||
|
|
||||||
|
def resume(self):
|
||||||
|
if self.paused_at == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
pause_duration = time.time() - self.paused_at
|
||||||
|
for m in self.marks:
|
||||||
|
self.marks[m] += pause_duration
|
||||||
|
|
||||||
|
self.paused_at = 0
|
||||||
|
self.isRunning.set()
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.marks = {}
|
||||||
|
self.setMark('start')
|
||||||
|
self.paused_at = 0
|
||||||
|
self.isRunning.set()
|
||||||
|
|
||||||
|
def setMark(self, name):
|
||||||
|
self.marks[name] = time.time()
|
||||||
|
|
||||||
|
|
||||||
|
def clearMark(self, name):
|
||||||
|
if name in self.marks:
|
||||||
|
self.marks.pop(name)
|
||||||
|
|
||||||
class Story(object):
|
class Story(object):
|
||||||
"""Story represents and manages a story/narrative flow"""
|
"""Story represents and manages a story/narrative flow"""
|
||||||
#TODO should we separate 'narrative' (the graph) from the story (the current user flow)
|
# TODO should we separate 'narrative' (the graph) from the story (the
|
||||||
|
# current user flow)
|
||||||
|
|
||||||
def __init__(self, hugvey_state):
|
def __init__(self, hugvey_state):
|
||||||
super(Story, self).__init__()
|
super(Story, self).__init__()
|
||||||
self.hugvey = hugvey_state
|
self.hugvey = hugvey_state
|
||||||
|
|
||||||
self.events = [] # queue of received events
|
self.events = [] # queue of received events
|
||||||
self.commands = [] # queue of commands to send
|
self.commands = [] # queue of commands to send
|
||||||
self.log = [] # all nodes/elements that are triggered
|
self.log = [] # all nodes/elements that are triggered
|
||||||
self.currentMessage = None
|
self.currentMessage = None
|
||||||
|
self.timer = Stopwatch()
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
logger.debug('pause hugvey')
|
||||||
|
self.timer.pause()
|
||||||
|
|
||||||
|
def resume(self):
|
||||||
|
logger.debug('resume hugvey')
|
||||||
|
self.timer.resume()
|
||||||
|
|
||||||
|
def getStoryCounts(self):
|
||||||
|
# counts = {}
|
||||||
|
# for item in self.log:
|
||||||
|
# n =item.__class__.__name__
|
||||||
|
# if n not in counts:
|
||||||
|
# counts[n] = 0
|
||||||
|
# counts[n] += 1
|
||||||
|
# return counts
|
||||||
|
return {
|
||||||
|
'messages': len([e for e in self.log if isinstance(e, Message)]),
|
||||||
|
'interruptions': len([e for e in self.log if isinstance(e, Interruption)])
|
||||||
|
}
|
||||||
|
|
||||||
def setStoryData(self, story_data):
|
def setStoryData(self, story_data):
|
||||||
"""
|
"""
|
||||||
|
@ -171,7 +246,7 @@ class Story(object):
|
||||||
self.elements = {}
|
self.elements = {}
|
||||||
self.interruptions = []
|
self.interruptions = []
|
||||||
self.directionsPerMsg = {}
|
self.directionsPerMsg = {}
|
||||||
self.startMessage = None # The entrypoint to the graph
|
self.startMessage = None # The entrypoint to the graph
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
for el in self.data:
|
for el in self.data:
|
||||||
|
@ -185,17 +260,26 @@ class Story(object):
|
||||||
if currentId:
|
if currentId:
|
||||||
self.currentMessage = self.get(currentId)
|
self.currentMessage = self.get(currentId)
|
||||||
if self.currentMessage:
|
if self.currentMessage:
|
||||||
logger.info(f"Reinstantiated current message: {self.currentMessage.id}")
|
logger.info(
|
||||||
|
f"Reinstantiated current message: {self.currentMessage.id}")
|
||||||
else:
|
else:
|
||||||
logger.warn("Could not reinstatiate current message. Starting over")
|
logger.warn(
|
||||||
|
"Could not reinstatiate current message. Starting over")
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.startTime = time.time()
|
self.timer.reset()
|
||||||
self.currentMessage = None # currently active message, determines active listeners etc.
|
# self.startTime = time.time()
|
||||||
|
# currently active message, determines active listeners etc.
|
||||||
|
self.currentMessage = None
|
||||||
self.lastMsgTime = None
|
self.lastMsgTime = None
|
||||||
self.lastSpeechStartTime = None
|
self.lastSpeechStartTime = None
|
||||||
self.lastSpeechEndTime = None
|
self.lastSpeechEndTime = None
|
||||||
self.variables = {} # captured variables from replies
|
self.variables = {} # captured variables from replies
|
||||||
|
self.finish_time = False
|
||||||
|
|
||||||
|
self.events = [] # queue of received events
|
||||||
|
self.commands = [] # queue of commands to send
|
||||||
|
self.log = [] # all nodes/elements that are triggered
|
||||||
|
|
||||||
def add(self, obj):
|
def add(self, obj):
|
||||||
if obj.id in self.elements:
|
if obj.id in self.elements:
|
||||||
|
@ -223,25 +307,24 @@ class Story(object):
|
||||||
return self.elements[id]
|
return self.elements[id]
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
logger.info("Stop Story")
|
logger.info("Stop Story")
|
||||||
if self.isRunning:
|
if self.isRunning:
|
||||||
self.isRunning = False
|
self.isRunning = False
|
||||||
|
|
||||||
|
|
||||||
def _processPendingEvents(self):
|
def _processPendingEvents(self):
|
||||||
# Gather events:
|
# Gather events:
|
||||||
nr = len(self.events)
|
nr = len(self.events)
|
||||||
for i in range(nr):
|
for i in range(nr):
|
||||||
e = self.events.pop(0)
|
e = self.events.pop(0)
|
||||||
logger.info("handle '{}'".format( e ))
|
logger.info("handle '{}'".format(e))
|
||||||
if e['event'] == "exit":
|
if e['event'] == "exit":
|
||||||
self.stop()
|
self.stop()
|
||||||
if e['event'] == 'connect':
|
if e['event'] == 'connect':
|
||||||
# a client connected. Shold only happen in the beginning or in case of error
|
# a client connected. Shold only happen in the beginning or in case of error
|
||||||
# that is, until we have a 'reset' or 'start' event.
|
# that is, until we have a 'reset' or 'start' event.
|
||||||
self.setCurrentMessage(self.currentMessage) # reinitiate current message
|
# reinitiate current message
|
||||||
|
self.setCurrentMessage(self.currentMessage)
|
||||||
|
|
||||||
if e['event'] == "playbackFinish":
|
if e['event'] == "playbackFinish":
|
||||||
if e['msgId'] == self.currentMessage.id:
|
if e['msgId'] == self.currentMessage.id:
|
||||||
|
@ -254,6 +337,7 @@ class Story(object):
|
||||||
|
|
||||||
if e['event'] == 'speech':
|
if e['event'] == 'speech':
|
||||||
# log if somebody starts speaking
|
# log if somebody starts speaking
|
||||||
|
# TODO: use pausing timer
|
||||||
if self.lastSpeechStartTime is None or self.lastSpeechStartTime < self.lastMsgTime:
|
if self.lastSpeechStartTime is None or self.lastSpeechStartTime < self.lastMsgTime:
|
||||||
self.lastSpeechStartTime = e['time']
|
self.lastSpeechStartTime = e['time']
|
||||||
|
|
||||||
|
@ -266,7 +350,8 @@ class Story(object):
|
||||||
for direction in directions:
|
for direction in directions:
|
||||||
for condition in direction.conditions:
|
for condition in direction.conditions:
|
||||||
if condition.isMet(self):
|
if condition.isMet(self):
|
||||||
logger.info("Condition is met: {0}, going to {1}".format(condition.id, direction.msgTo.id))
|
logger.info("Condition is met: {0}, going to {1}".format(
|
||||||
|
condition.id, direction.msgTo.id))
|
||||||
self.log.append(condition)
|
self.log.append(condition)
|
||||||
self.log.append(direction)
|
self.log.append(direction)
|
||||||
self.setCurrentMessage(direction.msgTo)
|
self.setCurrentMessage(direction.msgTo)
|
||||||
|
@ -276,20 +361,21 @@ class Story(object):
|
||||||
"""
|
"""
|
||||||
every 1/10 sec. determine what needs to be done based on the current story state
|
every 1/10 sec. determine what needs to be done based on the current story state
|
||||||
"""
|
"""
|
||||||
loopDuration = 0.1 # Configure fps
|
loopDuration = 0.1 # Configure fps
|
||||||
lastTime = time.time()
|
lastTime = time.time()
|
||||||
logger.info("Start renderer")
|
logger.info("Start renderer")
|
||||||
while self.isRunning:
|
while self.isRunning:
|
||||||
if self.isRunning is False:
|
if self.isRunning is False:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# pause on timer paused
|
||||||
|
await self.timer.isRunning.wait() # wait for un-pause
|
||||||
|
|
||||||
for i in range(len(self.events)):
|
for i in range(len(self.events)):
|
||||||
self._processPendingEvents()
|
self._processPendingEvents()
|
||||||
|
|
||||||
if self.currentMessage.id not in self.directionsPerMsg:
|
if self.currentMessage.id not in self.directionsPerMsg:
|
||||||
# TODO: finish!
|
self.finish()
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
directions = self.getCurrentDirections()
|
directions = self.getCurrentDirections()
|
||||||
self._processDirections(directions)
|
self._processDirections(directions)
|
||||||
|
@ -307,9 +393,11 @@ class Story(object):
|
||||||
def setCurrentMessage(self, message):
|
def setCurrentMessage(self, message):
|
||||||
self.currentMessage = message
|
self.currentMessage = message
|
||||||
self.lastMsgTime = time.time()
|
self.lastMsgTime = time.time()
|
||||||
self.lastMsgFinishTime = None # to be filled in by the event
|
self.lastMsgFinishTime = None # to be filled in by the event
|
||||||
|
|
||||||
logger.info("Current message: ({0}) \"{1}\"".format(message.id, message.text))
|
logger.info("Current message: ({0}) \"{1}\"".format(
|
||||||
|
message.id, message.text))
|
||||||
|
self.log.append(message)
|
||||||
# TODO: prep events & timer etc.
|
# TODO: prep events & timer etc.
|
||||||
self.hugvey.sendCommand({
|
self.hugvey.sendCommand({
|
||||||
'action': 'play',
|
'action': 'play',
|
||||||
|
@ -321,8 +409,8 @@ class Story(object):
|
||||||
|
|
||||||
for direction in self.getCurrentDirections():
|
for direction in self.getCurrentDirections():
|
||||||
conditions = [c.id for c in direction.conditions]
|
conditions = [c.id for c in direction.conditions]
|
||||||
logger.debug("- {0} -> {1} (when: {2}) ".format(direction.msgFrom.id, direction.msgTo.id, conditions))
|
logger.debug(
|
||||||
|
"- {0} -> {1} (when: {2}) ".format(direction.msgFrom.id, direction.msgTo.id, conditions))
|
||||||
|
|
||||||
def getCurrentDirections(self):
|
def getCurrentDirections(self):
|
||||||
if self.currentMessage.id not in self.directionsPerMsg:
|
if self.currentMessage.id not in self.directionsPerMsg:
|
||||||
|
@ -332,10 +420,21 @@ class Story(object):
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
logger.info("Starting story")
|
logger.info("Starting story")
|
||||||
self.startTime = time.time()
|
self.timer.reset()
|
||||||
|
# self.startTime = time.time()
|
||||||
self.isRunning = True
|
self.isRunning = True
|
||||||
self.setCurrentMessage(self.startMessage)
|
self.setCurrentMessage(self.startMessage)
|
||||||
await self._renderer()
|
await self._renderer()
|
||||||
|
|
||||||
|
def isFinished(self):
|
||||||
|
if hasattr(self, 'finish_time'):
|
||||||
|
return self.finish_time
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def finish(self):
|
||||||
|
logger.info(f"Finished story for {self.hugvey.id}")
|
||||||
|
self.hugvey.pause()
|
||||||
|
self.finish_time = time.time()
|
||||||
|
self.timer.pause()
|
||||||
|
|
||||||
|
|
|
@ -32,17 +32,26 @@ class GoogleVoiceClient(object):
|
||||||
|
|
||||||
# Create a thread-safe buffer of audio data
|
# Create a thread-safe buffer of audio data
|
||||||
self.buffer = queue.Queue()
|
self.buffer = queue.Queue()
|
||||||
self.isRunning = False
|
self.isRunning = threading.Event()
|
||||||
|
self.toBeShutdown = False
|
||||||
self.target_rate = 16000
|
self.target_rate = 16000
|
||||||
self.cv_laststate = None
|
self.cv_laststate = None
|
||||||
self.restart = False
|
self.restart = False
|
||||||
|
|
||||||
|
|
||||||
self.task = threading.Thread(target=self.run, name=f"hugvey#{self.hugvey.id}v")
|
self.task = threading.Thread(target=self.run, name=f"hugvey#{self.hugvey.id}v")
|
||||||
self.task.setDaemon(True)
|
self.task.setDaemon(True)
|
||||||
self.task.start()
|
self.task.start()
|
||||||
|
|
||||||
|
def pause(self):
|
||||||
|
self.isRunning.clear()
|
||||||
|
self.restart = True
|
||||||
|
|
||||||
|
def resume(self):
|
||||||
|
self.isRunning.set()
|
||||||
|
|
||||||
def generator(self):
|
def generator(self):
|
||||||
while self.isRunning:
|
while not self.toBeShutdown:
|
||||||
yield self.buffer.get()
|
yield self.buffer.get()
|
||||||
|
|
||||||
def setLanguage(self, language_code):
|
def setLanguage(self, language_code):
|
||||||
|
@ -54,11 +63,13 @@ class GoogleVoiceClient(object):
|
||||||
self.restart = True
|
self.restart = True
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
self.isRunning = True
|
self.isRunning.set()
|
||||||
|
|
||||||
|
|
||||||
while self.isRunning:
|
while not self.toBeShutdown:
|
||||||
try:
|
try:
|
||||||
|
self.isRunning.wait()
|
||||||
|
|
||||||
self.speech_client = speech.SpeechClient()
|
self.speech_client = speech.SpeechClient()
|
||||||
config = types.RecognitionConfig(
|
config = types.RecognitionConfig(
|
||||||
encoding=enums.RecognitionConfig.AudioEncoding.LINEAR16,
|
encoding=enums.RecognitionConfig.AudioEncoding.LINEAR16,
|
||||||
|
@ -122,7 +133,7 @@ class GoogleVoiceClient(object):
|
||||||
self.restart = False
|
self.restart = False
|
||||||
raise RequireRestart("Restart required")
|
raise RequireRestart("Restart required")
|
||||||
|
|
||||||
if not self.isRunning:
|
if self.toBeShutdown:
|
||||||
logger.warn("Stopping voice loop")
|
logger.warn("Stopping voice loop")
|
||||||
break
|
break
|
||||||
except RequireRestart as e:
|
except RequireRestart as e:
|
||||||
|
@ -142,7 +153,7 @@ class GoogleVoiceClient(object):
|
||||||
self.buffer.put_nowait(data)
|
self.buffer.put_nowait(data)
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
self.isRunning = False
|
self.toBeShutdown = True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
3
local/.gitignore
vendored
Normal file
3
local/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
*
|
||||||
|
!.gitignore
|
||||||
|
|
|
@ -7,7 +7,7 @@ voice:
|
||||||
port: 4444
|
port: 4444
|
||||||
chunk: 2972
|
chunk: 2972
|
||||||
google_credentials: "/home/ruben/Documents/Projecten/2018/Hugvey/test_googlespeech/My First Project-0c7833e0d5fa.json"
|
google_credentials: "/home/ruben/Documents/Projecten/2018/Hugvey/test_googlespeech/My First Project-0c7833e0d5fa.json"
|
||||||
hugveys: 3
|
hugveys: 25
|
||||||
languages:
|
languages:
|
||||||
- code: en-GB
|
- code: en-GB
|
||||||
file: story_en.json
|
file: story_en.json
|
||||||
|
|
100
www/hugvey_console.js
Normal file
100
www/hugvey_console.js
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
var panopticon;
|
||||||
|
|
||||||
|
class Panopticon {
|
||||||
|
constructor() {
|
||||||
|
console.log( "Init panopticon" );
|
||||||
|
this.hugveys = new Vue( {
|
||||||
|
el: "#status",
|
||||||
|
data: {
|
||||||
|
uptime: 0,
|
||||||
|
languages: [],
|
||||||
|
hugveys: []
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
time_passed: function (hugvey, property) {
|
||||||
|
console.log("property!", Date(hugvey[property] * 1000));
|
||||||
|
return moment(Date(hugvey[property] * 1000)).fromNow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
|
||||||
|
|
||||||
|
this.socket = new ReconnectingWebSocket( "ws://localhost:8888/ws", null, { debug: true, reconnectInterval: 3000 } );
|
||||||
|
|
||||||
|
|
||||||
|
this.socket.addEventListener( 'open', ( e ) => {
|
||||||
|
this.send( { action: 'init' } );
|
||||||
|
} );
|
||||||
|
|
||||||
|
this.socket.addEventListener( 'close', function( e ) {
|
||||||
|
console.log( 'Closed connection' );
|
||||||
|
} );
|
||||||
|
this.socket.addEventListener( 'message', ( e ) => {
|
||||||
|
let msg = JSON.parse( e.data );
|
||||||
|
if ( typeof msg['alert'] !== 'undefined' ) {
|
||||||
|
alert(msg['alert']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( typeof msg['action'] === 'undefined' ) {
|
||||||
|
console.error( "not a valid message: " + e.data );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ( msg['action'] ) {
|
||||||
|
|
||||||
|
case 'status':
|
||||||
|
this.hugveys.uptime = this.stringToHHMMSS(msg['uptime']);
|
||||||
|
this.hugveys.languages = msg['languages'];
|
||||||
|
this.hugveys.hugveys = msg['hugveys'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
send( msg ) {
|
||||||
|
this.socket.send( JSON.stringify( msg ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
getStatus() {
|
||||||
|
// console.log('get status', this, panopticon);
|
||||||
|
panopticon.send( { action: 'get_status' } );
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
setInterval( this.getStatus, 3000 );
|
||||||
|
}
|
||||||
|
|
||||||
|
stringToHHMMSS (string) {
|
||||||
|
var sec_num = parseInt(string, 10); // don't forget the second param
|
||||||
|
var hours = Math.floor(sec_num / 3600);
|
||||||
|
var minutes = Math.floor((sec_num - (hours * 3600)) / 60);
|
||||||
|
var seconds = sec_num - (hours * 3600) - (minutes * 60);
|
||||||
|
|
||||||
|
if (hours < 10) {hours = "0"+hours;}
|
||||||
|
if (minutes < 10) {minutes = "0"+minutes;}
|
||||||
|
if (seconds < 10) {seconds = "0"+seconds;}
|
||||||
|
return hours+':'+minutes+':'+seconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
loadNarrative(code, file) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
resume(hv_id) {
|
||||||
|
this.send({ action: 'resume', hugvey: hv_id })
|
||||||
|
}
|
||||||
|
pause(hv_id) {
|
||||||
|
this.send({ action: 'play', hugvey: hv_id })
|
||||||
|
}
|
||||||
|
restart(hv_id) {
|
||||||
|
this.send({ action: 'restart', hugvey: hv_id })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
window.addEventListener( 'load', function() {
|
||||||
|
panopticon = new Panopticon();
|
||||||
|
panopticon.init();
|
||||||
|
})
|
|
@ -1,17 +1,43 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Pillow Talk Control Interface</title>
|
<title>Pillow Talk Control Interface</title>
|
||||||
<!-- development version, includes helpful console warnings -->
|
<!-- development version, includes helpful console warnings -->
|
||||||
<script src="/vue.js"></script>
|
<script src="/vue.js"></script>
|
||||||
|
<script src="/reconnecting-websocket.js"></script>
|
||||||
|
<script src="/moment.min.js"></script>
|
||||||
|
<link rel="stylesheet" href="styles.css"></link>
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id='status'>
|
<div id='status'>
|
||||||
</div>
|
<div id='overview'>
|
||||||
<div id='story'>
|
<dl>
|
||||||
</div>
|
<dt>Uptime</dt>
|
||||||
<div id='hugvey'>
|
<dd>{{uptime}}</dd>
|
||||||
</div>
|
<dt>Languages</dt>
|
||||||
<div id='app'>{{message}}</div>
|
<dd v-for="lang in languages" :title="lang.file"
|
||||||
<script src="/hugvey_console.js"></script>
|
@click="panopticon.loadNarrative(lang.code, lang.file)">{{lang.code}}</dd>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div class='hugvey' v-for="hv in hugveys"
|
||||||
|
:class="[{'hugvey--off': hv.status == 'off'},{'hugvey--on': hv.status != 'off'},{'hugvey--paused': hv.status == 'paused'},{'hugvey--running': hv.status == 'running'}]">
|
||||||
|
<h1>
|
||||||
|
{{ hv.id }}
|
||||||
|
<!-- / {{ hv.status }} -->
|
||||||
|
</h1>
|
||||||
|
<div v-if="hv.status != 'off'">
|
||||||
|
{{ hv.language }} / {{ hv.msg }}
|
||||||
|
<div v-if="hv.finished != false">
|
||||||
|
Finished: {{time_passed(hv, 'finished')}}
|
||||||
|
</div>
|
||||||
|
<div v-for="c, key in hv.counts"><dt>{{key}}</dt><dd>{{c}}</dd></div>
|
||||||
|
<div v-if="hv.status != 'running'" @click="panopticon.pause(hv.id)">Pause</div>
|
||||||
|
<div v-if="hv.status != 'paused'" @click="panopticon.resume(hv.id)">Resume</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id='story'></div>
|
||||||
|
<div id='hugvey'></div>
|
||||||
|
<script type='application/javascript' src="/hugvey_console.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
1
www/moment.min.js
vendored
Normal file
1
www/moment.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
832
www/narrative_builder.html
Normal file
832
www/narrative_builder.html
Normal file
|
@ -0,0 +1,832 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Pillow Talk - Narrative Builder</title>
|
||||||
|
<script src="https://d3js.org/d3.v5.min.js"></script>
|
||||||
|
<script type="text/javascript">
|
||||||
|
// https://raw.githubusercontent.com/KoryNunn/crel/master/crel.min.js
|
||||||
|
!function(n,e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):n.crel=e()}(this,function(){function n(a){var d,s=arguments,p=s[1],y=2,m=s.length,x=n[o];if(a=n[u](a)?a:c.createElement(a),m>1){if((!e(p,r)||n[f](p)||Array.isArray(p))&&(--y,p=null),m-y==1&&e(s[y],"string"))a.textContent=s[y];else for(;y<m;++y)null!==(d=s[y])&&l(a,d);for(var v in p)if(x[v]){var A=x[v];e(A,t)?A(a,p[v]):a[i](A,p[v])}else e(p[v],t)?a[v]=p[v]:a[i](v,p[v])}return a}var e=function(n,e){return typeof n===e},t="function",r="object",i="setAttribute",o="attrMap",f="isNode",u="isElement",c=document,a=function(n){return n instanceof Node},d=function(n){return n instanceof Element},l=function(e,t){if(Array.isArray(t))return void t.map(function(n){l(e,n)});n[f](t)||(t=c.createTextNode(t)),e.appendChild(t)};return n[o]={},n[u]=d,n[f]=a,e(Proxy,"undefined")||(n.proxy=new Proxy(n,{get:function(e,t){return!(t in n)&&(n[t]=n.bind(null,t)),n[t]}})),n});
|
||||||
|
|
||||||
|
crel.attrMap['on'] = function(element, value) {
|
||||||
|
for (var eventName in value) {
|
||||||
|
element.addEventListener(eventName, value[eventName]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style media="screen">
|
||||||
|
body{
|
||||||
|
margin:0;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: "Noto Sans", sans-serif;
|
||||||
|
}
|
||||||
|
svg{
|
||||||
|
width:100vw;
|
||||||
|
height: 100vh;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
svg:active{
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
circle{
|
||||||
|
cursor: pointer;
|
||||||
|
fill: rgb(119, 97, 142);
|
||||||
|
}
|
||||||
|
.startMsg circle{
|
||||||
|
fill: lightseagreen;
|
||||||
|
}
|
||||||
|
.endMsg circle{
|
||||||
|
fill: lightslategray;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orphanedMsg{
|
||||||
|
fill: lightcoral;
|
||||||
|
}
|
||||||
|
text{
|
||||||
|
text-anchor: middle;
|
||||||
|
font-size: 11pt;
|
||||||
|
font-family: sans-serif;
|
||||||
|
fill: white;
|
||||||
|
}
|
||||||
|
line{
|
||||||
|
marker-end: url('#arrowHead');
|
||||||
|
stroke-width: 2px;
|
||||||
|
stroke: black;
|
||||||
|
}
|
||||||
|
line.link--noconditions{
|
||||||
|
stroke-dasharray: 5 4;
|
||||||
|
stroke: red;
|
||||||
|
}
|
||||||
|
label::after {
|
||||||
|
content: '';
|
||||||
|
clear: both;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
label{
|
||||||
|
width:100%;
|
||||||
|
font-weight:bold;
|
||||||
|
display: block;
|
||||||
|
margin: 0 -10px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
||||||
|
label input,label select{
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
label:nth-child(odd){
|
||||||
|
background-color: rgba(255,255,255,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#msg{
|
||||||
|
position: absolute;
|
||||||
|
top:0;
|
||||||
|
right:0;
|
||||||
|
width: 30%;
|
||||||
|
max-height:100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
#msg .msg__info, #msg .directions > div{
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
background:lightgray;
|
||||||
|
}
|
||||||
|
#opener{
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#nodes g:hover circle,
|
||||||
|
.selectedMsg circle {
|
||||||
|
stroke: lightgreen;
|
||||||
|
stroke-width: 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlDown #nodes g:hover circle,
|
||||||
|
.secondaryMsg circle {
|
||||||
|
stroke: lightgreen;
|
||||||
|
stroke-width: 5;
|
||||||
|
stroke-dasharray: 10 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition h4{
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.condition + .condition::before {
|
||||||
|
content: "OR";
|
||||||
|
display: block;
|
||||||
|
border-bottom: solid 2px;
|
||||||
|
height: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
text-align: center;
|
||||||
|
text-shadow: 2px 2px 2px lightgray,-2px 2px 2px lightgray,2px -2px 2px lightgray,-2px -2px 2px lightgray;
|
||||||
|
|
||||||
|
}
|
||||||
|
.condition--add{
|
||||||
|
/* text-align: center; */
|
||||||
|
}
|
||||||
|
.btn{
|
||||||
|
padding: 5px;
|
||||||
|
background:lightgray;
|
||||||
|
border-radius: 5px;
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
}
|
||||||
|
.btn:hover{
|
||||||
|
background: lightblue;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="opener">
|
||||||
|
<h1>Hugvey</h1>
|
||||||
|
<h3>Select a narrative json file</h3>
|
||||||
|
<input id='fileOpener' type="file" />
|
||||||
|
<div>
|
||||||
|
<input id='fileLoad' type="submit" value="Load file" />
|
||||||
|
<input id='stateLoad' type="button" value="Load last state" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="msg"></div>
|
||||||
|
<div id="controls">
|
||||||
|
<div id='btn-save' class='btn'>Save JSON</div>
|
||||||
|
<div id='btn-addMsg' class='btn'>New Message</div>
|
||||||
|
</div>
|
||||||
|
<svg id='graph' viewbox="0 0 1280 1024" preserveAspectRatio="xMidYMid">
|
||||||
|
<defs>
|
||||||
|
<marker markerHeight="8" markerWidth="8" refY="0" refX="12" viewBox="0 -6 16 12" preserveAspectRatio="none" orient="auto" id="arrowHead"><path d="M0,-6L16,0L0,6" fill="black"></path></marker>
|
||||||
|
</defs>
|
||||||
|
<g id='container'>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</body>
|
||||||
|
<script type="text/javascript">
|
||||||
|
class Graph{
|
||||||
|
constructor() {
|
||||||
|
this.width = 1280;
|
||||||
|
this.height = 1024;
|
||||||
|
this.nodeSize = 80;
|
||||||
|
this.maxChars = 16;
|
||||||
|
this.svg = d3.select('#graph');
|
||||||
|
this.container = d3.select('#container');
|
||||||
|
this.selectedMsg = null;
|
||||||
|
this.messages = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
||||||
|
this.directions = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
||||||
|
this.conditions = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
||||||
|
this.interruptions = []; // initialise empty array. For the simulation, make sure we keep the same array object
|
||||||
|
|
||||||
|
let graph = this;
|
||||||
|
this.controlDown = false;
|
||||||
|
document.addEventListener('keydown', function(e){
|
||||||
|
console.log(e);
|
||||||
|
if(e.which == "17") {
|
||||||
|
graph.controlDown = true;
|
||||||
|
document.body.classList.add('controlDown');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.addEventListener('keyup', function(e){
|
||||||
|
console.log(e);
|
||||||
|
if(e.which == "17") {
|
||||||
|
graph.controlDown = false;
|
||||||
|
document.body.classList.remove('controlDown');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let c = this.container;
|
||||||
|
let zoomed = function(){
|
||||||
|
c.attr("transform", d3.event.transform);
|
||||||
|
}
|
||||||
|
this.svg.call(d3.zoom()
|
||||||
|
.scaleExtent([1 / 2, 8])
|
||||||
|
.on("zoom", zoomed));
|
||||||
|
|
||||||
|
this.nodesG = this.container.append("g")
|
||||||
|
.attr("id", "nodes")
|
||||||
|
|
||||||
|
this.linkG = this.container.append("g")
|
||||||
|
.attr("id", "links");
|
||||||
|
|
||||||
|
document.getElementById('btn-save').addEventListener('click', function(e){ graph.saveJson(); });
|
||||||
|
document.getElementById('btn-addMsg').addEventListener('click', function(e){ graph.createMsg(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
clickMsg(msg) {
|
||||||
|
// event when a message is clicked.
|
||||||
|
console.log(msg);
|
||||||
|
|
||||||
|
if(this.controlDown) {
|
||||||
|
this.secondarySelectMsg(msg);
|
||||||
|
} else {
|
||||||
|
this.selectMsg(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
secondarySelectMsg(msg) {
|
||||||
|
if(this.selectedMsg !== null) {
|
||||||
|
this.addDirection(this.selectedMsg, msg);
|
||||||
|
} else {
|
||||||
|
console.error('No message selected as Source');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectMsg(msg) {
|
||||||
|
let selectedEls = document.getElementsByClassName('selectedMsg');
|
||||||
|
while(selectedEls.length > 0){
|
||||||
|
selectedEls[0].classList.remove('selectedMsg');
|
||||||
|
}
|
||||||
|
document.getElementById(msg['@id']).classList.add('selectedMsg');
|
||||||
|
|
||||||
|
this.selectedMsg = msg;
|
||||||
|
|
||||||
|
this.showMsg(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMsg() {
|
||||||
|
// used eg. after a condition creation.
|
||||||
|
this.showMsg(this.selectedMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
showMsg(msg) {
|
||||||
|
let msgEl = document.getElementById('msg');
|
||||||
|
msgEl.innerHTML = "";
|
||||||
|
let startAttributes = {
|
||||||
|
'name': msg['@id'] + '-start',
|
||||||
|
'disabled': true,
|
||||||
|
'type': 'checkbox',
|
||||||
|
'on': {
|
||||||
|
'change': this.getEditEventListener()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(msg['start'] == true) {
|
||||||
|
startAttributes['checked'] = 'checked';
|
||||||
|
}
|
||||||
|
let msgInfoEl = crel('div', {'class': 'msg__info'},
|
||||||
|
crel('h1', {'class':'msg__id'}, msg['@id']),
|
||||||
|
crel('label',
|
||||||
|
crel('span', 'Text'),
|
||||||
|
crel('input', {
|
||||||
|
'name': msg['@id'] + '-text',
|
||||||
|
'value': msg['text'],
|
||||||
|
'on': {
|
||||||
|
'change': this.getEditEventListener()
|
||||||
|
}
|
||||||
|
} )
|
||||||
|
),
|
||||||
|
crel('label',
|
||||||
|
crel('span', 'Start'),
|
||||||
|
crel('input', startAttributes)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
msgEl.appendChild(msgInfoEl);
|
||||||
|
|
||||||
|
// let directionHEl = document.createElement('h2');
|
||||||
|
// directionHEl.innerHTML = "Directions";
|
||||||
|
|
||||||
|
let fromDirections =[] , toDirections = [];
|
||||||
|
|
||||||
|
for(let direction of this.getDirectionsTo(msg)) {
|
||||||
|
toDirections.push(this.getDirectionEl(direction, msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let direction of this.getDirectionsFrom(msg)) {
|
||||||
|
fromDirections.push(this.getDirectionEl(direction, msg));
|
||||||
|
}
|
||||||
|
let directionsEl = crel('div', {'class': 'directions'},
|
||||||
|
crel('h2', 'Directions'),
|
||||||
|
...toDirections, ...fromDirections
|
||||||
|
);
|
||||||
|
msgEl.appendChild(directionsEl);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
getDirectionEl(direction, msg) {
|
||||||
|
let directionEl = document.createElement('div');
|
||||||
|
if(direction['source'] == msg) {
|
||||||
|
directionEl.innerHTML = `<h3>To ${direction['target']['@id']}</h3>`;
|
||||||
|
} else {
|
||||||
|
directionEl.innerHTML = `<h3>From ${direction['source']['@id']}</h3>`;
|
||||||
|
}
|
||||||
|
let del = document.createElement('div');
|
||||||
|
del.innerHTML = "delete";
|
||||||
|
del.classList.add("deleteBtn");
|
||||||
|
let g = this;
|
||||||
|
del.addEventListener('click', (e) => g.rmDirection(direction));
|
||||||
|
directionEl.appendChild(del);
|
||||||
|
|
||||||
|
// TODO; conditions
|
||||||
|
|
||||||
|
for(let conditionId of direction['conditions']) {
|
||||||
|
let condition = this.getNodeById(conditionId);
|
||||||
|
directionEl.appendChild(this.getEditConditionFormEl(condition, direction));
|
||||||
|
}
|
||||||
|
|
||||||
|
directionEl.appendChild(this.getAddConditionFormEl(direction));
|
||||||
|
|
||||||
|
return directionEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
getEditConditionFormEl(condition, direction) {
|
||||||
|
let conditionEl = crel('div', {'class': 'condition condition--edit'},
|
||||||
|
crel('h4', {'title': condition['@id']}, condition['type'])
|
||||||
|
)
|
||||||
|
let labelLabel = document.createElement('label');
|
||||||
|
labelLabel.innerHTML = "Description";
|
||||||
|
let labelInput = crel('input',{
|
||||||
|
'name': `${condition['@id']}-label`,
|
||||||
|
'value': typeof condition['label'] == 'undefined' ? "" : condition['label'],
|
||||||
|
'on': {
|
||||||
|
'change': this.getEditEventListener()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
labelLabel.appendChild(labelInput);
|
||||||
|
conditionEl.appendChild(labelLabel);
|
||||||
|
|
||||||
|
for(let v in condition['vars']) {
|
||||||
|
let varLabel = document.createElement('label');
|
||||||
|
varLabel.innerHTML = v;
|
||||||
|
let varInput = document.createElement('input');
|
||||||
|
if(v == 'seconds') {
|
||||||
|
varInput.type = 'number';
|
||||||
|
}
|
||||||
|
varInput.name = `${condition['@id']}-vars.${v}`;
|
||||||
|
varInput.value = condition['vars'][v];
|
||||||
|
varInput.addEventListener('change', this.getEditEventListener());
|
||||||
|
varLabel.appendChild(varInput);
|
||||||
|
conditionEl.appendChild(varLabel);
|
||||||
|
}
|
||||||
|
return conditionEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
getConditionTypes() {
|
||||||
|
if(typeof this.conditionTypes === 'undefined') {
|
||||||
|
// type: vars: attribtes for crel()
|
||||||
|
this.conditionTypes = {
|
||||||
|
'timeout': {
|
||||||
|
'seconds': {'type': 'number', 'value': 10, 'min':0, 'step': 0.1}
|
||||||
|
},
|
||||||
|
'replyContains': {
|
||||||
|
'regex': {'value': '.+'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.conditionTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
fillConditionFormForType(conditionForm, type) {
|
||||||
|
conditionForm.innerHTML = "";
|
||||||
|
let vars = this.getConditionTypes()[type];
|
||||||
|
for(let v in vars){
|
||||||
|
let attr = vars[v];
|
||||||
|
attr['name'] = v;
|
||||||
|
conditionForm.appendChild(
|
||||||
|
crel('label',
|
||||||
|
crel('span', v),
|
||||||
|
crel('input', attr)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAddConditionFormEl(direction) {
|
||||||
|
|
||||||
|
let optionEls = [];
|
||||||
|
let types = this.getConditionTypes();
|
||||||
|
for(let type in types) {
|
||||||
|
optionEls.push(crel('option', type));
|
||||||
|
}
|
||||||
|
|
||||||
|
let conditionForm = crel('div', {'class': 'condition--vars'});
|
||||||
|
let g = this;
|
||||||
|
let addConditionEl = crel('div', {'class': 'condition condition--add'},
|
||||||
|
crel('form', {
|
||||||
|
'on': {
|
||||||
|
'submit': function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
let form = new FormData(e.target);
|
||||||
|
console.log('submit', form);
|
||||||
|
let type = form.get('type');
|
||||||
|
form.delete('type');
|
||||||
|
let label = form.get('label');
|
||||||
|
form.delete('label');
|
||||||
|
let vars = {};
|
||||||
|
for(var pair of form.entries()) {
|
||||||
|
vars[pair[0]] = pair[1];
|
||||||
|
}
|
||||||
|
g.addConditionForDirection(type, label, vars, direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
crel("h4", "Create New Condition"),
|
||||||
|
crel("label",
|
||||||
|
crel('span', "Type"),
|
||||||
|
crel('select', {
|
||||||
|
'name': 'type',
|
||||||
|
'on': {
|
||||||
|
'change': function(e){
|
||||||
|
g.fillConditionFormForType(conditionForm, e.target.value);
|
||||||
|
}
|
||||||
|
}}, optionEls),
|
||||||
|
),
|
||||||
|
crel("label",
|
||||||
|
crel('span', "Description"),
|
||||||
|
crel('input', {'name': 'label'})
|
||||||
|
),
|
||||||
|
conditionForm,
|
||||||
|
crel('input', {
|
||||||
|
'type':'submit',
|
||||||
|
'value':'create'
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
this.fillConditionFormForType(conditionForm, optionEls[0].value);
|
||||||
|
|
||||||
|
return addConditionEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
rmConditionFromDirection(condition, direction) {
|
||||||
|
let id = condition['@id'];
|
||||||
|
// TODO
|
||||||
|
if(typeof direction != 'undefined') {
|
||||||
|
|
||||||
|
}
|
||||||
|
this._rmNode(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
getConditionEl(condition) {
|
||||||
|
let conditionEl = document.createElement('div');
|
||||||
|
|
||||||
|
return conditionEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDirectionsFrom(msg) {
|
||||||
|
return this.directions.filter(d => d['source'] == msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDirectionsTo(msg) {
|
||||||
|
return this.directions.filter(d => d['target'] == msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
addMsg() {
|
||||||
|
let msg = {
|
||||||
|
"@id": "n" + Date.now().toString(36),
|
||||||
|
"@type": "Msg",
|
||||||
|
"text": "New",
|
||||||
|
"start": false
|
||||||
|
}
|
||||||
|
this.data.push(msg);
|
||||||
|
this.updateFromData();
|
||||||
|
this.build();
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
rmMsg(msg) {
|
||||||
|
let invalidatedDirections = this.directions.filter(d => d['source'] == msg || d['target'] == msg);
|
||||||
|
console.log('invalidated', invalidatedDirections);
|
||||||
|
for(let dir of invalidatedDirections) {
|
||||||
|
let i = this.data.indexOf(dir);
|
||||||
|
this.data.splice(i, 1);
|
||||||
|
}
|
||||||
|
this._rmNode(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
_rmNode(node) {
|
||||||
|
// remove msg/direction/condition/etc
|
||||||
|
let i = this.data.indexOf(node);
|
||||||
|
this.data.splice(i, 1);
|
||||||
|
this.updateFromData();
|
||||||
|
this.build();
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
addConditionForDirection(type, label, vars, direction) {
|
||||||
|
let con = this.addCondition(type, label, vars, true);
|
||||||
|
direction['conditions'].push(con['@id']);
|
||||||
|
this.updateFromData();
|
||||||
|
this.build();
|
||||||
|
|
||||||
|
this.updateMsg();
|
||||||
|
}
|
||||||
|
|
||||||
|
addCondition(type, label, vars, skip) {
|
||||||
|
let con = {
|
||||||
|
"@id": "c" + Date.now().toString(36),
|
||||||
|
"@type": "Condition",
|
||||||
|
"type": type,
|
||||||
|
"label": label,
|
||||||
|
"vars": vars
|
||||||
|
}
|
||||||
|
this.data.push(con);
|
||||||
|
if(skip !== true) {
|
||||||
|
this.updateFromData();
|
||||||
|
this.build();
|
||||||
|
}
|
||||||
|
return con;
|
||||||
|
}
|
||||||
|
|
||||||
|
addDirection(source, target) {
|
||||||
|
let dir = {
|
||||||
|
"@id": "d" + Date.now().toString(36),
|
||||||
|
"@type": "Direction",
|
||||||
|
"source": source,
|
||||||
|
"target": target,
|
||||||
|
"conditions": []
|
||||||
|
}
|
||||||
|
this.data.push(dir);
|
||||||
|
this.updateFromData();
|
||||||
|
this.build();
|
||||||
|
return dir;
|
||||||
|
}
|
||||||
|
|
||||||
|
rmDirection(dir) {
|
||||||
|
this._rmNode(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
createMsg() {
|
||||||
|
this.addMsg();
|
||||||
|
this.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
getNodeById(id) {
|
||||||
|
return this.data.filter(node => node['@id'] == id)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use wrapper method, because for event handlers 'this' will refer to
|
||||||
|
* the input object
|
||||||
|
*/
|
||||||
|
getEditEventListener(){
|
||||||
|
let graph = this;
|
||||||
|
let el = function(e){
|
||||||
|
let parts = e.srcElement.name.split('-');
|
||||||
|
let id = parts[0], field = parts[1];
|
||||||
|
console.log(this, graph);
|
||||||
|
let node = graph.getNodeById(id);
|
||||||
|
let path = field.split('.'); // use vars.test to set ['vars']['test'] = value
|
||||||
|
var res=node;
|
||||||
|
for (var i=0;i<path.length;i++){
|
||||||
|
if(i == (path.length -1)) {
|
||||||
|
console.log('last', path[i]);
|
||||||
|
res[path[i]] = e.srcElement.value;
|
||||||
|
} else {
|
||||||
|
res=res[path[i]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// node[field] = e.srcElement.value;
|
||||||
|
|
||||||
|
graph.build();
|
||||||
|
}
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
getJsonString() {
|
||||||
|
// recreate array to have the right order of items.
|
||||||
|
this.data = [...this.messages, ...this.conditions,
|
||||||
|
...this.directions, ...this.interruptions]
|
||||||
|
let d = [];
|
||||||
|
let toRemove = ['sourceX', 'sourceY', 'targetX', 'targetY', 'x','y', 'vx','vy']
|
||||||
|
for(let node of this.data) {
|
||||||
|
let n = {};
|
||||||
|
console.log(node['source']);
|
||||||
|
for (let e in node) {
|
||||||
|
if (node.hasOwnProperty(e) && toRemove.indexOf(e) == -1 ) {
|
||||||
|
if(this.data.indexOf(node[e]) != -1) {
|
||||||
|
n[e] = node[e]['@id'];
|
||||||
|
} else {
|
||||||
|
n[e] = node[e];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
d.push(n);
|
||||||
|
}
|
||||||
|
return JSON.stringify(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveJson() {
|
||||||
|
var blob = new Blob([this.getJsonString()], {type: 'application/json'});
|
||||||
|
if(window.navigator.msSaveOrOpenBlob) {
|
||||||
|
window.navigator.msSaveBlob(blob, "pillow_talk.json");
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
var elem = window.document.createElement('a');
|
||||||
|
elem.href = window.URL.createObjectURL(blob);
|
||||||
|
elem.download = "pillow_talk.json";
|
||||||
|
document.body.appendChild(elem);
|
||||||
|
elem.click();
|
||||||
|
document.body.removeChild(elem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadData(data) {
|
||||||
|
this.data = data;
|
||||||
|
this.updateFromData();
|
||||||
|
this.build(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFromData() {
|
||||||
|
this.messages = this.data.filter((node) => node['@type'] == 'Msg');
|
||||||
|
this.directions = this.data.filter((node) => node['@type'] == 'Direction');
|
||||||
|
this.conditions = this.data.filter((node) => node['@type'] == 'Condition');
|
||||||
|
this.interruptions = this.data.filter((node) => node['@type'] == 'Interruption');
|
||||||
|
|
||||||
|
// save state;
|
||||||
|
this.saveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
saveState() {
|
||||||
|
window.localStorage.setItem("lastState", this.getJsonString());
|
||||||
|
}
|
||||||
|
|
||||||
|
hasSavedState() {
|
||||||
|
return window.localStorage.getItem("lastState") !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFromState() {
|
||||||
|
this.loadData(JSON.parse(window.localStorage.getItem("lastState")));
|
||||||
|
}
|
||||||
|
|
||||||
|
build(isInit) {
|
||||||
|
this.simulation = d3.forceSimulation(this.messages)
|
||||||
|
.force("link", d3.forceLink(this.directions).id(d => d['@id']))
|
||||||
|
.force("charge", d3.forceManyBody().strength(-1000))
|
||||||
|
.force("center", d3.forceCenter(this.width / 2, this.height / 2))
|
||||||
|
.force("collide", d3.forceCollide(this.nodeSize*2))
|
||||||
|
;
|
||||||
|
|
||||||
|
// Update existing nodes
|
||||||
|
let node = this.nodesG
|
||||||
|
.selectAll("g")
|
||||||
|
.data(this.messages, n => n['@id'])
|
||||||
|
;
|
||||||
|
|
||||||
|
// Update existing nodes
|
||||||
|
let newNode = node.enter();
|
||||||
|
|
||||||
|
let newNodeG = newNode.append("g")
|
||||||
|
.attr('id', d => d['@id'])
|
||||||
|
.call(d3.drag(this.simulation))
|
||||||
|
.on('click', function(d){
|
||||||
|
this.clickMsg(d);
|
||||||
|
}.bind(this))
|
||||||
|
;
|
||||||
|
console.log('a');
|
||||||
|
let circle = newNodeG.append("circle")
|
||||||
|
.attr('r', this.nodeSize)
|
||||||
|
// .text(d => d.id)
|
||||||
|
;
|
||||||
|
let text = newNodeG.append("text")
|
||||||
|
;
|
||||||
|
|
||||||
|
// remove
|
||||||
|
node.exit().remove();
|
||||||
|
node = node.merge(newNodeG);
|
||||||
|
|
||||||
|
// for all existing nodes:
|
||||||
|
node.attr('class', msg => {
|
||||||
|
let classes = [];
|
||||||
|
if( this.selectedMsg == msg) classes.push('selectedMsg');
|
||||||
|
if( msg['start'] == true ) classes.push('startMsg');
|
||||||
|
if(this.getDirectionsFrom(msg).length < 1) {
|
||||||
|
classes.push('endMsg');
|
||||||
|
if(this.getDirectionsTo(msg).length < 1) classes.push('orphanedMsg');
|
||||||
|
}
|
||||||
|
|
||||||
|
return classes.join(' ');
|
||||||
|
})
|
||||||
|
|
||||||
|
let link = this.linkG
|
||||||
|
.selectAll("line")
|
||||||
|
.data(this.directions)
|
||||||
|
;
|
||||||
|
let newLink = link.enter()
|
||||||
|
.append("line")
|
||||||
|
;
|
||||||
|
//remove
|
||||||
|
link.exit().remove();
|
||||||
|
link = link.merge(newLink);
|
||||||
|
|
||||||
|
link.attr('class', l => { return `link ` + (l['conditions'].length == 0 ? "link--noconditions" : "link--withconditions"); });
|
||||||
|
|
||||||
|
// console.log('c');
|
||||||
|
let formatText = (t) => {
|
||||||
|
if(t.length > this.maxChars) {
|
||||||
|
return t.substr(0, this.maxChars - 3) + '...';
|
||||||
|
} else {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
node.selectAll("text").text(d => formatText(`(${d['@id']}) ${d['text']}`));
|
||||||
|
// console.log('q');
|
||||||
|
// // TODO: update text
|
||||||
|
// let text = newNodeG.append("text")
|
||||||
|
// // .attr('stroke', "black")
|
||||||
|
// .text(d => formatText(`(${d['@id']}) ${d['text']}`))
|
||||||
|
// // .attr('title', d => d.label)
|
||||||
|
// ;
|
||||||
|
|
||||||
|
let n = this.nodesG;
|
||||||
|
this.simulation.on("tick", () => {
|
||||||
|
link
|
||||||
|
.each(function(d){
|
||||||
|
let sourceX, targetX, midX, dx, dy, angle;
|
||||||
|
// This mess makes the arrows exactly perfect.
|
||||||
|
// thanks to http://bl.ocks.org/curran/9b73eb564c1c8a3d8f3ab207de364bf4
|
||||||
|
if( d.source.x < d.target.x ){
|
||||||
|
sourceX = d.source.x;
|
||||||
|
targetX = d.target.x;
|
||||||
|
} else if( d.target.x < d.source.x ){
|
||||||
|
targetX = d.target.x;
|
||||||
|
sourceX = d.source.x;
|
||||||
|
} else if (d.target.isCircle) {
|
||||||
|
targetX = sourceX = d.target.x;
|
||||||
|
} else if (d.source.isCircle) {
|
||||||
|
targetX = sourceX = d.source.x;
|
||||||
|
} else {
|
||||||
|
midX = (d.source.x + d.target.x) / 2;
|
||||||
|
if(midX > d.target.x){
|
||||||
|
midX = d.target.x;
|
||||||
|
} else if(midX > d.source.x){
|
||||||
|
midX = d.source.x;
|
||||||
|
} else if(midX < d.target.x){
|
||||||
|
midX = d.target.x;
|
||||||
|
} else if(midX < d.source.x){
|
||||||
|
midX = d.source.x;
|
||||||
|
}
|
||||||
|
targetX = sourceX = midX;
|
||||||
|
}
|
||||||
|
|
||||||
|
dx = targetX - sourceX;
|
||||||
|
dy = d.target.y - d.source.y;
|
||||||
|
angle = Math.atan2(dx, dy);
|
||||||
|
|
||||||
|
// Compute the line endpoint such that the arrow
|
||||||
|
// is touching the edge of the node rectangle perfectly.
|
||||||
|
d.sourceX = sourceX + Math.sin(angle) * this.nodeSize;
|
||||||
|
d.targetX = targetX - Math.sin(angle) * this.nodeSize;
|
||||||
|
d.sourceY = d.source.y + Math.cos(angle) * this.nodeSize;
|
||||||
|
d.targetY = d.target.y - Math.cos(angle) * this.nodeSize;
|
||||||
|
}.bind(this))
|
||||||
|
.attr("x1", function(d) { return d.sourceX; })
|
||||||
|
.attr("y1", function(d) { return d.sourceY; })
|
||||||
|
.attr("x2", function(d) { return d.targetX; })
|
||||||
|
.attr("y2", function(d) { return d.targetY; });
|
||||||
|
|
||||||
|
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||||
|
// .attr("cy", d => d.y);
|
||||||
|
});
|
||||||
|
|
||||||
|
// this.simulation.alpha(1);
|
||||||
|
// this.simulation.restart();
|
||||||
|
if(typeof isInit != 'undefined' && isInit) {
|
||||||
|
for (let i = 0, n = Math.ceil(Math.log(this.simulation.alphaMin()) / Math.log(1 - this.simulation.alphaDecay())); i < n; ++i) {
|
||||||
|
this.simulation.tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.svg.node();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var graph = new Graph();
|
||||||
|
|
||||||
|
var openerDiv = document.getElementById("opener");
|
||||||
|
var fileOpenerBtn = document.getElementById('fileOpener');
|
||||||
|
var openerSubmit = document.getElementById('fileLoad');
|
||||||
|
var loadFileFromInput = function (inputEl) {
|
||||||
|
if(inputEl.files && inputEl.files[0]){
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function (e) {
|
||||||
|
var output=e.target.result;
|
||||||
|
let j = JSON.parse(output);
|
||||||
|
graph.loadData(j);
|
||||||
|
openerDiv.parentElement.removeChild(openerDiv);
|
||||||
|
};//end onload()
|
||||||
|
reader.readAsText(inputEl.files[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileOpenerBtn.addEventListener('change', function(){ loadFileFromInput(this); })
|
||||||
|
|
||||||
|
if(fileOpenerBtn.files.length) {
|
||||||
|
openerSubmit.addEventListener('click', function() { loadFileFromInput(fileOpener); });
|
||||||
|
} else {
|
||||||
|
openerSubmit.parentElement.removeChild(openerSubmit);
|
||||||
|
}
|
||||||
|
|
||||||
|
let loadStateBtn = document.getElementById('stateLoad');
|
||||||
|
if(graph.hasSavedState()) {
|
||||||
|
loadStateBtn.addEventListener('click', function() {
|
||||||
|
graph.loadFromState();
|
||||||
|
|
||||||
|
openerDiv.parentElement.removeChild(openerDiv);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
loadStateBtn.parentElement.removeChild(loadStateBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</html>
|
365
www/reconnecting-websocket.js
Normal file
365
www/reconnecting-websocket.js
Normal file
|
@ -0,0 +1,365 @@
|
||||||
|
// MIT License:
|
||||||
|
//
|
||||||
|
// Copyright (c) 2010-2012, Joe Walnes
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included in
|
||||||
|
// all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
// THE SOFTWARE.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This behaves like a WebSocket in every way, except if it fails to connect,
|
||||||
|
* or it gets disconnected, it will repeatedly poll until it successfully connects
|
||||||
|
* again.
|
||||||
|
*
|
||||||
|
* It is API compatible, so when you have:
|
||||||
|
* ws = new WebSocket('ws://....');
|
||||||
|
* you can replace with:
|
||||||
|
* ws = new ReconnectingWebSocket('ws://....');
|
||||||
|
*
|
||||||
|
* The event stream will typically look like:
|
||||||
|
* onconnecting
|
||||||
|
* onopen
|
||||||
|
* onmessage
|
||||||
|
* onmessage
|
||||||
|
* onclose // lost connection
|
||||||
|
* onconnecting
|
||||||
|
* onopen // sometime later...
|
||||||
|
* onmessage
|
||||||
|
* onmessage
|
||||||
|
* etc...
|
||||||
|
*
|
||||||
|
* It is API compatible with the standard WebSocket API, apart from the following members:
|
||||||
|
*
|
||||||
|
* - `bufferedAmount`
|
||||||
|
* - `extensions`
|
||||||
|
* - `binaryType`
|
||||||
|
*
|
||||||
|
* Latest version: https://github.com/joewalnes/reconnecting-websocket/
|
||||||
|
* - Joe Walnes
|
||||||
|
*
|
||||||
|
* Syntax
|
||||||
|
* ======
|
||||||
|
* var socket = new ReconnectingWebSocket(url, protocols, options);
|
||||||
|
*
|
||||||
|
* Parameters
|
||||||
|
* ==========
|
||||||
|
* url - The url you are connecting to.
|
||||||
|
* protocols - Optional string or array of protocols.
|
||||||
|
* options - See below
|
||||||
|
*
|
||||||
|
* Options
|
||||||
|
* =======
|
||||||
|
* Options can either be passed upon instantiation or set after instantiation:
|
||||||
|
*
|
||||||
|
* var socket = new ReconnectingWebSocket(url, null, { debug: true, reconnectInterval: 4000 });
|
||||||
|
*
|
||||||
|
* or
|
||||||
|
*
|
||||||
|
* var socket = new ReconnectingWebSocket(url);
|
||||||
|
* socket.debug = true;
|
||||||
|
* socket.reconnectInterval = 4000;
|
||||||
|
*
|
||||||
|
* debug
|
||||||
|
* - Whether this instance should log debug messages. Accepts true or false. Default: false.
|
||||||
|
*
|
||||||
|
* automaticOpen
|
||||||
|
* - Whether or not the websocket should attempt to connect immediately upon instantiation. The socket can be manually opened or closed at any time using ws.open() and ws.close().
|
||||||
|
*
|
||||||
|
* reconnectInterval
|
||||||
|
* - The number of milliseconds to delay before attempting to reconnect. Accepts integer. Default: 1000.
|
||||||
|
*
|
||||||
|
* maxReconnectInterval
|
||||||
|
* - The maximum number of milliseconds to delay a reconnection attempt. Accepts integer. Default: 30000.
|
||||||
|
*
|
||||||
|
* reconnectDecay
|
||||||
|
* - The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. Accepts integer or float. Default: 1.5.
|
||||||
|
*
|
||||||
|
* timeoutInterval
|
||||||
|
* - The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. Accepts integer. Default: 2000.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
(function (global, factory) {
|
||||||
|
if (typeof define === 'function' && define.amd) {
|
||||||
|
define([], factory);
|
||||||
|
} else if (typeof module !== 'undefined' && module.exports){
|
||||||
|
module.exports = factory();
|
||||||
|
} else {
|
||||||
|
global.ReconnectingWebSocket = factory();
|
||||||
|
}
|
||||||
|
})(this, function () {
|
||||||
|
|
||||||
|
if (!('WebSocket' in window)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReconnectingWebSocket(url, protocols, options) {
|
||||||
|
|
||||||
|
// Default settings
|
||||||
|
var settings = {
|
||||||
|
|
||||||
|
/** Whether this instance should log debug messages. */
|
||||||
|
debug: false,
|
||||||
|
|
||||||
|
/** Whether or not the websocket should attempt to connect immediately upon instantiation. */
|
||||||
|
automaticOpen: true,
|
||||||
|
|
||||||
|
/** The number of milliseconds to delay before attempting to reconnect. */
|
||||||
|
reconnectInterval: 1000,
|
||||||
|
/** The maximum number of milliseconds to delay a reconnection attempt. */
|
||||||
|
maxReconnectInterval: 30000,
|
||||||
|
/** The rate of increase of the reconnect delay. Allows reconnect attempts to back off when problems persist. */
|
||||||
|
reconnectDecay: 1.5,
|
||||||
|
|
||||||
|
/** The maximum time in milliseconds to wait for a connection to succeed before closing and retrying. */
|
||||||
|
timeoutInterval: 2000,
|
||||||
|
|
||||||
|
/** The maximum number of reconnection attempts to make. Unlimited if null. */
|
||||||
|
maxReconnectAttempts: null,
|
||||||
|
|
||||||
|
/** The binary type, possible values 'blob' or 'arraybuffer', default 'blob'. */
|
||||||
|
binaryType: 'blob'
|
||||||
|
}
|
||||||
|
if (!options) { options = {}; }
|
||||||
|
|
||||||
|
// Overwrite and define settings with options if they exist.
|
||||||
|
for (var key in settings) {
|
||||||
|
if (typeof options[key] !== 'undefined') {
|
||||||
|
this[key] = options[key];
|
||||||
|
} else {
|
||||||
|
this[key] = settings[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// These should be treated as read-only properties
|
||||||
|
|
||||||
|
/** The URL as resolved by the constructor. This is always an absolute URL. Read only. */
|
||||||
|
this.url = url;
|
||||||
|
|
||||||
|
/** The number of attempted reconnects since starting, or the last successful connection. Read only. */
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current state of the connection.
|
||||||
|
* Can be one of: WebSocket.CONNECTING, WebSocket.OPEN, WebSocket.CLOSING, WebSocket.CLOSED
|
||||||
|
* Read only.
|
||||||
|
*/
|
||||||
|
this.readyState = WebSocket.CONNECTING;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A string indicating the name of the sub-protocol the server selected; this will be one of
|
||||||
|
* the strings specified in the protocols parameter when creating the WebSocket object.
|
||||||
|
* Read only.
|
||||||
|
*/
|
||||||
|
this.protocol = null;
|
||||||
|
|
||||||
|
// Private state variables
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
var ws;
|
||||||
|
var forcedClose = false;
|
||||||
|
var timedOut = false;
|
||||||
|
var eventTarget = document.createElement('div');
|
||||||
|
|
||||||
|
// Wire up "on*" properties as event handlers
|
||||||
|
|
||||||
|
eventTarget.addEventListener('open', function(event) { self.onopen(event); });
|
||||||
|
eventTarget.addEventListener('close', function(event) { self.onclose(event); });
|
||||||
|
eventTarget.addEventListener('connecting', function(event) { self.onconnecting(event); });
|
||||||
|
eventTarget.addEventListener('message', function(event) { self.onmessage(event); });
|
||||||
|
eventTarget.addEventListener('error', function(event) { self.onerror(event); });
|
||||||
|
|
||||||
|
// Expose the API required by EventTarget
|
||||||
|
|
||||||
|
this.addEventListener = eventTarget.addEventListener.bind(eventTarget);
|
||||||
|
this.removeEventListener = eventTarget.removeEventListener.bind(eventTarget);
|
||||||
|
this.dispatchEvent = eventTarget.dispatchEvent.bind(eventTarget);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function generates an event that is compatible with standard
|
||||||
|
* compliant browsers and IE9 - IE11
|
||||||
|
*
|
||||||
|
* This will prevent the error:
|
||||||
|
* Object doesn't support this action
|
||||||
|
*
|
||||||
|
* http://stackoverflow.com/questions/19345392/why-arent-my-parameters-getting-passed-through-to-a-dispatched-event/19345563#19345563
|
||||||
|
* @param s String The name that the event should use
|
||||||
|
* @param args Object an optional object that the event will use
|
||||||
|
*/
|
||||||
|
function generateEvent(s, args) {
|
||||||
|
var evt = document.createEvent("CustomEvent");
|
||||||
|
evt.initCustomEvent(s, false, false, args);
|
||||||
|
return evt;
|
||||||
|
};
|
||||||
|
|
||||||
|
this.open = function (reconnectAttempt) {
|
||||||
|
ws = new WebSocket(self.url, protocols || []);
|
||||||
|
ws.binaryType = this.binaryType;
|
||||||
|
|
||||||
|
if (reconnectAttempt) {
|
||||||
|
if (this.maxReconnectAttempts && this.reconnectAttempts > this.maxReconnectAttempts) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
eventTarget.dispatchEvent(generateEvent('connecting'));
|
||||||
|
this.reconnectAttempts = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self.debug || ReconnectingWebSocket.debugAll) {
|
||||||
|
console.debug('ReconnectingWebSocket', 'attempt-connect', self.url);
|
||||||
|
}
|
||||||
|
|
||||||
|
var localWs = ws;
|
||||||
|
var timeout = setTimeout(function() {
|
||||||
|
if (self.debug || ReconnectingWebSocket.debugAll) {
|
||||||
|
console.debug('ReconnectingWebSocket', 'connection-timeout', self.url);
|
||||||
|
}
|
||||||
|
timedOut = true;
|
||||||
|
localWs.close();
|
||||||
|
timedOut = false;
|
||||||
|
}, self.timeoutInterval);
|
||||||
|
|
||||||
|
ws.onopen = function(event) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
if (self.debug || ReconnectingWebSocket.debugAll) {
|
||||||
|
console.debug('ReconnectingWebSocket', 'onopen', self.url);
|
||||||
|
}
|
||||||
|
self.protocol = ws.protocol;
|
||||||
|
self.readyState = WebSocket.OPEN;
|
||||||
|
self.reconnectAttempts = 0;
|
||||||
|
var e = generateEvent('open');
|
||||||
|
e.isReconnect = reconnectAttempt;
|
||||||
|
reconnectAttempt = false;
|
||||||
|
eventTarget.dispatchEvent(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = function(event) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
ws = null;
|
||||||
|
if (forcedClose) {
|
||||||
|
self.readyState = WebSocket.CLOSED;
|
||||||
|
eventTarget.dispatchEvent(generateEvent('close'));
|
||||||
|
} else {
|
||||||
|
self.readyState = WebSocket.CONNECTING;
|
||||||
|
var e = generateEvent('connecting');
|
||||||
|
e.code = event.code;
|
||||||
|
e.reason = event.reason;
|
||||||
|
e.wasClean = event.wasClean;
|
||||||
|
eventTarget.dispatchEvent(e);
|
||||||
|
if (!reconnectAttempt && !timedOut) {
|
||||||
|
if (self.debug || ReconnectingWebSocket.debugAll) {
|
||||||
|
console.debug('ReconnectingWebSocket', 'onclose', self.url);
|
||||||
|
}
|
||||||
|
eventTarget.dispatchEvent(generateEvent('close'));
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeout = self.reconnectInterval * Math.pow(self.reconnectDecay, self.reconnectAttempts);
|
||||||
|
setTimeout(function() {
|
||||||
|
self.reconnectAttempts++;
|
||||||
|
self.open(true);
|
||||||
|
}, timeout > self.maxReconnectInterval ? self.maxReconnectInterval : timeout);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ws.onmessage = function(event) {
|
||||||
|
if (self.debug || ReconnectingWebSocket.debugAll) {
|
||||||
|
console.debug('ReconnectingWebSocket', 'onmessage', self.url, event.data);
|
||||||
|
}
|
||||||
|
var e = generateEvent('message');
|
||||||
|
e.data = event.data;
|
||||||
|
eventTarget.dispatchEvent(e);
|
||||||
|
};
|
||||||
|
ws.onerror = function(event) {
|
||||||
|
if (self.debug || ReconnectingWebSocket.debugAll) {
|
||||||
|
console.debug('ReconnectingWebSocket', 'onerror', self.url, event);
|
||||||
|
}
|
||||||
|
eventTarget.dispatchEvent(generateEvent('error'));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether or not to create a websocket upon instantiation
|
||||||
|
if (this.automaticOpen == true) {
|
||||||
|
this.open(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transmits data to the server over the WebSocket connection.
|
||||||
|
*
|
||||||
|
* @param data a text string, ArrayBuffer or Blob to send to the server.
|
||||||
|
*/
|
||||||
|
this.send = function(data) {
|
||||||
|
if (ws) {
|
||||||
|
if (self.debug || ReconnectingWebSocket.debugAll) {
|
||||||
|
console.debug('ReconnectingWebSocket', 'send', self.url, data);
|
||||||
|
}
|
||||||
|
return ws.send(data);
|
||||||
|
} else {
|
||||||
|
throw 'INVALID_STATE_ERR : Pausing to reconnect websocket';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Closes the WebSocket connection or connection attempt, if any.
|
||||||
|
* If the connection is already CLOSED, this method does nothing.
|
||||||
|
*/
|
||||||
|
this.close = function(code, reason) {
|
||||||
|
// Default CLOSE_NORMAL code
|
||||||
|
if (typeof code == 'undefined') {
|
||||||
|
code = 1000;
|
||||||
|
}
|
||||||
|
forcedClose = true;
|
||||||
|
if (ws) {
|
||||||
|
ws.close(code, reason);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional public API method to refresh the connection if still open (close, re-open).
|
||||||
|
* For example, if the app suspects bad data / missed heart beats, it can try to refresh.
|
||||||
|
*/
|
||||||
|
this.refresh = function() {
|
||||||
|
if (ws) {
|
||||||
|
ws.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event listener to be called when the WebSocket connection's readyState changes to OPEN;
|
||||||
|
* this indicates that the connection is ready to send and receive data.
|
||||||
|
*/
|
||||||
|
ReconnectingWebSocket.prototype.onopen = function(event) {};
|
||||||
|
/** An event listener to be called when the WebSocket connection's readyState changes to CLOSED. */
|
||||||
|
ReconnectingWebSocket.prototype.onclose = function(event) {};
|
||||||
|
/** An event listener to be called when a connection begins being attempted. */
|
||||||
|
ReconnectingWebSocket.prototype.onconnecting = function(event) {};
|
||||||
|
/** An event listener to be called when a message is received from the server. */
|
||||||
|
ReconnectingWebSocket.prototype.onmessage = function(event) {};
|
||||||
|
/** An event listener to be called when an error occurs. */
|
||||||
|
ReconnectingWebSocket.prototype.onerror = function(event) {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether all instances of ReconnectingWebSocket should log debug messages.
|
||||||
|
* Setting this to true is the equivalent of setting all instances of ReconnectingWebSocket.debug to true.
|
||||||
|
*/
|
||||||
|
ReconnectingWebSocket.debugAll = false;
|
||||||
|
|
||||||
|
ReconnectingWebSocket.CONNECTING = WebSocket.CONNECTING;
|
||||||
|
ReconnectingWebSocket.OPEN = WebSocket.OPEN;
|
||||||
|
ReconnectingWebSocket.CLOSING = WebSocket.CLOSING;
|
||||||
|
ReconnectingWebSocket.CLOSED = WebSocket.CLOSED;
|
||||||
|
|
||||||
|
return ReconnectingWebSocket;
|
||||||
|
});
|
44
www/styles.css
Normal file
44
www/styles.css
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
body{
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#status{
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
width: 430px;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
#status > div{
|
||||||
|
width: 33.3333333%;
|
||||||
|
height: 150px;
|
||||||
|
border: solid 1px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hugvey{
|
||||||
|
background-image: linear-gradient(to top, #587457, #35a589);
|
||||||
|
color:white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.hugvey.hugvey--off{
|
||||||
|
background-image: linear-gradient(to top, #575d74, #3572a5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hugvey h1{
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.hugvey.hugvey--off h1::after{
|
||||||
|
content: '[off]'
|
||||||
|
}
|
||||||
|
.hugvey.hugvey--on h1 {
|
||||||
|
position: absolute;
|
||||||
|
left: 5px;
|
||||||
|
top: 5px;
|
||||||
|
|
||||||
|
}
|
11055
www/vue.js
Normal file
11055
www/vue.js
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue