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: Edouard@2771: from __future__ import absolute_import Edouard@2799: import errno Edouard@2822: from threading import RLock, Timer 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@2773: # TODO multiclient : Edouard@2773: # session list lock Edouard@2773: # svghmi_sessions = [] Edouard@2822: # svghmi_watchdogs = [] Edouard@2773: Edouard@2773: svghmi_session = None Edouard@2822: svghmi_watchdog = None Edouard@2771: 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@2774: ctypes.POINTER(ctypes.c_uint32), # size Edouard@2799: ctypes.POINTER(ctypes.c_void_p)] # data ptr Edouard@2774: # TODO multiclient : switch to arrays Edouard@2774: 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@2788: ctypes.c_uint32, # size Edouard@2779: ctypes.c_char_p] # data ptr Edouard@2774: # TODO multiclient : switch to arrays Edouard@2774: Edouard@2771: class HMISession(object): Edouard@2771: def __init__(self, protocol_instance): Edouard@2773: global svghmi_session Edouard@2799: Edouard@2799: # Single client : Edouard@2799: # Creating a new HMISession closes pre-existing HMISession Edouard@2799: if svghmi_session is not None: Edouard@2799: svghmi_session.close() Edouard@2773: svghmi_session = self Edouard@2773: self.protocol_instance = protocol_instance Edouard@2771: Edouard@2771: # TODO multiclient : Edouard@2773: # svghmi_sessions.append(self) Edouard@2771: # get a unique bit index amont other svghmi_sessions, Edouard@2771: # so that we can match flags passed by C->python callback Edouard@2775: Edouard@2799: def close(self): Edouard@2773: global svghmi_session Edouard@2799: if svghmi_session == self: Edouard@2799: svghmi_session = None Edouard@2799: self.protocol_instance.sendClose(WebSocketProtocol.CLOSE_STATUS_CODE_NORMAL) Edouard@2771: Edouard@2772: def onMessage(self, msg): Edouard@2779: # pass message to the C side recieve_message() Edouard@2822: return svghmi_recv_dispatch(len(msg), msg) Edouard@2771: Edouard@2771: # TODO multiclient : pass client index as well Edouard@2777: Edouard@2774: def sendMessage(self, msg): 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: self.callback = callback 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@2832: # wait for initial timeout on re-start Edouard@2832: 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@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@2799: assert(self._hmi_session is None) Edouard@2771: self._hmi_session = HMISession(self) Edouard@2771: Edouard@2771: def onClose(self, wasClean, code, reason): Edouard@2771: self._hmi_session = None Edouard@2771: Edouard@2771: def onMessage(self, msg, isBinary): Edouard@2799: assert(self._hmi_session is not None) Edouard@2822: Edouard@2822: result = self._hmi_session.onMessage(msg) Edouard@2822: if result == 1 : # 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@2776: def SendThreadProc(): Edouard@2819: global svghmi_session Edouard@2819: size = ctypes.c_uint32() Edouard@2819: ptr = ctypes.c_void_p() Edouard@2819: res = 0 Edouard@2819: while True: Edouard@2819: res=svghmi_send_collect(ctypes.byref(size), ctypes.byref(ptr)) Edouard@2819: if res == 0: Edouard@2819: # TODO multiclient : dispatch to sessions Edouard@2819: if svghmi_session is not None: Edouard@2819: svghmi_session.sendMessage(ctypes.string_at(ptr.value,size.value)) Edouard@2819: elif res == errno.ENODATA: Edouard@2819: # this happens when there is no data after wakeup Edouard@2819: # because of hmi data refresh period longer than PLC common ticktime Edouard@2819: pass Edouard@2819: else: Edouard@2819: # this happens when finishing Edouard@2819: break Edouard@2776: edouard@3269: def AddPathToSVGHMIServers(path, factory): edouard@3269: for k,v in svghmi_servers.iteritems(): edouard@3269: svghmi_root, svghmi_listener, path_list = v edouard@3269: svghmi_root.putChild(path, factory()) 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@2799: if svghmi_session is not None: Edouard@2799: svghmi_session.close() 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: