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@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@2822: def __init__(self, initial_timeout, callback): Edouard@2822: self._callback = callback Edouard@2822: self.lock = RLock() Edouard@2822: self.initial_timeout = initial_timeout Edouard@2822: self.callback = callback Edouard@2822: with self.lock: Edouard@2822: self._start() Edouard@2822: Edouard@2822: def _start(self): Edouard@2822: self.timer = Timer(self.initial_timeout, self.trigger) Edouard@2822: self.timer.start() 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@2822: def feed(self): Edouard@2822: with self.lock: Edouard@2822: self._stop() Edouard@2822: self._start() Edouard@2822: Edouard@2822: def trigger(self): Edouard@2822: self._callback() Edouard@2822: self.feed() 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@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@2771: svghmi_root = None Edouard@2771: svghmi_listener = None 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@2776: Edouard@2822: def watchdog_trigger(): Edouard@2822: print("SVGHMI watchdog trigger") Edouard@2822: Edouard@2779: Edouard@2771: # Called by PLCObject at start Edouard@2771: def _runtime_svghmi0_start(): Edouard@2822: global svghmi_listener, svghmi_root, svghmi_send_thread, svghmi_watchdog Edouard@2771: Edouard@2771: svghmi_root = Resource() Edouard@2779: svghmi_root.putChild("ws", WebSocketResource(HMIWebSocketServerFactory())) Edouard@2771: Edouard@2779: svghmi_listener = reactor.listenTCP(8008, Site(svghmi_root)) 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@2822: svghmi_watchdog = Watchdog(5, watchdog_trigger) Edouard@2771: Edouard@2771: # Called by PLCObject at stop Edouard@2771: def _runtime_svghmi0_stop(): Edouard@2822: global svghmi_listener, svghmi_root, svghmi_send_thread, svghmi_session, svghmi_watchdog Edouard@2822: Edouard@2822: if svghmi_watchdog is not None: Edouard@2822: svghmi_watchdog.cancel() Edouard@2822: svghmi_watchdog = None Edouard@2822: Edouard@2799: if svghmi_session is not None: Edouard@2799: svghmi_session.close() Edouard@2775: svghmi_root.delEntity("ws") Edouard@2775: svghmi_root = None Edouard@2772: svghmi_listener.stopListening() Edouard@2775: svghmi_listener = None 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: