├── README.md ├── README.txt ├── Start.bat ├── build └── lib │ ├── index │ ├── __init__.py │ ├── forms.py │ ├── models.py │ ├── routes.py │ └── utils.py │ ├── myplc │ └── __init__.py │ └── myserver │ └── __init__.py ├── dist └── s71200opc-0.1.dev0-py3.7.egg ├── index ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-37.pyc │ ├── forms.cpython-37.pyc │ ├── models.cpython-37.pyc │ ├── routes.cpython-37.pyc │ └── utils.cpython-37.pyc ├── forms.py ├── models.py ├── routes.py ├── site.db ├── static │ ├── True.png │ ├── bootstrap.min.css │ ├── bootstrap.min.js │ ├── feather.min.js │ ├── jquery.js │ ├── popper.js │ ├── styles.css │ └── styles.js ├── templates │ ├── base.html │ ├── index.html │ ├── partials │ │ ├── nav.html │ │ ├── object_creation.html │ │ └── variable_creation.html │ └── server.html └── utils.py ├── install.bat ├── myplc ├── __init__.py └── __pycache__ │ └── __init__.cpython-37.pyc ├── myserver ├── __init__.py └── __pycache__ │ └── __init__.cpython-37.pyc ├── run.py ├── s71200opc.egg-info ├── PKG-INFO ├── SOURCES.txt ├── dependency_links.txt ├── requires.txt └── top_level.txt └── setup.py /README.md: -------------------------------------------------------------------------------- 1 | # opcserver 2 | A project designed to let people create an intance of an OPC UA server with a browser-based GUI 3 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | This project allows creation of an opc server that can allow a plc to be connected to. -------------------------------------------------------------------------------- /Start.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | title S71200 OPC 3 | python run.py 4 | echo Open localhost:5000 5 | PAUSE -------------------------------------------------------------------------------- /build/lib/index/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_sqlalchemy import SQLAlchemy 3 | 4 | 5 | app = Flask(__name__) 6 | app.config['SECRET_KEY'] = 'f1d9d48ec0e26e2a250839fa36ea2c602cc4f85ccfeb5c65' 7 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db' 8 | from flask_wtf.csrf import CSRFProtect 9 | csrf = CSRFProtect(app) 10 | db = SQLAlchemy(app) 11 | 12 | from index import routes 13 | -------------------------------------------------------------------------------- /build/lib/index/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm,Form 2 | from wtforms import StringField,SubmitField,SelectField,HiddenField,BooleanField 3 | from wtforms.validators import DataRequired 4 | from index.models import Server,Object 5 | 6 | 7 | class ServerCreateForm( FlaskForm ): 8 | 9 | server_name = StringField('Server', 10 | validators=[ DataRequired() ], 11 | render_kw = { 'placeholder': "Server Name" 12 | } 13 | ) 14 | 15 | endpoint_url = StringField('Endpoint Url', 16 | validators=[ DataRequired() ], 17 | render_kw = { 18 | 'placeholder': "Endpoint URL" 19 | } 20 | ) 21 | 22 | namespace = StringField('Namespace', 23 | render_kw = { 24 | 'placeholder': "Namespace" 25 | } 26 | ) 27 | 28 | submit = SubmitField('Create Server',id="create_server_button") 29 | 30 | class ObjectCreateForm( FlaskForm ): 31 | 32 | object_name = StringField('Object Name', 33 | validators=[ DataRequired() ], 34 | render_kw = { 35 | 'placeholder': "Object Name" 36 | } 37 | ) 38 | 39 | server = HiddenField('Server', 40 | validators=[ DataRequired() ], 41 | render_kw = { 42 | 'placeholder': 'Server' 43 | } 44 | ) 45 | 46 | submit = SubmitField('Add Object') 47 | 48 | 49 | class VariableCreateForm( FlaskForm ): 50 | 51 | name = StringField('Variable Name', 52 | validators=[ DataRequired() ], 53 | render_kw = { 54 | 'placeholder': "Variable Name" 55 | } 56 | ) 57 | 58 | var_type = HiddenField('Data Type',default='NoneType') 59 | 60 | writable = BooleanField('Writable',id="check_writable", render_kw = { 61 | 'placeholder': "Writable" 62 | } 63 | ) 64 | address = StringField('Address', validators=[ DataRequired() ], 65 | render_kw = { 'placeholder': "Address" 66 | } 67 | ) 68 | 69 | var_object = SelectField('Object',validators=[ DataRequired() ] ) 70 | 71 | value = StringField('Default Value', 72 | render_kw = { 73 | 'placeholder': "Value" 74 | } 75 | ) 76 | 77 | submit = SubmitField('Add Variable') 78 | -------------------------------------------------------------------------------- /build/lib/index/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from index import db 3 | import logging 4 | 5 | class Server(db.Model): 6 | id = db.Column(db.Integer, primary_key=True) 7 | server_name = db.Column( db.String(120), nullable=False ) 8 | server_endpoint_url = db.Column( db.String(120), unique=True, nullable=False ) 9 | server_namespace = db.Column( db.String(120), nullable=True, ) 10 | server_created_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow ) 11 | server_objects = db.relationship('Object',backref='server',cascade="all, delete-orphan" , lazy='dynamic') 12 | 13 | def __repr__( self ): 14 | return "Server: {}".format(self.server_name) 15 | 16 | class Object(db.Model): 17 | id = db.Column(db.Integer, primary_key=True) 18 | object_name = db.Column( db.String(120), nullable=False ) 19 | object_parent_id = db.Column( db.Integer, nullable=True ) 20 | object_server_id = db.Column(db.Integer,db.ForeignKey('server.id',ondelete='CASCADE'),nullable=False) 21 | object_variables = db.relationship("Variable", backref="object", cascade="all, delete-orphan" , lazy='dynamic' ) 22 | 23 | def has_child( self ): 24 | return True if Object.query.filter_by(object_parent_id=self.id) else False 25 | 26 | def is_parent( self ): 27 | return True if self.object_parent_id is None else False 28 | 29 | def get_parent( self ): 30 | return Object.query.get( self.object_parent_id ) 31 | 32 | def get_child_objects( self ): 33 | return self.query.filter_by(object_parent_id=self.id) if Object.query.filter_by(object_parent_id=self.id) else False 34 | 35 | def __repr__( self ): 36 | return "Object: {}".format(self.object_name) 37 | 38 | class Variable( db.Model ): 39 | id = db.Column( db.Integer, primary_key=True ) 40 | variable_name = db.Column( db.String(120), nullable=False ) 41 | variable_type = db.Column( db.String(20), nullable=True ) 42 | variable_writable = db.Column( db.Boolean(), nullable=False ) 43 | variable_address = db.Column( db.String(120), nullable=False) 44 | variable_value = db.Column( db.String(100), nullable=True) 45 | variable_object_id = db.Column(db.Integer,db.ForeignKey('object.id',ondelete='CASCADE'),nullable=False ) 46 | 47 | @staticmethod 48 | def validate( obj_id, address ): 49 | allvarrs = Object.query.get(obj_id).object_variables.all() 50 | for var in allvarrs: 51 | if var.variable_address == address : 52 | return False 53 | return True 54 | 55 | def __repr__( self ): 56 | return "Variable: {}".format(self.variable_name) 57 | -------------------------------------------------------------------------------- /build/lib/index/routes.py: -------------------------------------------------------------------------------- 1 | from flask import (render_template, flash, url_for, jsonify,request,redirect) 2 | from index import app,db 3 | from index.models import Server,Object, Variable 4 | from index.forms import ServerCreateForm,ObjectCreateForm,VariableCreateForm 5 | from . import utils 6 | from myserver import MyServer 7 | from myplc import MyPlc 8 | from snap7.snap7exceptions import Snap7Exception 9 | import time 10 | 11 | class Control(MyServer,MyPlc): 12 | def __init__(self): 13 | self.ip = None 14 | 15 | def get_db(self, server_id,ip=None): 16 | self.db_server = Server.query.get(server_id) 17 | if ip is not None: 18 | self.ip = ip 19 | self.inits() 20 | 21 | def inits(self): 22 | MyServer.__init__(self) 23 | MyPlc.__init__(self) 24 | 25 | def set_opc(self): 26 | self.instantiate_server_vars() 27 | 28 | def make_tags_dict(self,allvars): 29 | for var in allvars: 30 | self.varsdict[var.variable_address] = { 31 | 'obj' : self.opc_variables_dict[var.variable_address], 32 | 'type' : var.variable_type 33 | } 34 | 35 | # global ms,mp 36 | global ctrl 37 | ctrl = Control() 38 | 39 | @app.route("/") 40 | def home(): 41 | servers = Server.query.all() 42 | form = ServerCreateForm() 43 | return render_template('index.html',form=form,servers=servers) 44 | 45 | @app.route("/",methods= ['POST','GET'] ) 46 | def create_server(): 47 | form = ServerCreateForm() 48 | if form.validate_on_submit(): 49 | server = Server( server_name=form.server_name.data, 50 | server_endpoint_url=form.endpoint_url.data,server_namespace=form.namespace.data ) 51 | db.session.add(server) 52 | db.session.commit() 53 | resp = { 54 | 'message' : '{} Created Successfully'.format(form.server_name.data), 55 | 'servers' : Server.query.all() 56 | } 57 | return redirect(url_for('home')) 58 | return jsonify(data=form.errors) 59 | 60 | @app.route("/server//edit",methods= ['POST'] ) 61 | def edit_server( serverid ): 62 | form = ServerCreateForm() 63 | if form.validate_on_submit(): 64 | server = Server.query.get( serverid ) 65 | server.server_name = form.server_name.data 66 | server.server_endpoint_url = form.endpoint_url.data 67 | server.server_namespace = form.namespace.data 68 | db.session.add( server ) 69 | db.session.commit() 70 | flash('{} Edited Successfully'.format(server.server_name), 'success') 71 | return redirect(url_for('home')) 72 | flash('Could not edit Server' 'danger') 73 | return redirect(url_for('home')) 74 | 75 | @app.route("/server/delete/",methods= ['POST'] ) 76 | def delete_server( serverid ): 77 | server = Server.query.get( serverid ) 78 | servername = server.name 79 | db.session.delete(server) 80 | db.session.commit() 81 | flash('{} Deleted Successfully'.format(servername), 'success') 82 | return redirect(url_for('create_server')) 83 | 84 | 85 | @app.route("/server/",methods= ['GET'] ) 86 | def server_populate(serverid): 87 | server = Server.query.get( serverid ) 88 | objform = ObjectCreateForm() 89 | varform = VariableCreateForm() 90 | objects = server.server_objects 91 | varform.var_object.choices = utils.selectVals(objects) 92 | # objform.parent_object.choices = selectVals(objects) 93 | # vars = server.server_objects 94 | return render_template('server.html', 95 | objects=objects, 96 | server=server, 97 | objform = ObjectCreateForm(), 98 | varform=varform 99 | ) 100 | @app.route("/start_server/",methods=['POST']) 101 | def start_server(serverid): 102 | if request.method=='POST' and request.form: 103 | server = Server.query.get(request.form['server']) 104 | ctrl.get_db(server.id) 105 | try: 106 | ctrl.opc_server.start() 107 | except OSError as ipexp: 108 | return jsonify({ 'warning':'The endpoint: {} is currently being used'.format(server.server_endpoint_url) }) 109 | else: 110 | ctrl.connections() 111 | return jsonify({ 'success':'PLC Connected' }) 112 | 113 | return jsonify({ 'success':'Server running at {}'.format( server.server_endpoint_url ) }) 114 | 115 | else: 116 | return jsonify("Web Server Error") 117 | 118 | 119 | @app.route("/stop_server/",methods=['GET']) 120 | def stop_server(serverid): 121 | server = Server.query.get( serverid ) 122 | ctrl.kill_threads() 123 | ctrl.opc_server.stop() 124 | return jsonify({"info":"Server at {} Stopped".format(server.server_endpoint_url)}) 125 | 126 | 127 | 128 | @app.route("/create_object",methods= ['POST'] ) 129 | def create_object(): 130 | objform = ObjectCreateForm() 131 | 132 | serverobj = Server.query.get(objform.server.data) 133 | if request.method=='POST' and request.form: 134 | obj = Object( object_name = request.form['object_name'], 135 | object_parent_id = request.form['parent_object'] if request.form['parent_object'] else None, 136 | server = Server.query.get(request.form['server']) 137 | ) 138 | db.session.add(obj) 139 | db.session.commit() 140 | return redirect( url_for('server_populate',serverid=serverobj.id) ) 141 | else: 142 | flash('Could not create {} object'.format(objform.object_name)) 143 | return redirect(url_for('server_populate',serverid=serverobj.id)) 144 | 145 | 146 | @app.route("/create_variable,/",methods= ['POST'] ) 147 | def create_variable(server_id): 148 | varform = VariableCreateForm() 149 | # obj = Object.query.get(varform.var_object.data) 150 | if utils.custom_validation( varform.data ): 151 | if Variable.validate(varform.var_object.data,varform.address.data): 152 | try: 153 | var = Variable( variable_name=varform.name.data, variable_type=varform.var_type.data, 154 | variable_writable=varform.writable.data, variable_address=varform.address.data, 155 | variable_value=varform.value.data, object=Object.query.get(varform.var_object.data), 156 | ) 157 | except AttributeError as aexp: 158 | flash('Ensure all fields are filled','warning') 159 | return redirect( url_for('server_populate',serverid=server_id) ) 160 | else: 161 | flash('The address {} has already been taken'.format(varform.address.data),'warning') 162 | return redirect( url_for('server_populate',serverid=server_id) ) 163 | 164 | 165 | try: 166 | db.session.add(var) 167 | db.session.commit() 168 | except AttributeError as aexp: 169 | flash('Ensure all fields are filled','warning') 170 | return redirect( url_for('server_populate',serverid=server_id) ) 171 | except Exception as exp: 172 | flash('Could not save Variable'.format(varform.name.data)) 173 | else: 174 | flash('{} Created Successfully'.format(var.variable_name),'success') 175 | return redirect(url_for('server_populate',serverid=server_id)) 176 | 177 | else: 178 | flash( 'Could not create {} Variable'.format(varform.name.data), 'danger' ) 179 | return redirect(url_for('server_populate',serverid=server_id)) 180 | 181 | 182 | @app.route("/variables//delete",methods= ['GET'] ) 183 | def delete_var(var_id): 184 | var = Variable.query.get(var_id) 185 | db.session.delete(var) 186 | db.session.commit() 187 | return jsonify("Deleted Successfully") 188 | 189 | @app.route("/delete_object",methods= ['POST'] ) 190 | def delete_object(): 191 | obj = Object.query.get(request.form['object_id']) 192 | objName = obj.name 193 | server_id = request.form['server_id'] 194 | 195 | db.session.delete( obj ) 196 | db.session.commit() 197 | flash('{} Deleted SUccessfully'.format(objName), 'success') 198 | return redirect(url_for('server_populate',serverid=server_id)) 199 | -------------------------------------------------------------------------------- /build/lib/index/utils.py: -------------------------------------------------------------------------------- 1 | import socket 2 | types = { 3 | "int" 4 | } 5 | 6 | def custom_validation(form): 7 | status = True 8 | for k,v in form.items(): 9 | if v is '': 10 | status = False 11 | return status 12 | 13 | def Variable_Validation(form): 14 | status = True 15 | 16 | 17 | def selectVals(objs): 18 | idlist = [] 19 | namelist = [] 20 | if objs: 21 | for obj in objs: 22 | idlist.append(obj.id) 23 | namelist.append(obj.object_name) 24 | return [('','Select Object')]+list(zip( idlist, namelist )) 25 | else: 26 | return [ ('', 'No Objects Defined' ) ] 27 | 28 | def convert_val(val,vtype): 29 | if vtype is 'int': 30 | return int(val) 31 | elif vtype == 'float': 32 | return float(val) 33 | elif vtype == 'string': 34 | return str(val) 35 | elif vtype == 'bool': 36 | if val == True or val == 'true' or val == 1: 37 | return True 38 | else: 39 | return False 40 | else: 41 | return val 42 | 43 | 44 | 45 | def isOpen( ep_url ): 46 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 47 | try: 48 | s.connect( ( ep_url.split(':')[0], int( ep_url.split(':')[1] ) ) ) 49 | s.shutdown(1) 50 | return True 51 | except: 52 | return False 53 | -------------------------------------------------------------------------------- /build/lib/myplc/__init__.py: -------------------------------------------------------------------------------- 1 | from snap7.client import Client as PlcClient 2 | from opcua import Client 3 | from index.models import Server 4 | from snap7.snap7types import areas 5 | from snap7.util import * 6 | import time 7 | import threading 8 | from random import randint 9 | import socket 10 | 11 | class MyPlc: 12 | area={ 'I' : 0x81, 'Q' : 0x82, 'M' : 0x83, 'D': 0x84 } 13 | szs = { 'x':1, 'X': 1, 'b':1, 'B':1, 'w' : 2, 'W' : 2, 'd' : 4, 'D' : 4 } 14 | 15 | def __init__(self, ip='192.168.0.1'): 16 | self.ip = ip 17 | self.INByteArray = bytearray([ 0, 0 ]) 18 | self.MKByteArray = bytearray([ 0, 0 ]) 19 | self.threadStatus = False 20 | self.varsdict = {} 21 | self.threads={} 22 | self.plc = PlcClient() 23 | self.subNodes = [] 24 | self.subNodesD = {} 25 | self.keysDict = {} 26 | self.inNodes = {} 27 | 28 | 29 | 30 | def get_db(self,server_id): 31 | self.db_server = Server.query.get( server_id ) 32 | self.connections() 33 | 34 | 35 | def connections( self ): 36 | self.opc_ns_uri = self.db_server.server_namespace 37 | self.ep_url = 'opc.tcp://'+self.db_server.server_endpoint_url 38 | self.opclient = Client(self.ep_url) 39 | try: 40 | self.plc.connect( self.ip, 0, 1 ) 41 | # pass 42 | except Exception: 43 | self.conn_stat = "Could not connect to PLC" 44 | else: 45 | self.conn_stat = "PLC Connected Successfully" 46 | 47 | self.opclient.connect() 48 | self.root = self.opclient.get_root_node() 49 | self.idx = self.opclient.get_namespace_index(self.db_server.server_namespace) 50 | self.set_tags(self.db_server.server_objects) 51 | for key,val in self.varsdict.items(): 52 | if re.search("^(M|m)([\d]+).[\d]*$", key) is not None: 53 | self.subNodes.append( val['obj'] ) 54 | self.subNodesD[key] = val 55 | else: 56 | self.inNodes[key] = val 57 | self.run_threads() 58 | handler = SubHandler(self) 59 | sub = self.opclient.create_subscription(200, handler) 60 | handle = sub.subscribe_data_change(self.subNodes) 61 | time.sleep(0.1) 62 | 63 | def set_tags(self,objs): 64 | for obj in objs: 65 | try: 66 | self.make_tag_dict(obj, obj.object_variables) 67 | except Exception: 68 | self.make_tags_dict(obj.object_variables) 69 | finally: 70 | for var in obj.object_variables: 71 | self.keysDict[var.variable_name] = var.variable_address 72 | 73 | 74 | def make_tag_dict(self,obj,allvars): 75 | for var in allvars: 76 | self.varsdict[var.variable_address] = { 77 | 'obj' :self.root.get_child( [ "0:Objects", "{}:{}".format(self.idx,obj.object_name), "{}:{}".format(self.idx,var.variable_name) ] ), 78 | 'type' : var.variable_type 79 | } 80 | def kill_threads(self): 81 | self.threadStatus = False 82 | 83 | def run_threads(self): 84 | self.threadStatus = True 85 | self.threads['update_server'] = threading.Thread( target=self.updateInputs ) 86 | self.threads['update_server'].start() 87 | 88 | def getInputs( self ): 89 | while self.threadStatus: 90 | self.INByteArray = self.plc.read_area( areas['PE'], 0, 0, 2 ) 91 | # self.INByteArray = bytearray([ randint(0,7), randint(0,7) ]) 92 | time.sleep(.1) 93 | 94 | # def get_bool(_bytearray, byte_index, bool_index): 95 | def updateInputs(self): 96 | while self.threadStatus: 97 | for key,val in self.inNodes.items(): 98 | self.update_server_vars(key) 99 | time.sleep(.01) 100 | 101 | def writetoPLC(self,value,node): 102 | key = self.keysDict[node.get_browse_name().to_string().split(':')[1]] 103 | self.write_to_plc( key, value ) 104 | 105 | ''' 106 | Get Data from the PLC and Update OPC Server variables 107 | ''' 108 | def update_server_vars(self, addr_key ): 109 | addr = addr_key.split('.') 110 | # Works with Boolean values from a Data Block 111 | if len(addr) == 3 and addr[0][0] == 'D': 112 | DBn = int(addr[0][2:]) 113 | DBt = addr[1][2] 114 | byt = int( addr[1][3:] ) 115 | bit = int( addr[2] ) 116 | reading = self.plc.read_area( MyPlc.area['D'], DBn, byt, szs[DBt] ) 117 | if DBt == 'X' or DBt == 'x': 118 | self.varsdict[addr_key]['obj'].set_value( get_bool( reading, 0, bit ) ) 119 | # return get_bool( reading, 0, bit ) 120 | else: 121 | self.varsdict[addr_key]['obj'].set_value( reading ) 122 | # return reading 123 | 124 | # Works with other data types from a Data Block 125 | elif len(addr) == 2 and addr[0][0] == 'D': 126 | DBn = int(addr[0][2:]) 127 | DBt = addr[1][2] 128 | byt = int( addr[1][3:] ) 129 | reading = self.plc.read_area( MyPlc.area['D'], DBn, byt, szs[DBt] ) 130 | if DBt == 'W' or DBt == 'w': 131 | self.varsdict[addr_key]['obj'].set_value( get_int(reading,0) ) 132 | # return get_int(reading,0) 133 | elif DBt == 'D' or DBt == 'd': 134 | self.varsdict[addr_key]['obj'].set_value( get_real(reading,0) ) 135 | # return get_real(reading,0) 136 | else: 137 | self.varsdict[addr_key]['obj'].set_value( reading ) 138 | 139 | # Works with boolean values from Inputs,Merkels and Outputs 140 | elif len(addr) == 2 : 141 | byt = int( addr[0][1:] ) 142 | bit = int( addr[1] ) 143 | reading = self.plc.read_area( MyPlc.area[addr[0][0]], 0, byt, 1 ) 144 | self.varsdict[addr_key]['obj'].set_value( get_bool(reading,0,bit) ) 145 | # return get_bool(reading,0,bit) 146 | 147 | # Works with other data types from Inputs,Merkels ot Outputs eg MW2 148 | elif len(addr) == 1: 149 | byt = int( addr[0][2:] ) 150 | typ = addr[0][1] 151 | reading = self.plc.read_area( MyPlc.area[ addr[0][0] ], 0, byt, 2 ) 152 | if typ == 'w' or typ == 'W': 153 | self.varsdict[addr_key]['obj'].set_value( get_int(reading,0) ) 154 | # return get_int(reading, 0) 155 | elif typ == 'd' or typ == 'D': 156 | self.varsdict[addr_key]['obj'].set_value( get_real(reading,0) ) 157 | # return get_real(reading, 0) 158 | else: 159 | self.varsdict[addr_key]['obj'].set_value( reading ) 160 | # return reading 161 | 162 | ''' 163 | WRITE DATA TO PLC FROM SERVER 164 | ''' 165 | def write_to_plc(self, addr_key, value ): 166 | addr = addr_key.split('.') 167 | print( "New data change on {} : {}".format( addr_key , value) ) 168 | 169 | # Works with Boolean values from a Data Block 170 | if len(addr) == 3 and addr[0][0] == 'D': 171 | DBn = int(addr[0][2:]) 172 | DBt = addr[1][2] 173 | byt = int( addr[1][3:] ) 174 | bit = int( addr[2] ) 175 | reading = self.plc.read_area( MyPlc.area['D'], DBn, byt, MyPlc.szs[DBt] ) 176 | if DBt == 'X' or DBt == 'x': 177 | set_bool(reading, 0, bit, value) 178 | self.plc.write_area( MyPlc.area['D'], DBn, byt, reading ) 179 | 180 | # Works with other data types from a Data Block 181 | elif len(addr) == 2 and addr[0][0] == 'D': 182 | DBn = int(addr[0][2:]) 183 | DBt = addr[1][2] 184 | byt = int( addr[1][3:] ) 185 | reading = self.plc.read_area( MyPlc.area['D'], DBn, byt, MyPlc.szs[DBt] ) 186 | if DBt == 'W' or DBt == 'w': 187 | set_int(reading, 0, value) 188 | elif DBt == 'D' or DBt == 'd': 189 | set_real(reading, 0, value) 190 | self.plc.write_area( MyPlc.area['D'], DBn, byt, reading ) 191 | 192 | # Works with boolean values from Inputs,Merkels ot Outputs 193 | elif len(addr) == 2 : 194 | byt = int( addr[0][1:] ) 195 | bit = int( addr[1] ) 196 | reading = self.plc.read_area( MyPlc.area[addr[0][0]], 0, byt, 1 ) 197 | set_bool(reading, 0, bit, value) 198 | self.plc.write_area( MyPlc.area[addr[0][0]], 0, byt, reading ) 199 | 200 | # Works with other data types from Inputs,Merkels ot Outputs eg MW2 201 | elif len(addr) == 1: 202 | byt = int( addr[0][2:] ) 203 | typ = addr[0][1] 204 | reading = self.plc.read_area( MyPlc.area[ addr[0][0] ], 0, byt, 2 ) 205 | if typ == 'w' or typ == 'W': 206 | set_int(reading, 0, value) 207 | elif typ == 'd' or typ == 'D': 208 | set_real(reading, 0, value) 209 | else: 210 | set_data( value ) 211 | self.plc.write_area( MyPlc.area[addr[0][0]], 0, byt, reading ) 212 | 213 | 214 | class SubHandler(): 215 | def __init__(self,myplc): 216 | self.myplc = myplc 217 | 218 | def datachange_notification(self, node, val, data): 219 | self.myplc.threads['writetoplc'] = threading.Thread( target=self.myplc.writetoPLC, args=(val,node) ) 220 | self.myplc.threads['writetoplc'].start() 221 | 222 | def event_notification(self, event): 223 | print("Python: New event", event) 224 | 225 | -------------------------------------------------------------------------------- /build/lib/myserver/__init__.py: -------------------------------------------------------------------------------- 1 | from opcua import Server as UAServer 2 | from index.models import Server,Object,Variable 3 | from index.utils import convert_val 4 | 5 | class MyServer: 6 | ''' 7 | initialise opcua server object 8 | ''' 9 | def __init__( self ): 10 | self.opc_server = UAServer() # OPC UA server instance 11 | self.opc_objects_dict = {} 12 | self.opc_variables_dict = {} 13 | # self.db_server=Server.query.get( server_id ) 14 | # self.initialise() 15 | self.instantiate_server_vars() 16 | 17 | def get_db(self,server_id): 18 | self.db_server = Server.query.get( server_id ) 19 | self.instantiate_server_vars() 20 | 21 | ''' 22 | Instantiate all server related variables from the Sqlite DB server 23 | to the opcua server instance 24 | ''' 25 | def initialise( self ): 26 | self.instantiate_server_vars() 27 | 28 | 29 | def instantiate_server_vars( self ): 30 | self.opc_server_endpoint = "opc.tcp://"+self.db_server.server_endpoint_url 31 | self.opc_server_name = self.db_server.server_name 32 | self.opc_server_uri = self.db_server.server_namespace 33 | self.ns_idx = self.opc_server.register_namespace( self.opc_server_uri ) 34 | self.opc_objects = self.opc_server.get_objects_node() 35 | self.opc_server.set_endpoint(self.opc_server_endpoint) 36 | self.load_server( self.db_server.server_objects ) 37 | 38 | ''' 39 | load opc server with objects and variables from the SQlite DB 40 | ''' 41 | def load_server( self, db_objects, parent_obj=None ): 42 | for server_obj in db_objects: 43 | if( server_obj.id not in self.opc_objects_dict ): 44 | if parent_obj is None: 45 | self.opc_objects_dict[server_obj.id] = self.opc_objects.add_object( self.ns_idx, server_obj.object_name ) 46 | else: 47 | self.opc_objects_dict[server_obj.id] = parent_obj.add_object( self.ns_idx, server_obj.object_name ) 48 | 49 | self.load_object_variables(server_obj.object_variables, self.opc_objects_dict[server_obj.id]) 50 | 51 | if( server_obj.get_child_objects().count() > 0 ): 52 | self.load_server(server_obj.get_child_objects(),self.opc_objects_dict[server_obj.id] ) 53 | 54 | 55 | def load_object_variables(self, variables, object_owner): 56 | for variable in variables: 57 | self.opc_variables_dict[variable.variable_address] = object_owner.add_variable( self.ns_idx, variable.variable_name, convert_val(variable.variable_value,variable.variable_type ) ) 58 | if variable.variable_writable: 59 | self.opc_variables_dict[variable.variable_address].set_writable() 60 | 61 | def start_opc_server(self): 62 | self.opc_server.start() 63 | 64 | def stop_opc_server(self): 65 | self.opc_server.stop() 66 | 67 | @classmethod 68 | def kill_all_servers(cls): 69 | for addr in addrs: 70 | pass 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /dist/s71200opc-0.1.dev0-py3.7.egg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andykibz/opcserver/ec06c31726a68f766a030d71e583ee63ee0acc07/dist/s71200opc-0.1.dev0-py3.7.egg -------------------------------------------------------------------------------- /index/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from flask_sqlalchemy import SQLAlchemy 3 | 4 | 5 | app = Flask(__name__) 6 | app.config['SECRET_KEY'] = 'f1d9d48ec0e26e2a250839fa36ea2c602cc4f85ccfeb5c65' 7 | app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///site.db' 8 | from flask_wtf.csrf import CSRFProtect 9 | csrf = CSRFProtect(app) 10 | db = SQLAlchemy(app) 11 | 12 | from index import routes 13 | -------------------------------------------------------------------------------- /index/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andykibz/opcserver/ec06c31726a68f766a030d71e583ee63ee0acc07/index/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /index/__pycache__/forms.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andykibz/opcserver/ec06c31726a68f766a030d71e583ee63ee0acc07/index/__pycache__/forms.cpython-37.pyc -------------------------------------------------------------------------------- /index/__pycache__/models.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andykibz/opcserver/ec06c31726a68f766a030d71e583ee63ee0acc07/index/__pycache__/models.cpython-37.pyc -------------------------------------------------------------------------------- /index/__pycache__/routes.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andykibz/opcserver/ec06c31726a68f766a030d71e583ee63ee0acc07/index/__pycache__/routes.cpython-37.pyc -------------------------------------------------------------------------------- /index/__pycache__/utils.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andykibz/opcserver/ec06c31726a68f766a030d71e583ee63ee0acc07/index/__pycache__/utils.cpython-37.pyc -------------------------------------------------------------------------------- /index/forms.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm,Form 2 | from wtforms import StringField,SubmitField,SelectField,HiddenField,BooleanField 3 | from wtforms.validators import DataRequired 4 | from index.models import Server,Object 5 | 6 | 7 | class ServerCreateForm( FlaskForm ): 8 | 9 | server_name = StringField('Server', 10 | validators=[ DataRequired() ], 11 | render_kw = { 'placeholder': "Server Name" 12 | } 13 | ) 14 | 15 | endpoint_url = StringField('Endpoint Url', 16 | validators=[ DataRequired() ], 17 | render_kw = { 18 | 'placeholder': "Endpoint URL" 19 | } 20 | ) 21 | 22 | namespace = StringField('Namespace', 23 | render_kw = { 24 | 'placeholder': "Namespace" 25 | } 26 | ) 27 | 28 | submit = SubmitField('Create Server',id="create_server_button") 29 | 30 | class ObjectCreateForm( FlaskForm ): 31 | 32 | object_name = StringField('Object Name', 33 | validators=[ DataRequired() ], 34 | render_kw = { 35 | 'placeholder': "Object Name" 36 | } 37 | ) 38 | 39 | server = HiddenField('Server', 40 | validators=[ DataRequired() ], 41 | render_kw = { 42 | 'placeholder': 'Server' 43 | } 44 | ) 45 | 46 | submit = SubmitField('Add Object') 47 | 48 | 49 | class VariableCreateForm( FlaskForm ): 50 | 51 | name = StringField('Variable Name', 52 | validators=[ DataRequired() ], 53 | render_kw = { 54 | 'placeholder': "Variable Name" 55 | } 56 | ) 57 | 58 | var_type = HiddenField('Data Type',default='NoneType') 59 | 60 | writable = BooleanField('Writable',id="check_writable", render_kw = { 61 | 'placeholder': "Writable" 62 | } 63 | ) 64 | address = StringField('Address', validators=[ DataRequired() ], 65 | render_kw = { 'placeholder': "Address" 66 | } 67 | ) 68 | 69 | var_object = SelectField('Object',validators=[ DataRequired() ] ) 70 | 71 | value = StringField('Default Value', 72 | render_kw = { 73 | 'placeholder': "Value" 74 | } 75 | ) 76 | 77 | submit = SubmitField('Add Variable') 78 | -------------------------------------------------------------------------------- /index/models.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from index import db 3 | import logging 4 | 5 | class Server(db.Model): 6 | id = db.Column(db.Integer, primary_key=True) 7 | server_name = db.Column( db.String(120), nullable=False ) 8 | server_endpoint_url = db.Column( db.String(120), unique=True, nullable=False ) 9 | server_namespace = db.Column( db.String(120), nullable=True, ) 10 | server_created_date = db.Column(db.DateTime, nullable=False, default=datetime.utcnow ) 11 | server_objects = db.relationship('Object',backref='server',cascade="all, delete-orphan" , lazy='dynamic') 12 | 13 | def __repr__( self ): 14 | return "Server: {}".format(self.server_name) 15 | 16 | class Object(db.Model): 17 | id = db.Column(db.Integer, primary_key=True) 18 | object_name = db.Column( db.String(120), nullable=False ) 19 | object_parent_id = db.Column( db.Integer, nullable=True ) 20 | object_server_id = db.Column(db.Integer,db.ForeignKey('server.id',ondelete='CASCADE'),nullable=False) 21 | object_variables = db.relationship("Variable", backref="object", cascade="all, delete-orphan" , lazy='dynamic' ) 22 | 23 | def has_child( self ): 24 | return True if Object.query.filter_by(object_parent_id=self.id) else False 25 | 26 | def is_parent( self ): 27 | return True if self.object_parent_id is None else False 28 | 29 | def get_parent( self ): 30 | return Object.query.get( self.object_parent_id ) 31 | 32 | def get_child_objects( self ): 33 | return self.query.filter_by(object_parent_id=self.id) if Object.query.filter_by(object_parent_id=self.id) else False 34 | 35 | def __repr__( self ): 36 | return "Object: {}".format(self.object_name) 37 | 38 | class Variable( db.Model ): 39 | id = db.Column( db.Integer, primary_key=True ) 40 | variable_name = db.Column( db.String(120), nullable=False ) 41 | variable_type = db.Column( db.String(20), nullable=True ) 42 | variable_writable = db.Column( db.Boolean(), nullable=False ) 43 | variable_address = db.Column( db.String(120), nullable=False) 44 | variable_value = db.Column( db.String(100), nullable=True) 45 | variable_object_id = db.Column(db.Integer,db.ForeignKey('object.id',ondelete='CASCADE'),nullable=False ) 46 | 47 | @staticmethod 48 | def validate( obj_id, address ): 49 | allvarrs = Object.query.get(obj_id).object_variables.all() 50 | for var in allvarrs: 51 | if var.variable_address == address : 52 | return False 53 | return True 54 | 55 | def __repr__( self ): 56 | return "Variable: {}".format(self.variable_name) 57 | -------------------------------------------------------------------------------- /index/routes.py: -------------------------------------------------------------------------------- 1 | from flask import (render_template, flash, url_for, jsonify,request,redirect) 2 | from index import app,db 3 | from index.models import Server,Object, Variable 4 | from index.forms import ServerCreateForm,ObjectCreateForm,VariableCreateForm 5 | from . import utils 6 | from myserver import MyServer 7 | from myplc import MyPlc 8 | from snap7.snap7exceptions import Snap7Exception 9 | import time 10 | 11 | class Control(MyServer,MyPlc): 12 | def __init__(self): 13 | self.ip = None 14 | 15 | def get_db(self, server_id,ip=None): 16 | self.db_server = Server.query.get(server_id) 17 | if ip is not None: 18 | self.ip = ip 19 | self.inits() 20 | 21 | def inits(self): 22 | MyServer.__init__(self) 23 | MyPlc.__init__(self) 24 | 25 | def set_opc(self): 26 | self.instantiate_server_vars() 27 | 28 | def make_tags_dict(self,allvars): 29 | for var in allvars: 30 | self.varsdict[var.variable_address] = { 31 | 'obj' : self.opc_variables_dict[var.variable_address], 32 | 'type' : var.variable_type 33 | } 34 | 35 | # global ms,mp 36 | global ctrl 37 | ctrl = Control() 38 | 39 | @app.route("/") 40 | def home(): 41 | servers = Server.query.all() 42 | form = ServerCreateForm() 43 | return render_template('index.html',form=form,servers=servers) 44 | 45 | @app.route("/",methods= ['POST','GET'] ) 46 | def create_server(): 47 | form = ServerCreateForm() 48 | if form.validate_on_submit(): 49 | server = Server( server_name=form.server_name.data, 50 | server_endpoint_url=form.endpoint_url.data,server_namespace=form.namespace.data ) 51 | db.session.add(server) 52 | db.session.commit() 53 | resp = { 54 | 'message' : '{} Created Successfully'.format(form.server_name.data), 55 | 'servers' : Server.query.all() 56 | } 57 | return redirect(url_for('home')) 58 | return jsonify(data=form.errors) 59 | 60 | @app.route("/server//edit",methods= ['POST'] ) 61 | def edit_server( serverid ): 62 | form = ServerCreateForm() 63 | if form.validate_on_submit(): 64 | server = Server.query.get( serverid ) 65 | server.server_name = form.server_name.data 66 | server.server_endpoint_url = form.endpoint_url.data 67 | server.server_namespace = form.namespace.data 68 | db.session.add( server ) 69 | db.session.commit() 70 | flash('{} Edited Successfully'.format(server.server_name), 'success') 71 | return redirect(url_for('home')) 72 | flash('Could not edit Server' 'danger') 73 | return redirect(url_for('home')) 74 | 75 | @app.route("/server/delete/",methods= ['POST'] ) 76 | def delete_server( serverid ): 77 | server = Server.query.get( serverid ) 78 | servername = server.name 79 | db.session.delete(server) 80 | db.session.commit() 81 | flash('{} Deleted Successfully'.format(servername), 'success') 82 | return redirect(url_for('create_server')) 83 | 84 | 85 | @app.route("/server/",methods= ['GET'] ) 86 | def server_populate(serverid): 87 | server = Server.query.get( serverid ) 88 | objform = ObjectCreateForm() 89 | varform = VariableCreateForm() 90 | objects = server.server_objects 91 | varform.var_object.choices = utils.selectVals(objects) 92 | # objform.parent_object.choices = selectVals(objects) 93 | # vars = server.server_objects 94 | return render_template('server.html', 95 | objects=objects, 96 | server=server, 97 | objform = ObjectCreateForm(), 98 | varform=varform 99 | ) 100 | @app.route("/start_server/",methods=['POST']) 101 | def start_server(serverid): 102 | if request.method=='POST' and request.form: 103 | server = Server.query.get(request.form['server']) 104 | ctrl.get_db(server.id) 105 | try: 106 | ctrl.opc_server.start() 107 | except OSError as ipexp: 108 | return jsonify({ 'warning':'The endpoint: {} is currently being used'.format(server.server_endpoint_url) }) 109 | else: 110 | ctrl.connections() 111 | return jsonify({ 'success':'PLC Connected' }) 112 | 113 | return jsonify({ 'success':'Server running at {}'.format( server.server_endpoint_url ) }) 114 | 115 | else: 116 | return jsonify("Web Server Error") 117 | 118 | 119 | @app.route("/stop_server/",methods=['GET']) 120 | def stop_server(serverid): 121 | server = Server.query.get( serverid ) 122 | ctrl.kill_threads() 123 | ctrl.opc_server.stop() 124 | return jsonify({"info":"Server at {} Stopped".format(server.server_endpoint_url)}) 125 | 126 | 127 | 128 | @app.route("/create_object",methods= ['POST'] ) 129 | def create_object(): 130 | objform = ObjectCreateForm() 131 | 132 | serverobj = Server.query.get(objform.server.data) 133 | if request.method=='POST' and request.form: 134 | obj = Object( object_name = request.form['object_name'], 135 | object_parent_id = request.form['parent_object'] if request.form['parent_object'] else None, 136 | server = Server.query.get(request.form['server']) 137 | ) 138 | db.session.add(obj) 139 | db.session.commit() 140 | return redirect( url_for('server_populate',serverid=serverobj.id) ) 141 | else: 142 | flash('Could not create {} object'.format(objform.object_name)) 143 | return redirect(url_for('server_populate',serverid=serverobj.id)) 144 | 145 | 146 | @app.route("/create_variable,/",methods= ['POST'] ) 147 | def create_variable(server_id): 148 | varform = VariableCreateForm() 149 | # obj = Object.query.get(varform.var_object.data) 150 | if utils.custom_validation( varform.data ): 151 | if Variable.validate(varform.var_object.data,varform.address.data): 152 | try: 153 | var = Variable( variable_name=varform.name.data, variable_type=varform.var_type.data, 154 | variable_writable=varform.writable.data, variable_address=varform.address.data, 155 | variable_value=varform.value.data, object=Object.query.get(varform.var_object.data), 156 | ) 157 | except AttributeError as aexp: 158 | flash('Ensure all fields are filled','warning') 159 | return redirect( url_for('server_populate',serverid=server_id) ) 160 | else: 161 | flash('The address {} has already been taken'.format(varform.address.data),'warning') 162 | return redirect( url_for('server_populate',serverid=server_id) ) 163 | 164 | 165 | try: 166 | db.session.add(var) 167 | db.session.commit() 168 | except AttributeError as aexp: 169 | flash('Ensure all fields are filled','warning') 170 | return redirect( url_for('server_populate',serverid=server_id) ) 171 | except Exception as exp: 172 | flash('Could not save Variable'.format(varform.name.data)) 173 | else: 174 | flash('{} Created Successfully'.format(var.variable_name),'success') 175 | return redirect(url_for('server_populate',serverid=server_id)) 176 | 177 | else: 178 | flash( 'Could not create {} Variable'.format(varform.name.data), 'danger' ) 179 | return redirect(url_for('server_populate',serverid=server_id)) 180 | 181 | 182 | @app.route("/variables//delete",methods= ['GET'] ) 183 | def delete_var(var_id): 184 | var = Variable.query.get(var_id) 185 | db.session.delete(var) 186 | db.session.commit() 187 | return jsonify("Deleted Successfully") 188 | 189 | @app.route("/delete_object",methods= ['POST'] ) 190 | def delete_object(): 191 | obj = Object.query.get(request.form['object_id']) 192 | objName = obj.name 193 | server_id = request.form['server_id'] 194 | 195 | db.session.delete( obj ) 196 | db.session.commit() 197 | flash('{} Deleted SUccessfully'.format(objName), 'success') 198 | return redirect(url_for('server_populate',serverid=server_id)) 199 | -------------------------------------------------------------------------------- /index/site.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andykibz/opcserver/ec06c31726a68f766a030d71e583ee63ee0acc07/index/site.db -------------------------------------------------------------------------------- /index/static/True.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andykibz/opcserver/ec06c31726a68f766a030d71e583ee63ee0acc07/index/static/True.png -------------------------------------------------------------------------------- /index/static/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.3.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t=t||self).bootstrap={},t.jQuery,t.Popper)}(this,function(t,g,u){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Ee},je="show",He="out",Re={HIDE:"hide"+De,HIDDEN:"hidden"+De,SHOW:"show"+De,SHOWN:"shown"+De,INSERTED:"inserted"+De,CLICK:"click"+De,FOCUSIN:"focusin"+De,FOCUSOUT:"focusout"+De,MOUSEENTER:"mouseenter"+De,MOUSELEAVE:"mouseleave"+De},xe="fade",Fe="show",Ue=".tooltip-inner",We=".arrow",qe="hover",Me="focus",Ke="click",Qe="manual",Be=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Fe))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(xe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,{placement:a,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:We},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),g(o).addClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===He&&e._leave(null,e)};if(g(this.tip).hasClass(xe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=g.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==je&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),g(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(g(this.element).trigger(i),!i.isDefaultPrevented()){if(g(n).removeClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ke]=!1,this._activeTrigger[Me]=!1,this._activeTrigger[qe]=!1,g(this.tip).hasClass(xe)){var r=_.getTransitionDurationFromElement(n);g(n).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Ae+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ue)),this.getTitle()),g(t).removeClass(xe+" "+Fe)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=Se(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Pe[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Qe){var e=t===qe?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===qe?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),g(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Me:qe]=!0),g(e.getTipElement()).hasClass(Fe)||e._hoverState===je?e._hoverState=je:(clearTimeout(e._timeout),e._hoverState=je,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===je&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Me:qe]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=He,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===He&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==Oe.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(be,t,this.constructor.DefaultType),t.sanitize&&(t.template=Se(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ne);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(xe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ie),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ie,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return Le}},{key:"NAME",get:function(){return be}},{key:"DATA_KEY",get:function(){return Ie}},{key:"Event",get:function(){return Re}},{key:"EVENT_KEY",get:function(){return De}},{key:"DefaultType",get:function(){return ke}}]),i}();g.fn[be]=Be._jQueryInterface,g.fn[be].Constructor=Be,g.fn[be].noConflict=function(){return g.fn[be]=we,Be._jQueryInterface};var Ve="popover",Ye="bs.popover",ze="."+Ye,Xe=g.fn[Ve],$e="bs-popover",Ge=new RegExp("(^|\\s)"+$e+"\\S+","g"),Je=l({},Be.Default,{placement:"right",trigger:"click",content:"",template:''}),Ze=l({},Be.DefaultType,{content:"(string|element|function)"}),tn="fade",en="show",nn=".popover-header",on=".popover-body",rn={HIDE:"hide"+ze,HIDDEN:"hidden"+ze,SHOW:"show"+ze,SHOWN:"shown"+ze,INSERTED:"inserted"+ze,CLICK:"click"+ze,FOCUSIN:"focusin"+ze,FOCUSOUT:"focusout"+ze,MOUSEENTER:"mouseenter"+ze,MOUSELEAVE:"mouseleave"+ze},sn=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){g(this.getTipElement()).addClass($e+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},o.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(nn),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(on),e),t.removeClass(tn+" "+en)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ge);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t0?l:i)(e)}},function(e,n,i){var l; 2 | /*! 3 | Copyright (c) 2016 Jed Watson. 4 | Licensed under the MIT License (MIT), see 5 | http://jedwatson.github.io/classnames 6 | */ 7 | /*! 8 | Copyright (c) 2016 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | !function(){"use strict";var i=function(){function e(){}function n(e,n){for(var i=n.length,l=0;l0?t(l(e),9007199254740991):0}},function(e,n){var i={}.toString;e.exports=function(e){return i.call(e).slice(8,-1)}},function(e,n,i){var l=i(48),t=i(14);e.exports=function(e){return l(t(e))}},function(e,n,i){var l=i(54);e.exports=function(e,n,i){if(l(e),void 0===n)return e;switch(i){case 1:return function(i){return e.call(n,i)};case 2:return function(i,l){return e.call(n,i,l)};case 3:return function(i,l,t){return e.call(n,i,l,t)}}return function(){return e.apply(n,arguments)}}},function(e,n,i){var l=i(1),t=i(7),r=i(3),o=i(11)("src"),a=Function.toString,c=(""+a).split("toString");i(2).inspectSource=function(e){return a.call(e)},(e.exports=function(e,n,i,a){var y="function"==typeof i;y&&(r(i,"name")||t(i,"name",n)),e[n]!==i&&(y&&(r(i,o)||t(i,o,e[n]?""+e[n]:c.join(String(n)))),e===l?e[n]=i:a?e[n]?e[n]=i:t(e,n,i):(delete e[n],t(e,n,i)))})(Function.prototype,"toString",function(){return"function"==typeof this&&this[o]||a.call(this)})},function(e,n,i){var l=i(13),t=i(1).document,r=l(t)&&l(t.createElement);e.exports=function(e){return r?t.createElement(e):{}}},function(e,n){e.exports=function(e){try{return!!e()}catch(e){return!0}}},function(e,n,i){var l=i(1),t=i(2),r=i(7),o=i(25),a=i(24),c=function(e,n,i){var y,p,h,x,s=e&c.F,u=e&c.G,d=e&c.S,f=e&c.P,v=e&c.B,g=u?l:d?l[n]||(l[n]={}):(l[n]||{}).prototype,m=u?t:t[n]||(t[n]={}),M=m.prototype||(m.prototype={});for(y in u&&(i=n),i)h=((p=!s&&g&&void 0!==g[y])?g:i)[y],x=v&&p?a(h,l):f&&"function"==typeof h?a(Function.call,h):h,g&&o(g,y,h,e&c.U),m[y]!=h&&r(m,y,x),f&&M[y]!=h&&(M[y]=h)};l.core=t,c.F=1,c.G=2,c.S=4,c.P=8,c.B=16,c.W=32,c.U=64,c.R=128,e.exports=c},function(e,n){e.exports=!1},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var l=Object.assign||function(e){for(var n=1;n0&&void 0!==arguments[0]?arguments[0]:{};if("undefined"==typeof document)throw new Error("`feather.replace()` only works in a browser environment.");var n=document.querySelectorAll("[data-feather]");Array.from(n).forEach(function(n){return function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},i=function(e){return Array.from(e.attributes).reduce(function(e,n){return e[n.name]=n.value,e},{})}(e),o=i["data-feather"];delete i["data-feather"];var a=r.default[o].toSvg(l({},n,i,{class:(0,t.default)(n.class,i.class)})),c=(new DOMParser).parseFromString(a,"image/svg+xml").querySelector("svg");e.parentNode.replaceChild(c,e)}(n,e)})}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var l,t=i(8),r=(l=t)&&l.__esModule?l:{default:l};n.default=function(e){var n=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};if(console.warn("feather.toSvg() is deprecated. Please use feather.icons[name].toSvg() instead."),!e)throw new Error("The required `key` (icon name) parameter is missing.");if(!r.default[e])throw new Error("No icon matching '"+e+"'. See the complete list of icons at https://feathericons.com");return r.default[e].toSvg(n)}},function(e){e.exports={activity:["pulse","health","action","motion"],airplay:["stream","cast","mirroring"],"alert-circle":["warning"],"alert-octagon":["warning"],"alert-triangle":["warning"],"at-sign":["mention"],award:["achievement","badge"],aperture:["camera","photo"],bell:["alarm","notification"],"bell-off":["alarm","notification","silent"],bluetooth:["wireless"],"book-open":["read"],book:["read","dictionary","booklet","magazine"],bookmark:["read","clip","marker","tag"],briefcase:["work","bag","baggage","folder"],clipboard:["copy"],clock:["time","watch","alarm"],"cloud-drizzle":["weather","shower"],"cloud-lightning":["weather","bolt"],"cloud-rain":["weather"],"cloud-snow":["weather","blizzard"],cloud:["weather"],codepen:["logo"],coffee:["drink","cup","mug","tea","cafe","hot","beverage"],command:["keyboard","cmd"],compass:["navigation","safari","travel"],copy:["clone","duplicate"],"corner-down-left":["arrow"],"corner-down-right":["arrow"],"corner-left-down":["arrow"],"corner-left-up":["arrow"],"corner-right-down":["arrow"],"corner-right-up":["arrow"],"corner-up-left":["arrow"],"corner-up-right":["arrow"],"credit-card":["purchase","payment","cc"],crop:["photo","image"],crosshair:["aim","target"],database:["storage"],delete:["remove"],disc:["album","cd","dvd","music"],"dollar-sign":["currency","money","payment"],droplet:["water"],edit:["pencil","change"],"edit-2":["pencil","change"],"edit-3":["pencil","change"],eye:["view","watch"],"eye-off":["view","watch"],"external-link":["outbound"],facebook:["logo"],"fast-forward":["music"],film:["movie","video"],"folder-minus":["directory"],"folder-plus":["directory"],folder:["directory"],frown:["emoji","face","bad","sad","emotion"],gift:["present","box","birthday","party"],"git-branch":["code","version control"],"git-commit":["code","version control"],"git-merge":["code","version control"],"git-pull-request":["code","version control"],github:["logo","version control"],gitlab:["logo","version control"],global:["world","browser","language","translate"],"hard-drive":["computer","server"],hash:["hashtag","number","pound"],headphones:["music","audio"],heart:["like","love"],"help-circle":["question mark"],home:["house"],image:["picture"],inbox:["email"],instagram:["logo","camera"],key:["password","login","authentication"],"life-bouy":["help","life ring","support"],linkedin:["logo"],lock:["security","password"],"log-in":["sign in","arrow"],"log-out":["sign out","arrow"],mail:["email"],"map-pin":["location","navigation","travel","marker"],map:["location","navigation","travel"],maximize:["fullscreen"],"maximize-2":["fullscreen","arrows"],meh:["emoji","face","neutral","emotion"],menu:["bars","navigation","hamburger"],"message-circle":["comment","chat"],"message-square":["comment","chat"],"mic-off":["record"],mic:["record"],minimize:["exit fullscreen"],"minimize-2":["exit fullscreen","arrows"],monitor:["tv"],moon:["dark","night"],"more-horizontal":["ellipsis"],"more-vertical":["ellipsis"],"mouse-pointer":["arrow","cursor"],move:["arrows"],navigation:["location","travel"],"navigation-2":["location","travel"],octagon:["stop"],package:["box"],paperclip:["attachment"],pause:["music","stop"],"pause-circle":["music","stop"],"pen-tool":["vector","drawing"],play:["music","start"],"play-circle":["music","start"],plus:["add","new"],"plus-circle":["add","new"],"plus-square":["add","new"],pocket:["logo","save"],power:["on","off"],radio:["signal"],rewind:["music"],rss:["feed","subscribe"],save:["floppy disk"],search:["find","magnifier","magnifying glass"],send:["message","mail","paper airplane"],settings:["cog","edit","gear","preferences"],shield:["security"],"shield-off":["security"],"shopping-bag":["ecommerce","cart","purchase","store"],"shopping-cart":["ecommerce","cart","purchase","store"],shuffle:["music"],"skip-back":["music"],"skip-forward":["music"],slash:["ban","no"],sliders:["settings","controls"],smile:["emoji","face","happy","good","emotion"],speaker:["music"],star:["bookmark","favorite","like"],sun:["brightness","weather","light"],sunrise:["weather"],sunset:["weather"],tag:["label"],target:["bullseye"],terminal:["code","command line"],"thumbs-down":["dislike","bad"],"thumbs-up":["like","good"],"toggle-left":["on","off","switch"],"toggle-right":["on","off","switch"],trash:["garbage","delete","remove"],"trash-2":["garbage","delete","remove"],triangle:["delta"],truck:["delivery","van","shipping"],twitter:["logo"],umbrella:["rain","weather"],"video-off":["camera","movie","film"],video:["camera","movie","film"],voicemail:["phone"],volume:["music","sound","mute"],"volume-1":["music","sound"],"volume-2":["music","sound"],"volume-x":["music","sound","mute"],watch:["clock","time"],wind:["weather","air"],"x-circle":["cancel","close","delete","remove","times"],"x-square":["cancel","close","delete","remove","times"],x:["cancel","close","delete","remove","times"],youtube:["logo","video","play"],"zap-off":["flash","camera","lightning"],zap:["flash","camera","lightning"]}},function(e){e.exports={activity:'',airplay:'',"alert-circle":'',"alert-octagon":'',"alert-triangle":'',"align-center":'',"align-justify":'',"align-left":'',"align-right":'',anchor:'',aperture:'',archive:'',"arrow-down-circle":'',"arrow-down-left":'',"arrow-down-right":'',"arrow-down":'',"arrow-left-circle":'',"arrow-left":'',"arrow-right-circle":'',"arrow-right":'',"arrow-up-circle":'',"arrow-up-left":'',"arrow-up-right":'',"arrow-up":'',"at-sign":'',award:'',"bar-chart-2":'',"bar-chart":'',"battery-charging":'',battery:'',"bell-off":'',bell:'',bluetooth:'',bold:'',"book-open":'',book:'',bookmark:'',box:'',briefcase:'',calendar:'',"camera-off":'',camera:'',cast:'',"check-circle":'',"check-square":'',check:'',"chevron-down":'',"chevron-left":'',"chevron-right":'',"chevron-up":'',"chevrons-down":'',"chevrons-left":'',"chevrons-right":'',"chevrons-up":'',chrome:'',circle:'',clipboard:'',clock:'',"cloud-drizzle":'',"cloud-lightning":'',"cloud-off":'',"cloud-rain":'',"cloud-snow":'',cloud:'',code:'',codepen:'',coffee:'',command:'',compass:'',copy:'',"corner-down-left":'',"corner-down-right":'',"corner-left-down":'',"corner-left-up":'',"corner-right-down":'',"corner-right-up":'',"corner-up-left":'',"corner-up-right":'',cpu:'',"credit-card":'',crop:'',crosshair:'',database:'',delete:'',disc:'',"dollar-sign":'',"download-cloud":'',download:'',droplet:'',"edit-2":'',"edit-3":'',edit:'',"external-link":'',"eye-off":'',eye:'',facebook:'',"fast-forward":'',feather:'',"file-minus":'',"file-plus":'',"file-text":'',file:'',film:'',filter:'',flag:'',"folder-minus":'',"folder-plus":'',folder:'',frown:'',gift:'',"git-branch":'',"git-commit":'',"git-merge":'',"git-pull-request":'',github:'',gitlab:'',globe:'',grid:'',"hard-drive":'',hash:'',headphones:'',heart:'',"help-circle":'',home:'',image:'',inbox:'',info:'',instagram:'',italic:'',key:'',layers:'',layout:'',"life-buoy":'',"link-2":'',link:'',linkedin:'',list:'',loader:'',lock:'',"log-in":'',"log-out":'',mail:'',"map-pin":'',map:'',"maximize-2":'',maximize:'',meh:'',menu:'',"message-circle":'',"message-square":'',"mic-off":'',mic:'',"minimize-2":'',minimize:'',"minus-circle":'',"minus-square":'',minus:'',monitor:'',moon:'',"more-horizontal":'',"more-vertical":'',"mouse-pointer":'',move:'',music:'',"navigation-2":'',navigation:'',octagon:'',package:'',paperclip:'',"pause-circle":'',pause:'',"pen-tool":'',percent:'',"phone-call":'',"phone-forwarded":'',"phone-incoming":'',"phone-missed":'',"phone-off":'',"phone-outgoing":'',phone:'',"pie-chart":'',"play-circle":'',play:'',"plus-circle":'',"plus-square":'',plus:'',pocket:'',power:'',printer:'',radio:'',"refresh-ccw":'',"refresh-cw":'',repeat:'',rewind:'',"rotate-ccw":'',"rotate-cw":'',rss:'',save:'',scissors:'',search:'',send:'',server:'',settings:'',"share-2":'',share:'',"shield-off":'',shield:'',"shopping-bag":'',"shopping-cart":'',shuffle:'',sidebar:'',"skip-back":'',"skip-forward":'',slack:'',slash:'',sliders:'',smartphone:'',smile:'',speaker:'',square:'',star:'',"stop-circle":'',sun:'',sunrise:'',sunset:'',tablet:'',tag:'',target:'',terminal:'',thermometer:'',"thumbs-down":'',"thumbs-up":'',"toggle-left":'',"toggle-right":'',"trash-2":'',trash:'',trello:'',"trending-down":'',"trending-up":'',triangle:'',truck:'',tv:'',twitter:'',type:'',umbrella:'',underline:'',unlock:'',"upload-cloud":'',upload:'',"user-check":'',"user-minus":'',"user-plus":'',"user-x":'',user:'',users:'',"video-off":'',video:'',voicemail:'',"volume-1":'',"volume-2":'',"volume-x":'',volume:'',watch:'',"wifi-off":'',wifi:'',wind:'',"x-circle":'',"x-square":'',x:'',youtube:'',"zap-off":'',zap:'',"zoom-in":'',"zoom-out":''}},function(e){e.exports={xmlns:"http://www.w3.org/2000/svg",width:24,height:24,viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":2,"stroke-linecap":"round","stroke-linejoin":"round"}},function(e,n,i){"use strict";Object.defineProperty(n,"__esModule",{value:!0});var l=Object.assign||function(e){for(var n=1;n2&&void 0!==arguments[2]?arguments[2]:[];!function(e,n){if(!(e instanceof n))throw new TypeError("Cannot call a class as a function")}(this,e),this.name=n,this.contents=i,this.tags=t,this.attrs=l({},o.default,{class:"feather feather-"+n})}return t(e,[{key:"toSvg",value:function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return""+this.contents+""}},{key:"toString",value:function(){return this.contents}}]),e}();n.default=c},function(e,n,i){"use strict";var l=o(i(8)),t=o(i(31)),r=o(i(30));function o(e){return e&&e.__esModule?e:{default:e}}e.exports={icons:l.default,toSvg:t.default,replace:r.default}},function(e,n,i){var l=i(0)("iterator"),t=!1;try{var r=[7][l]();r.return=function(){t=!0},Array.from(r,function(){throw 2})}catch(e){}e.exports=function(e,n){if(!n&&!t)return!1;var i=!1;try{var r=[7],o=r[l]();o.next=function(){return{done:i=!0}},r[l]=function(){return o},e(r)}catch(e){}return i}},function(e,n,i){var l=i(22),t=i(0)("toStringTag"),r="Arguments"==l(function(){return arguments}());e.exports=function(e){var n,i,o;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(i=function(e,n){try{return e[n]}catch(e){}}(n=Object(e),t))?i:r?l(n):"Object"==(o=l(n))&&"function"==typeof n.callee?"Arguments":o}},function(e,n,i){var l=i(38),t=i(0)("iterator"),r=i(10);e.exports=i(2).getIteratorMethod=function(e){if(void 0!=e)return e[t]||e["@@iterator"]||r[l(e)]}},function(e,n,i){"use strict";var l=i(6),t=i(12);e.exports=function(e,n,i){n in e?l.f(e,n,t(0,i)):e[n]=i}},function(e,n,i){var l=i(10),t=i(0)("iterator"),r=Array.prototype;e.exports=function(e){return void 0!==e&&(l.Array===e||r[t]===e)}},function(e,n,i){var l=i(5);e.exports=function(e,n,i,t){try{return t?n(l(i)[0],i[1]):n(i)}catch(n){var r=e.return;throw void 0!==r&&l(r.call(e)),n}}},function(e,n,i){"use strict";var l=i(24),t=i(28),r=i(17),o=i(42),a=i(41),c=i(21),y=i(40),p=i(39);t(t.S+t.F*!i(37)(function(e){Array.from(e)}),"Array",{from:function(e){var n,i,t,h,x=r(e),s="function"==typeof this?this:Array,u=arguments.length,d=u>1?arguments[1]:void 0,f=void 0!==d,v=0,g=p(x);if(f&&(d=l(d,u>2?arguments[2]:void 0,2)),void 0==g||s==Array&&a(g))for(i=new s(n=c(x.length));n>v;v++)y(i,v,f?d(x[v],v):x[v]);else for(h=g.call(x),i=new s;!(t=h.next()).done;v++)y(i,v,f?o(h,d,[t.value,v],!0):t.value);return i.length=v,i}})},function(e,n,i){var l=i(3),t=i(17),r=i(9)("IE_PROTO"),o=Object.prototype;e.exports=Object.getPrototypeOf||function(e){return e=t(e),l(e,r)?e[r]:"function"==typeof e.constructor&&e instanceof e.constructor?e.constructor.prototype:e instanceof Object?o:null}},function(e,n,i){var l=i(1).document;e.exports=l&&l.documentElement},function(e,n,i){var l=i(15),t=Math.max,r=Math.min;e.exports=function(e,n){return(e=l(e))<0?t(e+n,0):r(e,n)}},function(e,n,i){var l=i(23),t=i(21),r=i(46);e.exports=function(e){return function(n,i,o){var a,c=l(n),y=t(c.length),p=r(o,y);if(e&&i!=i){for(;y>p;)if((a=c[p++])!=a)return!0}else for(;y>p;p++)if((e||p in c)&&c[p]===i)return e||p||0;return!e&&-1}}},function(e,n,i){var l=i(22);e.exports=Object("z").propertyIsEnumerable(0)?Object:function(e){return"String"==l(e)?e.split(""):Object(e)}},function(e,n,i){var l=i(3),t=i(23),r=i(47)(!1),o=i(9)("IE_PROTO");e.exports=function(e,n){var i,a=t(e),c=0,y=[];for(i in a)i!=o&&l(a,i)&&y.push(i);for(;n.length>c;)l(a,i=n[c++])&&(~r(y,i)||y.push(i));return y}},function(e,n,i){var l=i(49),t=i(19);e.exports=Object.keys||function(e){return l(e,t)}},function(e,n,i){var l=i(6),t=i(5),r=i(50);e.exports=i(4)?Object.defineProperties:function(e,n){t(e);for(var i,o=r(n),a=o.length,c=0;a>c;)l.f(e,i=o[c++],n[i]);return e}},function(e,n,i){var l=i(5),t=i(51),r=i(19),o=i(9)("IE_PROTO"),a=function(){},c=function(){var e,n=i(26)("iframe"),l=r.length;for(n.style.display="none",i(45).appendChild(n),n.src="javascript:",(e=n.contentWindow.document).open(),e.write(" 20 | 21 | 22 | 23 | 24 | 27 | {% endblock %} 28 | 29 | 30 | -------------------------------------------------------------------------------- /index/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 |
4 | {% with messages = get_flashed_messages(with_categories=true) %} 5 | {% if messages %} 6 | {% for category, message in messages %} 7 |
8 | {{ message }} 9 |
10 | {% endfor %} 11 | {% endif %} 12 | {% endwith %} 13 |
14 | 15 | 16 | 54 | 55 | 56 | 86 | 87 |
88 | {{ form.hidden_tag() }} 89 |
90 |
91 | {{ form.server_name.label( class="form-control-label" ) }} 92 | {{ form.server_name( class="form-control", value="" ) }} 93 |
94 | 95 |
96 | {{ form.endpoint_url.label( class="form-control-label" ) }} 97 |
98 |
99 | ocp.tcp:// 100 | {{ form.endpoint_url(class="form-control", value="") }} 101 |
102 |
103 |
104 | 105 |
106 | {{ form.namespace.label( class="form-control-label" ) }} 107 | {{ form.namespace( class="form-control",value="" ) }} 108 |
109 |
110 | 111 |
112 | {{ form.submit( class="form-control btn btn-outline-info col-6 offset-3" ) }} 113 |
114 |
115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | {% if servers %} 127 | {% for server in servers %} 128 | 129 | 130 | 131 | 132 | 133 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | {% endfor %} 150 | {% else %} 151 | 152 | {% endif %} 153 | 154 |
#Server NameEndpont UrlNamespaceAction
{{ server.id }}{{ server.server_name }}{{ server.server_endpoint_url }}{{ server.server_namespace }} 134 |

135 | 138 | 139 | 140 | 141 |

142 |
No Servers Defined
155 | {% endblock %} 156 | {% block javascripts %} 157 | {{ super() }} 158 | 174 | {% endblock %} 175 | 176 | 177 | -------------------------------------------------------------------------------- /index/templates/partials/nav.html: -------------------------------------------------------------------------------- 1 | 21 | -------------------------------------------------------------------------------- /index/templates/partials/object_creation.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

5 | Objects 6 |

7 |
8 |
9 |
10 | {% if objects %} 11 | {% for object in objects %} 12 |
13 | {% if object.is_parent() %} 14 | 15 | 16 |
17 | 18 | 19 | 20 |
21 | {{ object.object_name }} 22 | 23 | Delete 24 | 25 |
26 | 27 | {% for child_obj in object.get_child_objects() %} 28 | 29 |
30 | 31 | 32 | 33 |
34 | {{ child_obj.object_name }} 35 | 36 | Delete 37 | 38 |
39 | {% endfor %} 40 | 41 |
42 | {% endif %} 43 |
44 | {% endfor %} 45 | {% else %} 46 | 47 | No Objects Defined Yet 48 | 49 | 50 | {% endif %} 51 |
52 |
53 |
54 |
55 | {{ objform.csrf_token }} 56 | 57 |
58 | {{ objform.object_name.label( class="form-control-label" ) }} 59 | {{ objform.object_name( class="form-control", value="" ) }} 60 |
61 | 62 |
63 | 64 | 65 | 78 |
79 | 80 |
81 | {# {{ objform.server.label( class="form-control-label" ) }} #} 82 | {{ objform.server( class="form-control",value="{}".format(server.id) ) }} 83 |
84 | 85 |
86 | {{ objform.submit( class="form-control btn btn-outline-info" ) }} 87 |
88 |
89 |
-------------------------------------------------------------------------------- /index/templates/partials/variable_creation.html: -------------------------------------------------------------------------------- 1 |
2 | {{ varform.hidden_tag() }} 3 |
4 | Add New Variable 5 |
6 |
7 | {{ varform.name( class="form-control",value="" ) }} 8 |
9 |
10 | {{ varform.address( class="form-control p-1",value="", style="font-size:1em!important;") }} 11 |
12 |
13 |
14 |
15 | {{ varform.var_object( class="custom-select custom-select-md" ) }} 16 |
17 |
18 | 19 | {{ varform.value( class="form-control " ) }} 20 | 21 |   22 |
23 |
24 |
25 |
26 | {{ varform.var_type( class="form-control",value="" ) }} 27 |
28 |
29 |
30 | {{ varform.writable( class="custom-control-input",) }} 31 | 32 |
33 |
34 |
35 |
36 | {{ varform.submit( class="form-control btn-outline-info", style="cursor:pointer" ) }} 37 |
38 |
39 |
-------------------------------------------------------------------------------- /index/templates/server.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | {% block content %} 3 | 4 | {% with messages = get_flashed_messages(with_categories=true) %} 5 | {% if messages %} 6 | {% for category, message in messages %} 7 |
8 | {{ message }} 9 |
10 | {% endfor %} 11 | {% endif %} 12 | {% endwith %} 13 | 14 |
15 | {% include('partials/object_creation.html') %} 16 | 17 |
18 | {% if objects %} 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {% for obj in objects %} 33 | {% for variable in obj.object_variables %} 34 | 35 | 36 | 37 | 38 | 41 | 42 | 43 | 48 | 49 | {% endfor %} 50 | {% endfor %} 51 | {# {% for variable in variables %} 52 | {% endfor %} #} 53 | 54 |
NameAddressTypeWritableObject NameValueAction
{{variable.variable_name}}{{ variable.variable_address }}{{ variable.variable_type }} 39 | {% if variable.variable_writable %} {% else %} {% endif %} 40 | {{ variable.object.object_name }}{{ variable.variable_value }} 44 |

45 | Delete 46 |

47 |
55 |
56 | {% include "partials/variable_creation.html" %} 57 | {% endif %} 58 |
59 |
60 | 61 | {% endblock %}} 62 | {% block javascripts %} 63 | {{ super() }} 64 | 107 | {% endblock %} 108 | 109 | -------------------------------------------------------------------------------- /index/utils.py: -------------------------------------------------------------------------------- 1 | import socket 2 | types = { 3 | "int" 4 | } 5 | 6 | def custom_validation(form): 7 | status = True 8 | for k,v in form.items(): 9 | if v is '': 10 | status = False 11 | return status 12 | 13 | def Variable_Validation(form): 14 | status = True 15 | 16 | 17 | def selectVals(objs): 18 | idlist = [] 19 | namelist = [] 20 | if objs: 21 | for obj in objs: 22 | idlist.append(obj.id) 23 | namelist.append(obj.object_name) 24 | return [('','Select Object')]+list(zip( idlist, namelist )) 25 | else: 26 | return [ ('', 'No Objects Defined' ) ] 27 | 28 | def convert_val(val,vtype): 29 | if vtype is 'int': 30 | return int(val) 31 | elif vtype == 'float': 32 | return float(val) 33 | elif vtype == 'string': 34 | return str(val) 35 | elif vtype == 'bool': 36 | if val == True or val == 'true' or val == 1: 37 | return True 38 | else: 39 | return False 40 | else: 41 | return val 42 | 43 | 44 | 45 | def isOpen( ep_url ): 46 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 47 | try: 48 | s.connect( ( ep_url.split(':')[0], int( ep_url.split(':')[1] ) ) ) 49 | s.shutdown(1) 50 | return True 51 | except: 52 | return False 53 | -------------------------------------------------------------------------------- /install.bat: -------------------------------------------------------------------------------- 1 | python setup.py install -------------------------------------------------------------------------------- /myplc/__init__.py: -------------------------------------------------------------------------------- 1 | from snap7.client import Client as PlcClient 2 | from opcua import Client 3 | from index.models import Server 4 | from snap7.snap7types import areas 5 | from snap7.util import * 6 | import time 7 | import threading 8 | from random import randint 9 | import socket 10 | 11 | class MyPlc: 12 | area={ 'I' : 0x81, 'Q' : 0x82, 'M' : 0x83, 'D': 0x84 } 13 | szs = { 'x':1, 'X': 1, 'b':1, 'B':1, 'w' : 2, 'W' : 2, 'd' : 4, 'D' : 4 } 14 | 15 | def __init__(self, ip='192.168.0.1'): 16 | self.ip = ip 17 | self.INByteArray = bytearray([ 0, 0 ]) 18 | self.MKByteArray = bytearray([ 0, 0 ]) 19 | self.threadStatus = False 20 | self.varsdict = {} 21 | self.threads={} 22 | self.plc = PlcClient() 23 | self.subNodes = [] 24 | self.subNodesD = {} 25 | self.keysDict = {} 26 | self.inNodes = {} 27 | 28 | 29 | 30 | def get_db(self,server_id): 31 | self.db_server = Server.query.get( server_id ) 32 | self.connections() 33 | 34 | 35 | def connections( self ): 36 | self.opc_ns_uri = self.db_server.server_namespace 37 | self.ep_url = 'opc.tcp://'+self.db_server.server_endpoint_url 38 | self.opclient = Client(self.ep_url) 39 | try: 40 | self.plc.connect( self.ip, 0, 1 ) 41 | # pass 42 | except Exception: 43 | self.conn_stat = "Could not connect to PLC" 44 | else: 45 | self.conn_stat = "PLC Connected Successfully" 46 | 47 | self.opclient.connect() 48 | self.root = self.opclient.get_root_node() 49 | self.idx = self.opclient.get_namespace_index(self.db_server.server_namespace) 50 | self.set_tags(self.db_server.server_objects) 51 | for key,val in self.varsdict.items(): 52 | if re.search("^(M|m)([\d]+).[\d]*$", key) is not None: 53 | self.subNodes.append( val['obj'] ) 54 | self.subNodesD[key] = val 55 | else: 56 | self.inNodes[key] = val 57 | self.run_threads() 58 | handler = SubHandler(self) 59 | sub = self.opclient.create_subscription(200, handler) 60 | handle = sub.subscribe_data_change(self.subNodes) 61 | time.sleep(0.1) 62 | 63 | def set_tags(self,objs): 64 | for obj in objs: 65 | try: 66 | self.make_tag_dict(obj, obj.object_variables) 67 | except Exception: 68 | self.make_tags_dict(obj.object_variables) 69 | finally: 70 | for var in obj.object_variables: 71 | self.keysDict[var.variable_name] = var.variable_address 72 | 73 | 74 | def make_tag_dict(self,obj,allvars): 75 | for var in allvars: 76 | self.varsdict[var.variable_address] = { 77 | 'obj' :self.root.get_child( [ "0:Objects", "{}:{}".format(self.idx,obj.object_name), "{}:{}".format(self.idx,var.variable_name) ] ), 78 | 'type' : var.variable_type 79 | } 80 | def kill_threads(self): 81 | self.threadStatus = False 82 | 83 | def run_threads(self): 84 | self.threadStatus = True 85 | self.threads['update_server'] = threading.Thread( target=self.updateInputs ) 86 | self.threads['update_server'].start() 87 | 88 | def getInputs( self ): 89 | while self.threadStatus: 90 | self.INByteArray = self.plc.read_area( areas['PE'], 0, 0, 2 ) 91 | # self.INByteArray = bytearray([ randint(0,7), randint(0,7) ]) 92 | time.sleep(.1) 93 | 94 | # def get_bool(_bytearray, byte_index, bool_index): 95 | def updateInputs(self): 96 | while self.threadStatus: 97 | for key,val in self.inNodes.items(): 98 | self.update_server_vars(key) 99 | time.sleep(.01) 100 | 101 | def writetoPLC(self,value,node): 102 | key = self.keysDict[node.get_browse_name().to_string().split(':')[1]] 103 | self.write_to_plc( key, value ) 104 | 105 | ''' 106 | Get Data from the PLC and Update OPC Server variables 107 | ''' 108 | def update_server_vars(self, addr_key ): 109 | addr = addr_key.split('.') 110 | # Works with Boolean values from a Data Block 111 | if len(addr) == 3 and addr[0][0] == 'D': 112 | DBn = int(addr[0][2:]) 113 | DBt = addr[1][2] 114 | byt = int( addr[1][3:] ) 115 | bit = int( addr[2] ) 116 | reading = self.plc.read_area( MyPlc.area['D'], DBn, byt, szs[DBt] ) 117 | if DBt == 'X' or DBt == 'x': 118 | self.varsdict[addr_key]['obj'].set_value( get_bool( reading, 0, bit ) ) 119 | # return get_bool( reading, 0, bit ) 120 | else: 121 | self.varsdict[addr_key]['obj'].set_value( reading ) 122 | # return reading 123 | 124 | # Works with other data types from a Data Block 125 | elif len(addr) == 2 and addr[0][0] == 'D': 126 | DBn = int(addr[0][2:]) 127 | DBt = addr[1][2] 128 | byt = int( addr[1][3:] ) 129 | reading = self.plc.read_area( MyPlc.area['D'], DBn, byt, szs[DBt] ) 130 | if DBt == 'W' or DBt == 'w': 131 | self.varsdict[addr_key]['obj'].set_value( get_int(reading,0) ) 132 | # return get_int(reading,0) 133 | elif DBt == 'D' or DBt == 'd': 134 | self.varsdict[addr_key]['obj'].set_value( get_real(reading,0) ) 135 | # return get_real(reading,0) 136 | else: 137 | self.varsdict[addr_key]['obj'].set_value( reading ) 138 | 139 | # Works with boolean values from Inputs,Merkels and Outputs 140 | elif len(addr) == 2 : 141 | byt = int( addr[0][1:] ) 142 | bit = int( addr[1] ) 143 | reading = self.plc.read_area( MyPlc.area[addr[0][0]], 0, byt, 1 ) 144 | self.varsdict[addr_key]['obj'].set_value( get_bool(reading,0,bit) ) 145 | # return get_bool(reading,0,bit) 146 | 147 | # Works with other data types from Inputs,Merkels ot Outputs eg MW2 148 | elif len(addr) == 1: 149 | byt = int( addr[0][2:] ) 150 | typ = addr[0][1] 151 | reading = self.plc.read_area( MyPlc.area[ addr[0][0] ], 0, byt, 2 ) 152 | if typ == 'w' or typ == 'W': 153 | self.varsdict[addr_key]['obj'].set_value( get_int(reading,0) ) 154 | # return get_int(reading, 0) 155 | elif typ == 'd' or typ == 'D': 156 | self.varsdict[addr_key]['obj'].set_value( get_real(reading,0) ) 157 | # return get_real(reading, 0) 158 | else: 159 | self.varsdict[addr_key]['obj'].set_value( reading ) 160 | # return reading 161 | 162 | ''' 163 | WRITE DATA TO PLC FROM SERVER 164 | ''' 165 | def write_to_plc(self, addr_key, value ): 166 | addr = addr_key.split('.') 167 | print( "New data change on {} : {}".format( addr_key , value) ) 168 | 169 | # Works with Boolean values from a Data Block 170 | if len(addr) == 3 and addr[0][0] == 'D': 171 | DBn = int(addr[0][2:]) 172 | DBt = addr[1][2] 173 | byt = int( addr[1][3:] ) 174 | bit = int( addr[2] ) 175 | reading = self.plc.read_area( MyPlc.area['D'], DBn, byt, MyPlc.szs[DBt] ) 176 | if DBt == 'X' or DBt == 'x': 177 | set_bool(reading, 0, bit, value) 178 | self.plc.write_area( MyPlc.area['D'], DBn, byt, reading ) 179 | 180 | # Works with other data types from a Data Block 181 | elif len(addr) == 2 and addr[0][0] == 'D': 182 | DBn = int(addr[0][2:]) 183 | DBt = addr[1][2] 184 | byt = int( addr[1][3:] ) 185 | reading = self.plc.read_area( MyPlc.area['D'], DBn, byt, MyPlc.szs[DBt] ) 186 | if DBt == 'W' or DBt == 'w': 187 | set_int(reading, 0, value) 188 | elif DBt == 'D' or DBt == 'd': 189 | set_real(reading, 0, value) 190 | self.plc.write_area( MyPlc.area['D'], DBn, byt, reading ) 191 | 192 | # Works with boolean values from Inputs,Merkels ot Outputs 193 | elif len(addr) == 2 : 194 | byt = int( addr[0][1:] ) 195 | bit = int( addr[1] ) 196 | reading = self.plc.read_area( MyPlc.area[addr[0][0]], 0, byt, 1 ) 197 | set_bool(reading, 0, bit, value) 198 | self.plc.write_area( MyPlc.area[addr[0][0]], 0, byt, reading ) 199 | 200 | # Works with other data types from Inputs,Merkels ot Outputs eg MW2 201 | elif len(addr) == 1: 202 | byt = int( addr[0][2:] ) 203 | typ = addr[0][1] 204 | reading = self.plc.read_area( MyPlc.area[ addr[0][0] ], 0, byt, 2 ) 205 | if typ == 'w' or typ == 'W': 206 | set_int(reading, 0, value) 207 | elif typ == 'd' or typ == 'D': 208 | set_real(reading, 0, value) 209 | else: 210 | set_data( value ) 211 | self.plc.write_area( MyPlc.area[addr[0][0]], 0, byt, reading ) 212 | 213 | 214 | class SubHandler(): 215 | def __init__(self,myplc): 216 | self.myplc = myplc 217 | 218 | def datachange_notification(self, node, val, data): 219 | self.myplc.threads['writetoplc'] = threading.Thread( target=self.myplc.writetoPLC, args=(val,node) ) 220 | self.myplc.threads['writetoplc'].start() 221 | 222 | def event_notification(self, event): 223 | print("Python: New event", event) 224 | 225 | -------------------------------------------------------------------------------- /myplc/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andykibz/opcserver/ec06c31726a68f766a030d71e583ee63ee0acc07/myplc/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /myserver/__init__.py: -------------------------------------------------------------------------------- 1 | from opcua import Server as UAServer 2 | from index.models import Server,Object,Variable 3 | from index.utils import convert_val 4 | 5 | class MyServer: 6 | ''' 7 | initialise opcua server object 8 | ''' 9 | def __init__( self ): 10 | self.opc_server = UAServer() # OPC UA server instance 11 | self.opc_objects_dict = {} 12 | self.opc_variables_dict = {} 13 | # self.db_server=Server.query.get( server_id ) 14 | # self.initialise() 15 | self.instantiate_server_vars() 16 | 17 | def get_db(self,server_id): 18 | self.db_server = Server.query.get( server_id ) 19 | self.instantiate_server_vars() 20 | 21 | ''' 22 | Instantiate all server related variables from the Sqlite DB server 23 | to the opcua server instance 24 | ''' 25 | def initialise( self ): 26 | self.instantiate_server_vars() 27 | 28 | 29 | def instantiate_server_vars( self ): 30 | self.opc_server_endpoint = "opc.tcp://"+self.db_server.server_endpoint_url 31 | self.opc_server_name = self.db_server.server_name 32 | self.opc_server_uri = self.db_server.server_namespace 33 | self.ns_idx = self.opc_server.register_namespace( self.opc_server_uri ) 34 | self.opc_objects = self.opc_server.get_objects_node() 35 | self.opc_server.set_endpoint(self.opc_server_endpoint) 36 | self.load_server( self.db_server.server_objects ) 37 | 38 | ''' 39 | load opc server with objects and variables from the SQlite DB 40 | ''' 41 | def load_server( self, db_objects, parent_obj=None ): 42 | for server_obj in db_objects: 43 | if( server_obj.id not in self.opc_objects_dict ): 44 | if parent_obj is None: 45 | self.opc_objects_dict[server_obj.id] = self.opc_objects.add_object( self.ns_idx, server_obj.object_name ) 46 | else: 47 | self.opc_objects_dict[server_obj.id] = parent_obj.add_object( self.ns_idx, server_obj.object_name ) 48 | 49 | self.load_object_variables(server_obj.object_variables, self.opc_objects_dict[server_obj.id]) 50 | 51 | if( server_obj.get_child_objects().count() > 0 ): 52 | self.load_server(server_obj.get_child_objects(),self.opc_objects_dict[server_obj.id] ) 53 | 54 | 55 | def load_object_variables(self, variables, object_owner): 56 | for variable in variables: 57 | self.opc_variables_dict[variable.variable_address] = object_owner.add_variable( self.ns_idx, variable.variable_name, convert_val(variable.variable_value,variable.variable_type ) ) 58 | if variable.variable_writable: 59 | self.opc_variables_dict[variable.variable_address].set_writable() 60 | 61 | def start_opc_server(self): 62 | self.opc_server.start() 63 | 64 | def stop_opc_server(self): 65 | self.opc_server.stop() 66 | 67 | @classmethod 68 | def kill_all_servers(cls): 69 | for addr in addrs: 70 | pass 71 | 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /myserver/__pycache__/__init__.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Andykibz/opcserver/ec06c31726a68f766a030d71e583ee63ee0acc07/myserver/__pycache__/__init__.cpython-37.pyc -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from index import app 2 | if __name__ == "__main__": 3 | app.run('0.0.0.0',debug=True) 4 | -------------------------------------------------------------------------------- /s71200opc.egg-info/PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.0 2 | Name: s71200opc 3 | Version: 0.1.dev0 4 | Summary: UNKNOWN 5 | Home-page: UNKNOWN 6 | Author: Andrew Kibor 7 | Author-email: andykibz@gmail.com 8 | License: MIT 9 | Description: Connect PLC to opcserver 10 | Platform: UNKNOWN 11 | -------------------------------------------------------------------------------- /s71200opc.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- 1 | README.txt 2 | setup.py 3 | index/__init__.py 4 | index/forms.py 5 | index/models.py 6 | index/routes.py 7 | index/utils.py 8 | myplc/__init__.py 9 | myserver/__init__.py 10 | s71200opc.egg-info/PKG-INFO 11 | s71200opc.egg-info/SOURCES.txt 12 | s71200opc.egg-info/dependency_links.txt 13 | s71200opc.egg-info/requires.txt 14 | s71200opc.egg-info/top_level.txt -------------------------------------------------------------------------------- /s71200opc.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /s71200opc.egg-info/requires.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | opcua 3 | python-snap7 4 | SQLAlchemy 5 | Flask-SQLAlchemy 6 | cryptography 7 | WTForms 8 | Flask-WTF 9 | -------------------------------------------------------------------------------- /s71200opc.egg-info/top_level.txt: -------------------------------------------------------------------------------- 1 | index 2 | myplc 3 | myserver 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='s71200opc', 5 | version='0.1dev', 6 | packages=['myplc','index','myserver'], 7 | license='MIT', 8 | author='Andrew Kibor', 9 | author_email='andykibz@gmail.com', 10 | long_description="Connect PLC to opcserver", 11 | install_requires=[ 12 | 'cffi', 13 | 'Click', 14 | 'cryptography', 15 | 'Flask', 16 | 'Flask-SQLAlchemy', 17 | 'Flask-WTF', 18 | 'itsdangerous', 19 | 'Jinja2', 20 | 'lxml', 21 | 'MarkupSafe', 22 | 'opcua', 23 | 'pycparser', 24 | 'python-dateutil', 25 | 'python-snap7', 26 | 'pytz', 27 | 'six', 28 | 'SQLAlchemy', 29 | 'Werkzeug', 30 | 'WTForms' 31 | ], 32 | ) 33 | --------------------------------------------------------------------------------