Edouard@2771: #!/usr/bin/env python Edouard@2771: # -*- coding: utf-8 -*- Edouard@2771: Edouard@2771: # This file is part of Beremiz Edouard@2771: # Copyright (C) 2019: Edouard TISSERANT Edouard@2771: # See COPYING file for copyrights details. Edouard@2771: kinsamanka@3750: Edouard@2799: import errno Edouard@2822: from threading import RLock, Timer Edouard@3647: import os, time Edouard@2771: Edouard@2823: try: Edouard@2823: from runtime.spawn_subprocess import Popen Edouard@2823: except ImportError: Edouard@2823: from subprocess import Popen Edouard@2823: Edouard@2771: from twisted.web.server import Site Edouard@2771: from twisted.web.resource import Resource Edouard@2771: from twisted.internet import reactor Edouard@2771: from twisted.web.static import File Edouard@2771: Edouard@2771: from autobahn.twisted.websocket import WebSocketServerFactory, WebSocketServerProtocol Edouard@2799: from autobahn.websocket.protocol import WebSocketProtocol Edouard@2771: from autobahn.twisted.resource import WebSocketResource Edouard@2771: Edouard@3647: from runtime.loglevels import LogLevelsDict Edouard@3647: from runtime import GetPLCObjectSingleton Edouard@3647: Edouard@3270: max_svghmi_sessions = None Edouard@2822: svghmi_watchdog = None Edouard@2771: Edouard@3271: Edouard@3271: svghmi_wait = PLCBinary.svghmi_wait Edouard@3271: svghmi_wait.restype = ctypes.c_int # error or 0 Edouard@3271: svghmi_wait.argtypes = [] Edouard@3271: Edouard@3281: svghmi_continue_collect = ctypes.c_int.in_dll(PLCBinary, "svghmi_continue_collect") Edouard@3281: Edouard@2774: svghmi_send_collect = PLCBinary.svghmi_send_collect Edouard@2774: svghmi_send_collect.restype = ctypes.c_int # error or 0 Edouard@2774: svghmi_send_collect.argtypes = [ Edouard@3270: ctypes.c_uint32, # index Edouard@2774: ctypes.POINTER(ctypes.c_uint32), # size Edouard@2799: ctypes.POINTER(ctypes.c_void_p)] # data ptr Edouard@2774: Edouard@3271: svghmi_reset = PLCBinary.svghmi_reset Edouard@3271: svghmi_reset.restype = ctypes.c_int # error or 0 Edouard@3271: svghmi_reset.argtypes = [ Edouard@3271: ctypes.c_uint32] # index Edouard@3271: Edouard@2774: svghmi_recv_dispatch = PLCBinary.svghmi_recv_dispatch Edouard@2774: svghmi_recv_dispatch.restype = ctypes.c_int # error or 0 Edouard@2774: svghmi_recv_dispatch.argtypes = [ Edouard@3270: ctypes.c_uint32, # index Edouard@2788: ctypes.c_uint32, # size Edouard@2779: ctypes.c_char_p] # data ptr Edouard@3270: Edouard@3270: class HMISessionMgr(object): Edouard@3270: def __init__(self): Edouard@3270: self.multiclient_sessions = set() Edouard@3270: self.watchdog_session = None Edouard@3270: self.session_count = 0 Edouard@3270: self.lock = RLock() Edouard@3270: self.indexes = set() Edouard@3270: Edouard@3270: def next_index(self): Edouard@3270: if self.indexes: Edouard@3270: greatest = max(self.indexes) Edouard@3270: holes = set(range(greatest)) - self.indexes Edouard@3270: index = min(holes) if holes else greatest+1 Edouard@3270: else: Edouard@3270: index = 0 Edouard@3270: self.indexes.add(index) Edouard@3270: return index Edouard@3270: Edouard@3270: def free_index(self, index): Edouard@3270: self.indexes.remove(index) Edouard@3270: Edouard@3270: def register(self, session): Edouard@3270: global max_svghmi_sessions Edouard@3270: with self.lock: Edouard@3270: if session.is_watchdog_session: Edouard@3270: # Creating a new watchdog session closes pre-existing one Edouard@3270: if self.watchdog_session is not None: Edouard@3270: self.unregister(self.watchdog_session) Edouard@3270: else: Edouard@3270: assert(self.session_count < max_svghmi_sessions) Edouard@3270: self.session_count += 1 Edouard@3270: Edouard@3270: self.watchdog_session = session Edouard@3270: else: Edouard@3270: assert(self.session_count < max_svghmi_sessions) Edouard@3270: self.multiclient_sessions.add(session) Edouard@3270: self.session_count += 1 Edouard@3270: session.session_index = self.next_index() Edouard@3270: Edouard@3270: def unregister(self, session): Edouard@3270: with self.lock: Edouard@3271: if session.is_watchdog_session : Edouard@3271: if self.watchdog_session != session: Edouard@3271: return Edouard@3270: self.watchdog_session = None Edouard@3270: else: Edouard@3271: try: Edouard@3278: self.multiclient_sessions.remove(session) Edouard@3271: except KeyError: Edouard@3271: return Edouard@3271: self.free_index(session.session_index) Edouard@3270: self.session_count -= 1 Edouard@3271: session.kill() Edouard@3273: Edouard@3270: def close_all(self): edouard@3272: for session in self.iter_sessions(): edouard@3272: self.unregister(session) Edouard@3273: Edouard@3270: def iter_sessions(self): Edouard@3270: with self.lock: edouard@3272: lst = list(self.multiclient_sessions) edouard@3272: if self.watchdog_session is not None: edouard@3272: lst = [self.watchdog_session]+lst edouard@3272: for nxt_session in lst: edouard@3272: yield nxt_session Edouard@3270: Edouard@3270: Edouard@3270: svghmi_session_manager = HMISessionMgr() Edouard@3270: Edouard@2774: Edouard@2771: class HMISession(object): Edouard@2771: def __init__(self, protocol_instance): Edouard@2773: self.protocol_instance = protocol_instance Edouard@3270: self._session_index = None Edouard@3271: self.closed = False Edouard@3270: Edouard@3270: @property Edouard@3270: def is_watchdog_session(self): Edouard@3270: return self.protocol_instance.has_watchdog Edouard@3270: Edouard@3270: @property Edouard@3270: def session_index(self): Edouard@3270: return self._session_index Edouard@3270: Edouard@3270: @session_index.setter Edouard@3270: def session_index(self, value): Edouard@3270: self._session_index = value Edouard@2775: Edouard@3271: def reset(self): Edouard@3271: return svghmi_reset(self.session_index) Edouard@3271: Edouard@2799: def close(self): Edouard@3271: if self.closed: return Edouard@2799: self.protocol_instance.sendClose(WebSocketProtocol.CLOSE_STATUS_CODE_NORMAL) Edouard@2771: Edouard@3271: def notify_closed(self): Edouard@3271: self.closed = True Edouard@3271: Edouard@3271: def kill(self): Edouard@3271: self.close() Edouard@3271: self.reset() Edouard@3271: Edouard@2772: def onMessage(self, msg): Edouard@2779: # pass message to the C side recieve_message() Edouard@3271: if self.closed: return Edouard@3270: return svghmi_recv_dispatch(self.session_index, len(msg), msg) Edouard@2777: Edouard@2774: def sendMessage(self, msg): Edouard@3271: if self.closed: return Edouard@2799: self.protocol_instance.sendMessage(msg, True) Edouard@2799: return 0 Edouard@2771: Edouard@2822: class Watchdog(object): Edouard@2831: def __init__(self, initial_timeout, interval, callback): Edouard@2822: self._callback = callback Edouard@2822: self.lock = RLock() Edouard@2822: self.initial_timeout = initial_timeout Edouard@2831: self.interval = interval Edouard@2822: with self.lock: Edouard@2822: self._start() Edouard@2822: Edouard@2831: def _start(self, rearm=False): Edouard@2831: duration = self.interval if rearm else self.initial_timeout Edouard@2831: if duration: Edouard@2831: self.timer = Timer(duration, self.trigger) Edouard@2831: self.timer.start() Edouard@2835: else: Edouard@2835: self.timer = None Edouard@2822: Edouard@2822: def _stop(self): Edouard@2822: if self.timer is not None: Edouard@2822: self.timer.cancel() Edouard@2822: self.timer = None Edouard@2822: Edouard@2822: def cancel(self): Edouard@2822: with self.lock: Edouard@2822: self._stop() Edouard@2822: Edouard@2832: def feed(self, rearm=True): Edouard@2822: with self.lock: Edouard@2822: self._stop() Edouard@2832: self._start(rearm) Edouard@2822: Edouard@2822: def trigger(self): Edouard@2822: self._callback() Edouard@3270: # Don't repeat trigger periodically Edouard@3270: # # wait for initial timeout on re-start Edouard@3270: # self.feed(rearm=False) Edouard@2822: Edouard@2771: class HMIProtocol(WebSocketServerProtocol): Edouard@2771: Edouard@2771: def __init__(self, *args, **kwargs): Edouard@2771: self._hmi_session = None Edouard@3270: self.has_watchdog = False Edouard@2771: WebSocketServerProtocol.__init__(self, *args, **kwargs) Edouard@2771: edouard@3268: def onConnect(self, request): edouard@3268: self.has_watchdog = request.params.get("mode", [None])[0] == "watchdog" edouard@3268: return WebSocketServerProtocol.onConnect(self, request) edouard@3268: Edouard@2771: def onOpen(self): Edouard@3270: global svghmi_session_manager Edouard@2799: assert(self._hmi_session is None) Edouard@3278: _hmi_session = HMISession(self) Edouard@3278: registered = svghmi_session_manager.register(_hmi_session) Edouard@3278: self._hmi_session = _hmi_session Edouard@3661: self._hmi_session.reset() Edouard@2771: Edouard@2771: def onClose(self, wasClean, code, reason): Edouard@3270: global svghmi_session_manager Edouard@3278: if self._hmi_session is None : return Edouard@3271: self._hmi_session.notify_closed() Edouard@3270: svghmi_session_manager.unregister(self._hmi_session) Edouard@2771: self._hmi_session = None Edouard@2771: Edouard@2771: def onMessage(self, msg, isBinary): Edouard@3270: global svghmi_watchdog Edouard@3278: if self._hmi_session is None : return Edouard@2822: result = self._hmi_session.onMessage(msg) Edouard@3270: if result == 1 and self.has_watchdog: # was heartbeat Edouard@2822: if svghmi_watchdog is not None: Edouard@2822: svghmi_watchdog.feed() Edouard@2775: Edouard@2779: class HMIWebSocketServerFactory(WebSocketServerFactory): Edouard@2779: protocol = HMIProtocol Edouard@2779: edouard@3269: svghmi_servers = {} Edouard@2775: svghmi_send_thread = None Edouard@2775: edouard@3310: # python's errno on windows seems to have no ENODATA edouard@3310: ENODATA = errno.ENODATA if hasattr(errno,"ENODATA") else None edouard@3310: Edouard@2776: def SendThreadProc(): Edouard@3270: global svghmi_session_manager Edouard@2819: size = ctypes.c_uint32() Edouard@2819: ptr = ctypes.c_void_p() Edouard@2819: res = 0 Edouard@3281: while svghmi_continue_collect: Edouard@3271: svghmi_wait() Edouard@3270: for svghmi_session in svghmi_session_manager.iter_sessions(): Edouard@3270: res = svghmi_send_collect( Edouard@3270: svghmi_session.session_index, Edouard@3270: ctypes.byref(size), ctypes.byref(ptr)) Edouard@3270: if res == 0: Edouard@3270: svghmi_session.sendMessage( Edouard@3270: ctypes.string_at(ptr.value,size.value)) edouard@3310: elif res == ENODATA: Edouard@3270: # this happens when there is no data after wakeup Edouard@3271: # because of hmi data refresh period longer than Edouard@3270: # PLC common ticktime Edouard@3271: pass Edouard@3270: else: Edouard@3270: # this happens when finishing Edouard@3270: break Edouard@2776: Edouard@3284: def AddPathToSVGHMIServers(path, factory, *args, **kwargs): kinsamanka@3750: for k,v in svghmi_servers.items(): edouard@3269: svghmi_root, svghmi_listener, path_list = v Edouard@3284: svghmi_root.putChild(path, factory(*args, **kwargs)) Edouard@2779: Edouard@2771: # Called by PLCObject at start Edouard@2993: def _runtime_00_svghmi_start(): edouard@3269: global svghmi_send_thread Edouard@2771: Edouard@2771: # start a thread that call the C part of SVGHMI Edouard@2775: svghmi_send_thread = Thread(target=SendThreadProc, name="SVGHMI Send") Edouard@2775: svghmi_send_thread.start() Edouard@2771: Edouard@2771: Edouard@2771: # Called by PLCObject at stop Edouard@2993: def _runtime_00_svghmi_stop(): edouard@3269: global svghmi_send_thread, svghmi_session Edouard@2822: Edouard@3270: svghmi_session_manager.close_all() Edouard@3270: Edouard@2775: # plc cleanup calls svghmi_(locstring)_cleanup and unlocks send thread Edouard@2775: svghmi_send_thread.join() Edouard@2775: svghmi_send_thread = None Edouard@2775: Edouard@2830: Edouard@2830: class NoCacheFile(File): Edouard@2830: def render_GET(self, request): Edouard@2830: request.setHeader(b"Cache-Control", b"no-cache, no-store") Edouard@2830: return File.render_GET(self, request) Edouard@2830: render_HEAD = render_GET Edouard@2830: Edouard@2830: Edouard@3647: def waitpid_timeout(proc, helpstr="", timeout = 3): Edouard@3647: if proc is None: Edouard@3647: return edouard@3864: def waitpid_timeout_loop(proc = proc, timeout = timeout): Edouard@3647: try: edouard@3864: while proc.poll() is None: edouard@4003: time.sleep(.1) edouard@4003: timeout = timeout - .1 edouard@4003: if timeout <= 0: Edouard@3647: GetPLCObjectSingleton().LogMessage( Edouard@3647: LogLevelsDict["WARNING"], edouard@3864: "Timeout waiting for {} PID: {}".format(helpstr, str(proc.pid))) Edouard@3647: break Edouard@3647: except OSError: Edouard@3647: # workaround exception "OSError: [Errno 10] No child processes" Edouard@3647: pass edouard@4003: waitpid_timeout_loop() edouard@4003: