Merge pull request #1 in IPP/vfd_ipp from feature/IPP-1-write-pocloud-driver-for-vfd_ipp to master

* commit '9963e29de5a1625c39c2eb34794d94040d745e4a':
  fixed driver so code will run.
  Updated config.txt with all files
  Added POCloud driver
  Helper programs to print channels to configure in POCloud
  Added programs to create pickle file of tags in PLC
  Add my micro800 pycomm module
  Added Remote Trip Alarm
This commit is contained in:
Patrick McDonagh
2016-03-02 17:04:41 +00:00
73 changed files with 8741 additions and 128 deletions

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
*.tmp
*.ccwsuo
*.pyc

View File

@@ -1 +1 @@
COMPILED
TO RELINK

View File

@@ -47,6 +47,7 @@ Controller.Micro820.Micro820._IO_P1_AI_00
Controller.Micro820.Micro820._IO_P1_AI_01
Controller.Micro820.Micro820.ALARM_Mode
Controller.Micro820.Micro820.ALARM_Pressure
Controller.Micro820.Micro820.ALARM_Remote
Controller.Micro820.Micro820.ALARM_Temperature
Controller.Micro820.Micro820.ALARM_TubingPressure
Controller.Micro820.Micro820.ALARM_VFD
1 Controller.Micro820.Micro820.__SYSVA_ABORT_CYCLE FALSE
47 Controller.Micro820.Micro820._IO_P1_AI_01
48 Controller.Micro820.Micro820.ALARM_Mode
49 Controller.Micro820.Micro820.ALARM_Pressure
50 Controller.Micro820.Micro820.ALARM_Remote
51 Controller.Micro820.Micro820.ALARM_Temperature
52 Controller.Micro820.Micro820.ALARM_TubingPressure
53 Controller.Micro820.Micro820.ALARM_VFD

View File

@@ -1,90 +1,90 @@
<modbusServer Version="2.0">
<modbusRegister name="DISCRETES_INPUTS">
<mapping variable="Auto_Mode" parent="Micro820" dataType="Bool" address="100001" va="0x470">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Test_Mode" parent="Micro820" dataType="Bool" address="100002" va="0x47a">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Hand_Mode" parent="Micro820" dataType="Bool" address="100003" va="0x46f">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Start_Permissive" parent="Micro820" dataType="Bool" address="100005" va="0x464">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Run_Permissive" parent="Micro820" dataType="Bool" address="100006" va="0x465">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Pressure_Shutdown_Enabled" parent="Micro820" dataType="Bool" address="100022" va="0x46b">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Temp_Startup_Enabled" parent="Micro820" dataType="Bool" address="100023" va="0x46a">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Pressure_Startup_Enabled" parent="Micro820" dataType="Bool" address="100024" va="0x46c">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Start_Command" parent="Micro820" dataType="Bool" address="100025" va="0x462">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Stop_Command" parent="Micro820" dataType="Bool" address="100026" va="0x463">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Pressure_Switch_Enabled" parent="Micro820" dataType="Bool" address="100027" va="0x49c">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Downhole_Tool_Enabled" parent="Micro820" dataType="Bool" address="100028" va="0x49d">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
</modbusRegister>
<modbusRegister name="HOLDING_REGISTERS">
<mapping variable="DH_DownholeStatus_INT" parent="Micro820" dataType="UInt" address="400047" va="0x512">
<MBVarInfo ElemType="UInt" SubElemType="Any" DataTypeSize="2" />
</mapping>
<mapping variable="DH_IntakePressure" parent="Micro820" dataType="Real" address="400048" va="0x550">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="DH_IntakeTemperature" parent="Micro820" dataType="Real" address="400050" va="0x54c">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="DH_MaxIntakePressure_Forever" parent="Micro820" dataType="UInt" address="400052" va="0x510">
<MBVarInfo ElemType="UInt" SubElemType="Any" DataTypeSize="2" />
</mapping>
<mapping variable="DH_MaxIntakePressure_Startup" parent="Micro820" dataType="UInt" address="400053" va="0x50a">
<MBVarInfo ElemType="UInt" SubElemType="Any" DataTypeSize="2" />
</mapping>
<mapping variable="DH_MaxIntakeTemperature_Forever" parent="Micro820" dataType="Real" address="400054" va="0x570">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="DH_MaxIntakeTemperature_Startup" parent="Micro820" dataType="Real" address="400056" va="0x56c">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="DH_NumChannels" parent="Micro820" dataType="UInt" address="400058" va="0x506">
<MBVarInfo ElemType="UInt" SubElemType="Any" DataTypeSize="2" />
</mapping>
<mapping variable="DH_PSIRating" parent="Micro820" dataType="UInt" address="400059" va="0x50e">
<MBVarInfo ElemType="UInt" SubElemType="Any" DataTypeSize="2" />
</mapping>
<mapping variable="DH_ToolType" parent="Micro820" dataType="UInt" address="400060" va="0x50c">
<MBVarInfo ElemType="UInt" SubElemType="Any" DataTypeSize="2" />
</mapping>
<mapping variable="DH_ToolVoltage" parent="Micro820" dataType="UInt" address="400061" va="0x508">
<MBVarInfo ElemType="UInt" SubElemType="Any" DataTypeSize="2" />
</mapping>
<mapping variable="Pressure_Shutdown" parent="Micro820" dataType="Real" address="400070" va="0x55c">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="Pressure_Startup" parent="Micro820" dataType="Real" address="400072" va="0x560">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="Temp_Shutdown" parent="Micro820" dataType="Real" address="400074" va="0x554">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="Temp_Startup" parent="Micro820" dataType="Real" address="400076" va="0x558">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="Pressure_In" parent="Micro820" dataType="Real" address="400138" va="0x6cc">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
</modbusRegister>
<modbusServer Version="2.0">
<modbusRegister name="DISCRETES_INPUTS">
<mapping variable="Auto_Mode" parent="Micro820" dataType="Bool" address="100001" va="0x470">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Test_Mode" parent="Micro820" dataType="Bool" address="100002" va="0x47a">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Hand_Mode" parent="Micro820" dataType="Bool" address="100003" va="0x46f">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Start_Permissive" parent="Micro820" dataType="Bool" address="100005" va="0x464">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Run_Permissive" parent="Micro820" dataType="Bool" address="100006" va="0x465">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Pressure_Shutdown_Enabled" parent="Micro820" dataType="Bool" address="100022" va="0x46b">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Temp_Startup_Enabled" parent="Micro820" dataType="Bool" address="100023" va="0x46a">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Pressure_Startup_Enabled" parent="Micro820" dataType="Bool" address="100024" va="0x46c">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Start_Command" parent="Micro820" dataType="Bool" address="100025" va="0x462">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Stop_Command" parent="Micro820" dataType="Bool" address="100026" va="0x463">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Pressure_Switch_Enabled" parent="Micro820" dataType="Bool" address="100027" va="0x49c">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
<mapping variable="Downhole_Tool_Enabled" parent="Micro820" dataType="Bool" address="100028" va="0x49d">
<MBVarInfo ElemType="Bool" SubElemType="Any" DataTypeSize="1" />
</mapping>
</modbusRegister>
<modbusRegister name="HOLDING_REGISTERS">
<mapping variable="DH_DownholeStatus_INT" parent="Micro820" dataType="UInt" address="400047" va="0x512">
<MBVarInfo ElemType="UInt" SubElemType="Any" DataTypeSize="2" />
</mapping>
<mapping variable="DH_IntakePressure" parent="Micro820" dataType="Real" address="400048" va="0x550">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="DH_IntakeTemperature" parent="Micro820" dataType="Real" address="400050" va="0x54c">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="DH_MaxIntakePressure_Forever" parent="Micro820" dataType="UInt" address="400052" va="0x510">
<MBVarInfo ElemType="UInt" SubElemType="Any" DataTypeSize="2" />
</mapping>
<mapping variable="DH_MaxIntakePressure_Startup" parent="Micro820" dataType="UInt" address="400053" va="0x50a">
<MBVarInfo ElemType="UInt" SubElemType="Any" DataTypeSize="2" />
</mapping>
<mapping variable="DH_MaxIntakeTemperature_Forever" parent="Micro820" dataType="Real" address="400054" va="0x570">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="DH_MaxIntakeTemperature_Startup" parent="Micro820" dataType="Real" address="400056" va="0x56c">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="DH_NumChannels" parent="Micro820" dataType="UInt" address="400058" va="0x506">
<MBVarInfo ElemType="UInt" SubElemType="Any" DataTypeSize="2" />
</mapping>
<mapping variable="DH_PSIRating" parent="Micro820" dataType="UInt" address="400059" va="0x50e">
<MBVarInfo ElemType="UInt" SubElemType="Any" DataTypeSize="2" />
</mapping>
<mapping variable="DH_ToolType" parent="Micro820" dataType="UInt" address="400060" va="0x50c">
<MBVarInfo ElemType="UInt" SubElemType="Any" DataTypeSize="2" />
</mapping>
<mapping variable="DH_ToolVoltage" parent="Micro820" dataType="UInt" address="400061" va="0x508">
<MBVarInfo ElemType="UInt" SubElemType="Any" DataTypeSize="2" />
</mapping>
<mapping variable="Pressure_Shutdown" parent="Micro820" dataType="Real" address="400070" va="0x55c">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="Pressure_Startup" parent="Micro820" dataType="Real" address="400072" va="0x560">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="Temp_Shutdown" parent="Micro820" dataType="Real" address="400074" va="0x554">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="Temp_Startup" parent="Micro820" dataType="Real" address="400076" va="0x558">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
<mapping variable="Pressure_In" parent="Micro820" dataType="Real" address="400138" va="0x6cc">
<MBVarInfo ElemType="Real" SubElemType="Any" DataTypeSize="4" />
</mapping>
</modbusRegister>
</modbusServer>

View File

@@ -1 +0,0 @@
TO RELINK

View File

@@ -1 +0,0 @@
COMPILED

View File

@@ -243,6 +243,8 @@ IF (NOT Run_Permissive AND VFD_Run_Cmd) THEN
ALARM_TubingPressure := TRUE;
ELSIF NOT RP_VFD THEN
ALARM_VFD := TRUE;
ELSIF NOT RP_Remote THEN
ALARM_Remote := TRUE;
END_IF;
VFD_Start := FALSE;

View File

@@ -1,17 +0,0 @@
RA_PFX_ENET_PAR_WRITE(168):FB,MSG_CIPGENERIC();FB,R_TRIG();
USINT_TO_UDINT(149):
VFD_MESSAGING(163):FB,RA_PFX_ENET_STS_CMD(164);
DINT_TO_USINT(154):
UINT_TO_USINT(155):
VFD_CONFIGMAP(167):FB,RA_PFX_ENET_PAR_READ(166);FB,RA_PFX_ENET_PAR_WRITE(168);
VFD_ERRORMAP(169):
USINT_TO_UINT(151):
RA_PFX_ENET_STS_CMD(164):FB,MSG_CIPGENERIC();FB,R_TRIG();FB,COP();
CONTROL(161):FB,LINEARSCALE(159);
USINT_TO_SINT(152):
LINEARSCALE(159):
SINT_TO_USINT(156):
UDINT_TO_USINT(153):
RA_PFX_ENET_PAR_READ(166):FB,MSG_CIPGENERIC();FB,R_TRIG();
USINT_TO_DINT(150):
READDOWNHOLEDATA(160):

View File

@@ -8,9 +8,9 @@ CRC_OLD=
CRC_NEW=0x84D1B047
DAT_ORG=0
DAT_OLD=0
DAT_NEW=1456350625
DAT_NEW=1456758427
VER_ORG=0
VER_OLD=0
VER_NEW=80
VER_NEW=81
CRC_ALL_OLD=
CRC_ALL_NEW=0xECC34EE1

Binary file not shown.

View File

@@ -1,3 +1,4 @@
Controller.Micro820.Micro820.SP_Remote
Controller.Micro820.Micro820.RP_Remote
Controller.Micro820.Micro820.Remote_Shutdown_Disabled
Controller.Micro820.Micro820.ALARM_Remote

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
MainHP - http://s3.amazonaws.com/device-drivers/multitech/OCG/mainMeshify/
M1 - http://s3.amazonaws.com/device-drivers/M1V2/M1/
vfdipp- https://s3.amazonaws.com/meshify-devicedrivers/vfdipp/

View File

@@ -0,0 +1,90 @@
alarm_mode - boolean
alarm_pressure - boolean
alarm_remote - boolean
alarm_temperature - boolean
alarm_tubingpressure - boolean
alarm_vfd - boolean
auto_mode - boolean
dh_dischargepressure - integer
dh_dischargetemperature - integer
dh_downholestatus_int - integer
dh_fluid_level - float
dh_intakepressure - float
dh_intaketemperature - float
dh_maxintakepressure_forever - integer
dh_maxintakepressure_startup - integer
dh_maxintaketemperature_forever - float
dh_maxintaketemperature_startup - float
dh_numchannels - integer
dh_pressure_shutdown_enabled - boolean
dh_pressure_shutdown_limit - float
dh_pressure_startup - float
dh_pressure_startup_enabled - boolean
dh_psirating - integer
dh_temp_shutdown - float
dh_temp_shutdown_enabled - boolean
dh_temp_startup_enabled - boolean
dh_temp_startup_limit - float
dh_tooltype - integer
dh_toolvoltage - integer
dh_windingtemperature - integer
downhole_tool_enabled - boolean
hand_mode - boolean
mode_test - boolean
off_mode - boolean
remote_shutdown_disabled - boolean
rp_mode - boolean
rp_pressure - boolean
rp_remote - boolean
rp_temperature - boolean
rp_trip - boolean
rp_tubingpressure - boolean
rp_vfd - boolean
run_permissive - boolean
sp_mode - boolean
sp_pressure - boolean
sp_remote - boolean
sp_temperature - boolean
sp_trip - boolean
sp_vfd - boolean
sp_voltage - boolean
start_permissive - boolean
stop_command - boolean
tubingpressure_alarm_delay - integer
tubingpressure_alarm_startup_delay - integer
tubingpressure_eu_max - float
tubingpressure_eu_min - float
tubingpressure_hi - boolean
tubingpressure_hi_sp - float
tubingpressure_lo - boolean
tubingpressure_lo_sp - float
tubingpressure_ok - boolean
tubingpressure_transducer_enabled - boolean
vfd_MinFreq - float
vfd_acceltime - float
vfd_active - boolean
vfd_atspeedref - boolean
vfd_clearfault - boolean
vfd_cmdfwd - boolean
vfd_dcbusvoltage - float
vfd_deceltime - float
vfd_disabled - boolean
vfd_fault - boolean
vfd_fault_code - integer
vfd_fault_commerror - boolean
vfd_fault_drivefault - boolean
vfd_maxfreq - float
vfd_motorpoles - integer
vfd_nameplatefla - float
vfd_nameplatehp - float
vfd_nameplatehz - float
vfd_nameplateolcurrent - float
vfd_nameplaterpm - float
vfd_nameplatevolts - float
vfd_outputcurrent - float
vfd_outputvoltage - float
vfd_ready - boolean
vfd_speedfdbk - float
vfd_speedref - float
vfd_stopmode - integer
vfd_torqueperfmode - integer

21
POCloud_Driver/config.txt Normal file
View File

@@ -0,0 +1,21 @@
{
"driverFileName":"vfdipp.py",
"deviceName":"vfdipp",
"driverId":"0080",
"releaseVersion":"1",
"files": {
"file1":"vfdipp.py",
"file2":"micro800.py",
"file3":"vfd_ipp_channels.p",
"file4":"vfd_ipp_channels_setup.py",
"file5":"pycomm_micro/__init__.py",
"file6":"pycomm_micro/common.py",
"file7":"pycomm_micro/ab_comm/__init__.py",
"file8":"pycomm_micro/ab_comm/clx.py",
"file9":"pycomm_micro/ab_comm/slc.py",
"file10":"pycomm_micro/cip/__init__.py",
"file11":"pycomm_micro/cip/cip_base.py",
"file12":"pycomm_micro/cip/cip_const.py" }
}

View File

@@ -0,0 +1,20 @@
import pickle
with open('vfd_ipp_channels.p', 'rb') as ch_f:
channels = pickle.load(ch_f)
out = []
for x in channels.keys():
chName = x
dType = channels[x]['data_type']
if dType == 'REAL':
dType = "float"
elif dType[-3:] == "INT":
dType = "integer"
elif dType == "BOOL":
dType = "boolean"
else:
print dType
out.append("{0} - {1}".format(chName, dType))
for a in sorted(out):
print a

View File

@@ -0,0 +1,94 @@
from pycomm_micro.ab_comm.clx import Driver as u800Driver
import logging
import sys
def readMicroTag(addr, tag):
logging.basicConfig(
filename="u800Driver.log",
format="%(levelname)-10s %(asctime)s %(message)s",
level=logging.DEBUG
)
c = u800Driver()
if c.open(addr):
try:
v = c.read_tag(tag)
# print(v)
return v
except Exception as e:
err = c.get_status()
c.close()
print err
pass
c.close()
def getTagType(addr, tag):
logging.basicConfig(
filename="u800Driver.log",
format="%(levelname)-10s %(asctime)s %(message)s",
level=logging.DEBUG
)
c = u800Driver()
if c.open(addr):
try:
return c.read_tag(tag)[1]
except Exception as e:
err = c.get_status()
c.close()
print err
pass
c.close()
def writeMicroTag(addr, tag, val):
logging.basicConfig(
filename="u800Driver.log",
format="%(levelname)-10s %(asctime)s %(message)s",
level=logging.DEBUG
)
c = u800Driver()
if c.open(addr):
try:
#typ = getTagType(addr, tag)
cv = c.read_tag(tag)
wt = c.write_tag(tag, val, cv[1])
# print(wt)
return wt
except Exception as e:
err = c.get_status()
c.close()
print err
pass
c.close()
def readMicroTagList(addr, tList):
logging.basicConfig(
filename="u800Driver.log",
format="%(levelname)-10s %(asctime)s %(message)s",
level=logging.DEBUG
)
c = u800Driver()
if c.open(addr):
vals = []
try:
for t in tList:
v = c.read_tag(t)
vals.append({"tag":t,"val":v[0], "type":v[1]})
# print(v)
# print("{0} - {1}".format(t, v))
except Exception as e:
err = c.get_status()
c.close()
print err
pass
c.close()
return vals
if __name__ == '__main__':
if len(sys.argv) > 2:
print(readMicroTag(sys.argv[1], sys.argv[2]))
else:
print ("Did not pass a target and tag name.")

View File

@@ -0,0 +1,27 @@
import pickle
import xml.etree.ElementTree as ET
tree = ET.parse('tags.xml')
root = tree.getroot()
channels = {}
for child in root:
try:
print "{0} - {1}".format(child[1].text, child[2].text)
tagName = child[1].text
tagType = child[2].text
channels[tagName] = {
'tag':tagName,
"last_value": "",
"data_type": tagType,
"change_amount": .5,
"last_time_uploaded": 0,
"min_time_between_uploads": 360
}
if tagType == "BOOL":
channels[tagName]['change_amount'] = None
except:
print "oops"
with open('testPickle.p','wb') as tp:
pickle.dump(channels, tp)

View File

@@ -0,0 +1 @@
__author__ = 'agostino'

View File

@@ -0,0 +1,2 @@
__author__ = 'agostino'
import logging

View File

@@ -0,0 +1,847 @@
# -*- coding: utf-8 -*-
#
# clx.py - Ethernet/IP Client for Rockwell PLCs
#
#
# Copyright (c) 2014 Agostino Ruscito <ruscito@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
from pycomm_micro.cip.cip_base import *
from pycomm_micro.common import setup_logger
import logging
class Driver(Base):
"""
This Ethernet/IP client is based on Rockwell specification. Please refer to the link below for details.
http://literature.rockwellautomation.com/idc/groups/literature/documents/pm/1756-pm020_-en-p.pdf
The following services have been implemented:
- Read Tag Service (0x4c)
- Read Tag Fragment Service (0x52)
- Write Tag Service (0x4d)
- Write Tag Fragment Service (0x53)
- Multiple Service Packet (0x0a)
The client has been successfully tested with the following PLCs:
- CompactLogix 5330ERM
- CompactLogix 5370
- ControlLogix 5572 and 1756-EN2T Module
"""
def __init__(self, debug=False, filename=None):
if debug:
super(Driver, self).__init__(setup_logger('ab_comm.clx', logging.DEBUG, filename))
else:
super(Driver, self).__init__(setup_logger('ab_comm.clx', logging.INFO, filename))
self._buffer = {}
self._get_template_in_progress = False
self.__version__ = '0.2'
def get_last_tag_read(self):
""" Return the last tag read by a multi request read
:return: A tuple (tag name, value, type)
"""
return self._last_tag_read
def get_last_tag_write(self):
""" Return the last tag write by a multi request write
:return: A tuple (tag name, 'GOOD') if the write was successful otherwise (tag name, 'BAD')
"""
return self._last_tag_write
def _parse_instance_attribute_list(self, start_tag_ptr, status):
""" extract the tags list from the message received
:param start_tag_ptr: The point in the message string where the tag list begin
:param status: The status of the message receives
"""
tags_returned = self._reply[start_tag_ptr:]
tags_returned_length = len(tags_returned)
idx = 0
instance = 0
count = 0
try:
while idx < tags_returned_length:
instance = unpack_dint(tags_returned[idx:idx+4])
idx += 4
tag_length = unpack_uint(tags_returned[idx:idx+2])
idx += 2
tag_name = tags_returned[idx:idx+tag_length]
idx += tag_length
symbol_type = unpack_uint(tags_returned[idx:idx+2])
idx += 2
count += 1
self._tag_list.append({'instance_id': instance,
'tag_name': tag_name,
'symbol_type': symbol_type})
except Exception as e:
raise DataError(e)
if status == SUCCESS:
self._last_instance = -1
elif status == 0x06:
self._last_instance = instance + 1
else:
self._status = (1, 'unknown status during _parse_tag_list')
self._last_instance = -1
def _parse_structure_makeup_attributes(self, start_tag_ptr, status):
""" extract the tags list from the message received
:param start_tag_ptr: The point in the message string where the tag list begin
:param status: The status of the message receives
"""
self._buffer = {}
if status != SUCCESS:
self._buffer['Error'] = status
return
attribute = self._reply[start_tag_ptr:]
idx = 4
try:
if unpack_uint(attribute[idx:idx + 2]) == SUCCESS:
idx += 2
self._buffer['object_definition_size'] = unpack_dint(attribute[idx:idx + 4])
else:
self._buffer['Error'] = 'object_definition Error'
return
idx += 6
if unpack_uint(attribute[idx:idx + 2]) == SUCCESS:
idx += 2
self._buffer['structure_size'] = unpack_dint(attribute[idx:idx + 4])
else:
self._buffer['Error'] = 'structure Error'
return
idx += 6
if unpack_uint(attribute[idx:idx + 2]) == SUCCESS:
idx += 2
self._buffer['member_count'] = unpack_uint(attribute[idx:idx + 2])
else:
self._buffer['Error'] = 'member_count Error'
return
idx += 4
if unpack_uint(attribute[idx:idx + 2]) == SUCCESS:
idx += 2
self._buffer['structure_handle'] = unpack_uint(attribute[idx:idx + 2])
else:
self._buffer['Error'] = 'structure_handle Error'
return
return self._buffer
except Exception as e:
raise DataError(e)
def _parse_template(self, start_tag_ptr, status):
""" extract the tags list from the message received
:param start_tag_ptr: The point in the message string where the tag list begin
:param status: The status of the message receives
"""
tags_returned = self._reply[start_tag_ptr:]
bytes_received = len(tags_returned)
self._buffer += tags_returned
if status == SUCCESS:
self._get_template_in_progress = False
elif status == 0x06:
self._byte_offset += bytes_received
else:
self._status = (1, 'unknown status {0} during _parse_template'.format(status))
self.logger.warning(self._status)
self._last_instance = -1
def _parse_fragment(self, start_ptr, status):
""" parse the fragment returned by a fragment service.
:param start_ptr: Where the fragment start within the replay
:param status: status field used to decide if keep parsing or stop
"""
try:
data_type = unpack_uint(self._reply[start_ptr:start_ptr+2])
fragment_returned = self._reply[start_ptr+2:]
except Exception as e:
raise DataError(e)
fragment_returned_length = len(fragment_returned)
idx = 0
while idx < fragment_returned_length:
try:
typ = I_DATA_TYPE[data_type]
value = UNPACK_DATA_FUNCTION[typ](fragment_returned[idx:idx+DATA_FUNCTION_SIZE[typ]])
idx += DATA_FUNCTION_SIZE[typ]
except Exception as e:
raise DataError(e)
self._tag_list.append((self._last_position, value))
self._last_position += 1
if status == SUCCESS:
self._byte_offset = -1
elif status == 0x06:
self._byte_offset += fragment_returned_length
else:
self._status = (2, 'unknown status during _parse_fragment')
self._byte_offset = -1
def _parse_multiple_request_read(self, tags):
""" parse the message received from a multi request read:
For each tag parsed, the information extracted includes the tag name, the value read and the data type.
Those information are appended to the tag list as tuple
:return: the tag list
"""
offset = 50
position = 50
try:
number_of_service_replies = unpack_uint(self._reply[offset:offset+2])
tag_list = []
for index in range(number_of_service_replies):
position += 2
start = offset + unpack_uint(self._reply[position:position+2])
general_status = unpack_usint(self._reply[start+2:start+3])
if general_status == 0:
data_type = unpack_uint(self._reply[start+4:start+6])
value_begin = start + 6
value_end = value_begin + DATA_FUNCTION_SIZE[I_DATA_TYPE[data_type]]
value = self._reply[value_begin:value_end]
self._last_tag_read = (tags[index], UNPACK_DATA_FUNCTION[I_DATA_TYPE[data_type]](value),
I_DATA_TYPE[data_type])
else:
self._last_tag_read = (tags[index], None, None)
tag_list.append(self._last_tag_read)
return tag_list
except Exception as e:
raise DataError(e)
def _parse_multiple_request_write(self, tags):
""" parse the message received from a multi request writ:
For each tag parsed, the information extracted includes the tag name and the status of the writing.
Those information are appended to the tag list as tuple
:return: the tag list
"""
offset = 50
position = 50
try:
number_of_service_replies = unpack_uint(self._reply[offset:offset+2])
tag_list = []
for index in range(number_of_service_replies):
position += 2
start = offset + unpack_uint(self._reply[position:position+2])
general_status = unpack_usint(self._reply[start+2:start+3])
if general_status == 0:
self._last_tag_write = (tags[index] + ('GOOD',))
else:
self._last_tag_write = (tags[index] + ('BAD',))
tag_list.append(self._last_tag_write)
return tag_list
except Exception as e:
raise DataError(e)
def _check_reply(self):
""" check the replayed message for error
"""
self._more_packets_available = False
try:
if self._reply is None:
self._status = (3, '%s without reply' % REPLAY_INFO[unpack_dint(self._message[:2])])
return False
# Get the type of command
typ = unpack_uint(self._reply[:2])
# Encapsulation status check
if unpack_dint(self._reply[8:12]) != SUCCESS:
self._status = (3, "{0} reply status:{1}".format(REPLAY_INFO[typ],
SERVICE_STATUS[unpack_dint(self._reply[8:12])]))
return False
# Command Specific Status check
if typ == unpack_uint(ENCAPSULATION_COMMAND["send_rr_data"]):
status = unpack_usint(self._reply[42:43])
if status != SUCCESS:
self._status = (3, "send_rr_data reply:{0} - Extend status:{1}".format(
SERVICE_STATUS[status], get_extended_status(self._reply, 42)))
return False
else:
return True
elif typ == unpack_uint(ENCAPSULATION_COMMAND["send_unit_data"]):
status = unpack_usint(self._reply[48:49])
if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Read Tag Fragmented"]:
self._parse_fragment(50, status)
return True
if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Get Instance Attributes List"]:
self._parse_instance_attribute_list(50, status)
return True
if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Get Attributes"]:
self._parse_structure_makeup_attributes(50, status)
return True
if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Read Template"] and \
self._get_template_in_progress:
self._parse_template(50, status)
return True
if status == 0x06:
self._status = (3, "Insufficient Packet Space")
self._more_packets_available = True
elif status != SUCCESS:
self._status = (3, "send_unit_data reply:{0} - Extend status:{1}".format(
SERVICE_STATUS[status], get_extended_status(self._reply, 48)))
return False
else:
return True
return True
except Exception as e:
raise DataError(e)
def read_tag(self, tag):
""" read tag from a connected plc
Possible combination can be passed to this method:
- ('Counts') a single tag name
- (['ControlWord']) a list with one tag or many
- (['parts', 'ControlWord', 'Counts'])
At the moment there is not a strong validation for the argument passed. The user should verify
the correctness of the format passed.
:return: None is returned in case of error otherwise the tag list is returned
"""
multi_requests = False
if isinstance(tag, list):
multi_requests = True
if not self._target_is_connected:
if not self.forward_open():
self._status = (6, "Target did not connected. read_tag will not be executed.")
self.logger.warning(self._status)
raise Error("Target did not connected. read_tag will not be executed.")
# multi_requests = False
if multi_requests:
rp_list = []
for t in tag:
rp = create_tag_rp(t, multi_requests=True)
if rp is None:
self._status = (6, "Cannot create tag {0} request packet. read_tag will not be executed.".format(tag))
raise DataError("Cannot create tag {0} request packet. read_tag will not be executed.".format(tag))
else:
rp_list.append(chr(TAG_SERVICES_REQUEST['Read Tag']) + rp + pack_uint(1))
message_request = build_multiple_service(rp_list, Base._get_sequence())
else:
rp = create_tag_rp(tag)
if rp is None:
self._status = (6, "Cannot create tag {0} request packet. read_tag will not be executed.".format(tag))
return None
else:
# Creating the Message Request Packet
message_request = [
pack_uint(Base._get_sequence()),
chr(TAG_SERVICES_REQUEST['Read Tag']), # the Request Service
chr(len(rp) / 2), # the Request Path Size length in word
rp, # the request path
pack_uint(1)
]
if self.send_unit_data(
build_common_packet_format(
DATA_ITEM['Connected'],
''.join(message_request),
ADDRESS_ITEM['Connection Based'],
addr_data=self._target_cid,
)) is None:
raise DataError("send_unit_data returned not valid data")
if multi_requests:
return self._parse_multiple_request_read(tag)
else:
# Get the data type
data_type = unpack_uint(self._reply[50:52])
# print I_DATA_TYPE[data_type]
try:
return UNPACK_DATA_FUNCTION[I_DATA_TYPE[data_type]](self._reply[52:]), I_DATA_TYPE[data_type]
except Exception as e:
raise DataError(e)
def read_array(self, tag, counts):
""" read array of atomic data type from a connected plc
At the moment there is not a strong validation for the argument passed. The user should verify
the correctness of the format passed.
:param tag: the name of the tag to read
:param counts: the number of element to read
:return: None is returned in case of error otherwise the tag list is returned
"""
if not self._target_is_connected:
if not self.forward_open():
self._status = (7, "Target did not connected. read_tag will not be executed.")
self.logger.warning(self._status)
raise Error("Target did not connected. read_tag will not be executed.")
self._byte_offset = 0
self._last_position = 0
self._tag_list = []
while self._byte_offset != -1:
rp = create_tag_rp(tag)
if rp is None:
self._status = (7, "Cannot create tag {0} request packet. read_tag will not be executed.".format(tag))
return None
else:
# Creating the Message Request Packet
message_request = [
pack_uint(Base._get_sequence()),
chr(TAG_SERVICES_REQUEST["Read Tag Fragmented"]), # the Request Service
chr(len(rp) / 2), # the Request Path Size length in word
rp, # the request path
pack_uint(counts),
pack_dint(self._byte_offset)
]
if self.send_unit_data(
build_common_packet_format(
DATA_ITEM['Connected'],
''.join(message_request),
ADDRESS_ITEM['Connection Based'],
addr_data=self._target_cid,
)) is None:
raise DataError("send_unit_data returned not valid data")
return self._tag_list
def write_tag(self, tag, value=None, typ=None):
""" write tag/tags from a connected plc
Possible combination can be passed to this method:
- ('tag name', Value, data type) as single parameters or inside a tuple
- ([('tag name', Value, data type), ('tag name2', Value, data type)]) as array of tuples
At the moment there is not a strong validation for the argument passed. The user should verify
the correctness of the format passed.
The type accepted are:
- BOOL
- SINT
- INT'
- DINT
- REAL
- LINT
- BYTE
- WORD
- DWORD
- LWORD
:param tag: tag name, or an array of tuple containing (tag name, value, data type)
:param value: the value to write or none if tag is an array of tuple or a tuple
:param typ: the type of the tag to write or none if tag is an array of tuple or a tuple
:return: None is returned in case of error otherwise the tag list is returned
"""
multi_requests = False
if isinstance(tag, list):
multi_requests = True
if not self._target_is_connected:
if not self.forward_open():
self._status = (8, "Target did not connected. write_tag will not be executed.")
self.logger.warning(self._status)
raise Error("Target did not connected. write_tag will not be executed.")
if multi_requests:
rp_list = []
tag_to_remove = []
idx = 0
for name, value, typ in tag:
# Create the request path to wrap the tag name
rp = create_tag_rp(name, multi_requests=True)
if rp is None:
self._status = (8, "Cannot create tag{0} req. packet. write_tag will not be executed".format(tag))
return None
else:
try: # Trying to add the rp to the request path list
val = PACK_DATA_FUNCTION[typ](value)
rp_list.append(
chr(TAG_SERVICES_REQUEST['Write Tag'])
+ rp
+ pack_uint(S_DATA_TYPE[typ])
+ pack_uint(1)
+ val
)
idx += 1
except (LookupError, struct.error) as e:
self._status = (8, "Tag:{0} type:{1} removed from write list. Error:{2}.".format(name, typ, e))
# The tag in idx position need to be removed from the rp list because has some kind of error
tag_to_remove.append(idx)
# Remove the tags that have not been inserted in the request path list
for position in tag_to_remove:
del tag[position]
# Create the message request
message_request = build_multiple_service(rp_list, Base._get_sequence())
else:
if isinstance(tag, tuple):
name, value, typ = tag
else:
name = tag
rp = create_tag_rp(name)
if rp is None:
self._status = (8, "Cannot create tag {0} request packet. write_tag will not be executed.".format(tag))
self.logger.warning(self._status)
return None
else:
# Creating the Message Request Packet
message_request = [
pack_uint(Base._get_sequence()),
chr(TAG_SERVICES_REQUEST["Write Tag"]), # the Request Service
chr(len(rp) / 2), # the Request Path Size length in word
rp, # the request path
pack_uint(S_DATA_TYPE[typ]), # data type
pack_uint(1), # Add the number of tag to write
PACK_DATA_FUNCTION[typ](value)
]
ret_val = self.send_unit_data(
build_common_packet_format(
DATA_ITEM['Connected'],
''.join(message_request),
ADDRESS_ITEM['Connection Based'],
addr_data=self._target_cid,
)
)
if multi_requests:
return self._parse_multiple_request_write(tag)
else:
if ret_val is None:
raise DataError("send_unit_data returned not valid data")
return ret_val
def write_array(self, tag, data_type, values):
""" write array of atomic data type from a connected plc
At the moment there is not a strong validation for the argument passed. The user should verify
the correctness of the format passed.
:param tag: the name of the tag to read
:param data_type: the type of tag to write
:param values: the array of values to write
"""
if not isinstance(values, list):
self._status = (9, "A list of tags must be passed to write_array.")
self.logger.warning(self._status)
raise DataError("A list of tags must be passed to write_array.")
if not self._target_is_connected:
if not self.forward_open():
self._status = (9, "Target did not connected. write_array will not be executed.")
self.logger.warning(self._status)
raise Error("Target did not connected. write_array will not be executed.")
array_of_values = ""
byte_size = 0
byte_offset = 0
for i, value in enumerate(values):
array_of_values += PACK_DATA_FUNCTION[data_type](value)
byte_size += DATA_FUNCTION_SIZE[data_type]
if byte_size >= 450 or i == len(values)-1:
# create the message and send the fragment
rp = create_tag_rp(tag)
if rp is None:
self._status = (9, "Cannot create tag {0} request packet. \
write_array will not be executed.".format(tag))
return None
else:
# Creating the Message Request Packet
message_request = [
pack_uint(Base._get_sequence()),
chr(TAG_SERVICES_REQUEST["Write Tag Fragmented"]), # the Request Service
chr(len(rp) / 2), # the Request Path Size length in word
rp, # the request path
pack_uint(S_DATA_TYPE[data_type]), # Data type to write
pack_uint(len(values)), # Number of elements to write
pack_dint(byte_offset),
array_of_values # Fragment of elements to write
]
byte_offset += byte_size
if self.send_unit_data(
build_common_packet_format(
DATA_ITEM['Connected'],
''.join(message_request),
ADDRESS_ITEM['Connection Based'],
addr_data=self._target_cid,
)) is None:
raise DataError("send_unit_data returned not valid data")
array_of_values = ""
byte_size = 0
def _get_instance_attribute_list_service(self):
""" Step 1: Finding user-created controller scope tags in a Logix5000 controller
This service returns instance IDs for each created instance of the symbol class, along with a list
of the attribute data associated with the requested attribute
"""
try:
if not self._target_is_connected:
if not self.forward_open():
self._status = (10, "Target did not connected. get_tag_list will not be executed.")
self.logger.warning(self._status)
raise Error("Target did not connected. get_tag_list will not be executed.")
self._last_instance = 0
self._get_template_in_progress = True
while self._last_instance != -1:
# Creating the Message Request Packet
message_request = [
pack_uint(Base._get_sequence()),
chr(TAG_SERVICES_REQUEST['Get Instance Attributes List']), # STEP 1
# the Request Path Size length in word
chr(3),
# Request Path ( 20 6B 25 00 Instance )
CLASS_ID["8-bit"], # Class id = 20 from spec 0x20
CLASS_CODE["Symbol Object"], # Logical segment: Symbolic Object 0x6B
INSTANCE_ID["16-bit"], # Instance Segment: 16 Bit instance 0x25
'\x00',
pack_uint(self._last_instance), # The instance
# Request Data
pack_uint(2), # Number of attributes to retrieve
pack_uint(1), # Attribute 1: Symbol name
pack_uint(2) # Attribute 2: Symbol type
]
if self.send_unit_data(
build_common_packet_format(
DATA_ITEM['Connected'],
''.join(message_request),
ADDRESS_ITEM['Connection Based'],
addr_data=self._target_cid,
)) is None:
raise DataError("send_unit_data returned not valid data")
self._get_template_in_progress = False
except Exception as e:
raise DataError(e)
def _get_structure_makeup(self, instance_id):
"""
get the structure makeup for a specific structure
"""
if not self._target_is_connected:
if not self.forward_open():
self._status = (10, "Target did not connected. get_tag_list will not be executed.")
self.logger.warning(self._status)
raise Error("Target did not connected. get_tag_list will not be executed.")
message_request = [
pack_uint(self._get_sequence()),
chr(TAG_SERVICES_REQUEST['Get Attributes']),
chr(3), # Request Path ( 20 6B 25 00 Instance )
CLASS_ID["8-bit"], # Class id = 20 from spec 0x20
CLASS_CODE["Template Object"], # Logical segment: Template Object 0x6C
INSTANCE_ID["16-bit"], # Instance Segment: 16 Bit instance 0x25
'\x00',
pack_uint(instance_id),
pack_uint(4), # Number of attributes
pack_uint(4), # Template Object Definition Size UDINT
pack_uint(5), # Template Structure Size UDINT
pack_uint(2), # Template Member Count UINT
pack_uint(1) # Structure Handle We can use this to read and write UINT
]
if self.send_unit_data(
build_common_packet_format(DATA_ITEM['Connected'],
''.join(message_request), ADDRESS_ITEM['Connection Based'],
addr_data=self._target_cid,)) is None:
raise DataError("send_unit_data returned not valid data")
return self._buffer
def _read_template(self, instance_id, object_definition_size):
""" get a list of the tags in the plc
"""
if not self._target_is_connected:
if not self.forward_open():
self._status = (10, "Target did not connected. get_tag_list will not be executed.")
self.logger.warning(self._status)
raise Error("Target did not connected. get_tag_list will not be executed.")
self._byte_offset = 0
self._buffer = ""
self._get_template_in_progress = True
try:
while self._get_template_in_progress:
# Creating the Message Request Packet
message_request = [
pack_uint(self._get_sequence()),
chr(TAG_SERVICES_REQUEST['Read Template']),
chr(3), # Request Path ( 20 6B 25 00 Instance )
CLASS_ID["8-bit"], # Class id = 20 from spec 0x20
CLASS_CODE["Template Object"], # Logical segment: Template Object 0x6C
INSTANCE_ID["16-bit"], # Instance Segment: 16 Bit instance 0x25
'\x00',
pack_uint(instance_id),
pack_dint(self._byte_offset), # Offset
pack_uint(((object_definition_size * 4)-23) - self._byte_offset)
]
if not self.send_unit_data(
build_common_packet_format(DATA_ITEM['Connected'], ''.join(message_request),
ADDRESS_ITEM['Connection Based'], addr_data=self._target_cid,)):
raise DataError("send_unit_data returned not valid data")
self._get_template_in_progress = False
return self._buffer
except Exception as e:
raise DataError(e)
def _isolating_user_tag(self):
try:
lst = self._tag_list
self._tag_list = []
for tag in lst:
if tag['tag_name'].find(':') != -1 or tag['tag_name'].find('__') != -1:
continue
if tag['symbol_type'] & 0b0001000000000000:
continue
dimension = tag['symbol_type'] & 0b0110000000000000 >> 13
template_instance_id = tag['symbol_type'] & 0b0000111111111111
if tag['symbol_type'] & 0b1000000000000000 :
tag_type = 'struct'
data_type = 'user-created'
self._tag_list.append({'instance_id': tag['instance_id'],
'template_instance_id': template_instance_id,
'tag_name': tag['tag_name'],
'dim': dimension,
'tag_type': tag_type,
'data_type': data_type,
'template': {},
'udt': {}})
else:
tag_type = 'atomic'
data_type = I_DATA_TYPE[template_instance_id]
self._tag_list.append({'instance_id': tag['instance_id'],
'tag_name': tag['tag_name'],
'dim': dimension,
'tag_type': tag_type,
'data_type': data_type})
except Exception as e:
raise DataError(e)
def _parse_udt_raw(self, tag):
try:
buff = self._read_template(tag['template_instance_id'], tag['template']['object_definition_size'])
member_count = tag['template']['member_count']
names = buff.split('\00')
lst = []
tag['udt']['name'] = 'Not an user defined structure'
for name in names:
if len(name) > 1:
if name.find(';') != -1:
tag['udt']['name'] = name[:name.find(';')]
elif name.find('ZZZZZZZZZZ') != -1:
continue
elif name.isalpha():
lst.append(name)
else:
continue
tag['udt']['internal_tags'] = lst
type_list = []
for i in xrange(member_count):
# skip member 1
if i != 0:
array_size = unpack_uint(buff[:2])
try:
data_type = I_DATA_TYPE[unpack_uint(buff[2:4])]
except Exception:
data_type = "None"
offset = unpack_dint(buff[4:8])
type_list.append((array_size, data_type, offset))
buff = buff[8:]
tag['udt']['data_type'] = type_list
except Exception as e:
raise DataError(e)
def get_tag_list(self):
self._tag_list = []
# Step 1
self._get_instance_attribute_list_service()
# Step 2
self._isolating_user_tag()
# Step 3
for tag in self._tag_list:
if tag['tag_type'] == 'struct':
tag['template'] = self._get_structure_makeup(tag['template_instance_id'])
for idx, tag in enumerate(self._tag_list):
# print (tag)
if tag['tag_type'] == 'struct':
self._parse_udt_raw(tag)
# Step 4
return self._tag_list

View File

@@ -0,0 +1,446 @@
# -*- coding: utf-8 -*-
#
# clx.py - Ethernet/IP Client for Rockwell PLCs
#
#
# Copyright (c) 2014 Agostino Ruscito <ruscito@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
from pycomm_micro.cip.cip_base import *
from pycomm_micro.common import setup_logger
import re
import logging
import math
def parse_tag(tag):
t = re.search(r"(?P<file_type>[CT])(?P<file_number>\d{1,3})"
r"(:)(?P<element_number>\d{1,3})"
r"(.)(?P<sub_element>ACC|PRE|EN|DN|TT|CU|CD|DN|OV|UN|UA)", tag, flags=re.IGNORECASE)
if t:
if (1 <= int(t.group('file_number')) <= 255) \
and (0 <= int(t.group('element_number')) <= 255):
return True, t.group(0), {'file_type': t.group('file_type').upper(),
'file_number': t.group('file_number'),
'element_number': t.group('element_number'),
'sub_element': PCCC_CT[t.group('sub_element').upper()],
'read_func': '\xa2',
'write_func': '\xab',
'address_field': 3}
t = re.search(r"(?P<file_type>[FBN])(?P<file_number>\d{1,3})"
r"(:)(?P<element_number>\d{1,3})"
r"(/(?P<sub_element>\d{1,2}))?",
tag, flags=re.IGNORECASE)
if t:
if t.group('sub_element') is not None:
if (1 <= int(t.group('file_number')) <= 255) \
and (0 <= int(t.group('element_number')) <= 255) \
and (0 <= int(t.group('sub_element')) <= 15):
return True, t.group(0), {'file_type': t.group('file_type').upper(),
'file_number': t.group('file_number'),
'element_number': t.group('element_number'),
'sub_element': t.group('sub_element'),
'read_func': '\xa2',
'write_func': '\xab',
'address_field': 3}
else:
if (1 <= int(t.group('file_number')) <= 255) \
and (0 <= int(t.group('element_number')) <= 255):
return True, t.group(0), {'file_type': t.group('file_type').upper(),
'file_number': t.group('file_number'),
'element_number': t.group('element_number'),
'sub_element': t.group('sub_element'),
'read_func': '\xa2',
'write_func': '\xab',
'address_field': 2}
t = re.search(r"(?P<file_type>[IO])(:)(?P<file_number>\d{1,3})"
r"(.)(?P<element_number>\d{1,3})"
r"(/(?P<sub_element>\d{1,2}))?", tag, flags=re.IGNORECASE)
if t:
if t.group('sub_element') is not None:
if (0 <= int(t.group('file_number')) <= 255) \
and (0 <= int(t.group('element_number')) <= 255) \
and (0 <= int(t.group('sub_element')) <= 15):
return True, t.group(0), {'file_type': t.group('file_type').upper(),
'file_number': t.group('file_number'),
'element_number': t.group('element_number'),
'sub_element': t.group('sub_element'),
'read_func': '\xa2',
'write_func': '\xab',
'address_field': 3}
else:
if (0 <= int(t.group('file_number')) <= 255) \
and (0 <= int(t.group('element_number')) <= 255):
return True, t.group(0), {'file_type': t.group('file_type').upper(),
'file_number': t.group('file_number'),
'element_number': t.group('element_number'),
'read_func': '\xa2',
'write_func': '\xab',
'address_field': 2}
t = re.search(r"(?P<file_type>S)"
r"(:)(?P<element_number>\d{1,3})"
r"(/(?P<sub_element>\d{1,2}))?", tag, flags=re.IGNORECASE)
if t:
if t.group('sub_element') is not None:
if (0 <= int(t.group('element_number')) <= 255) \
and (0 <= int(t.group('sub_element')) <= 15):
return True, t.group(0), {'file_type': t.group('file_type').upper(),
'file_number': '2',
'element_number': t.group('element_number'),
'sub_element': t.group('sub_element'),
'read_func': '\xa2',
'write_func': '\xab',
'address_field': 3}
else:
if 0 <= int(t.group('element_number')) <= 255:
return True, t.group(0), {'file_type': t.group('file_type').upper(),
'file_number': '2',
'element_number': t.group('element_number'),
'read_func': '\xa2',
'write_func': '\xab',
'address_field': 2}
t = re.search(r"(?P<file_type>B)(?P<file_number>\d{1,3})"
r"(/)(?P<element_number>\d{1,4})",
tag, flags=re.IGNORECASE)
if t:
if (1 <= int(t.group('file_number')) <= 255) \
and (0 <= int(t.group('element_number')) <= 4095):
bit_position = int(t.group('element_number'))
element_number = bit_position / 16
sub_element = bit_position - (element_number * 16)
return True, t.group(0), {'file_type': t.group('file_type').upper(),
'file_number': t.group('file_number'),
'element_number': element_number,
'sub_element': sub_element,
'read_func': '\xa2',
'write_func': '\xab',
'address_field': 3}
return False, tag
class Driver(Base):
"""
SLC/PLC_5 Implementation
"""
def __init__(self, debug=False, filename=None):
if debug:
super(Driver, self).__init__(setup_logger('ab_comm.slc', logging.DEBUG, filename))
else:
super(Driver, self).__init__(setup_logger('ab_comm.slc', logging.INFO, filename))
self.__version__ = '0.1'
self._last_sequence = 0
def _check_reply(self):
"""
check the replayed message for error
"""
self._more_packets_available = False
try:
if self._reply is None:
self._status = (3, '%s without reply' % REPLAY_INFO[unpack_dint(self._message[:2])])
return False
# Get the type of command
typ = unpack_uint(self._reply[:2])
# Encapsulation status check
if unpack_dint(self._reply[8:12]) != SUCCESS:
self._status = (3, "{0} reply status:{1}".format(REPLAY_INFO[typ],
SERVICE_STATUS[unpack_dint(self._reply[8:12])]))
return False
# Command Specific Status check
if typ == unpack_uint(ENCAPSULATION_COMMAND["send_rr_data"]):
status = unpack_usint(self._reply[42:43])
if status != SUCCESS:
self._status = (3, "send_rr_data reply:{0} - Extend status:{1}".format(
SERVICE_STATUS[status], get_extended_status(self._reply, 42)))
return False
else:
return True
elif typ == unpack_uint(ENCAPSULATION_COMMAND["send_unit_data"]):
status = unpack_usint(self._reply[48:49])
if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Read Tag Fragmented"]:
self._parse_fragment(50, status)
return True
if unpack_usint(self._reply[46:47]) == I_TAG_SERVICES_REPLY["Get Instance Attributes List"]:
self._parse_tag_list(50, status)
return True
if status == 0x06:
self._status = (3, "Insufficient Packet Space")
self._more_packets_available = True
elif status != SUCCESS:
self._status = (3, "send_unit_data reply:{0} - Extend status:{1}".format(
SERVICE_STATUS[status], get_extended_status(self._reply, 48)))
return False
else:
return True
return True
except Exception as e:
raise DataError(e)
def read_tag(self, tag, n=1):
""" read tag from a connected plc
Possible combination can be passed to this method:
print c.read_tag('F8:0', 3) return a list of 3 registers starting from F8:0
print c.read_tag('F8:0') return one value
It is possible to read status bit
:return: None is returned in case of error
"""
res = parse_tag(tag)
if not res[0]:
self._status = (1000, "Error parsing the tag passed to read_tag({0},{1})".format(tag, n))
self.logger.warning(self._status)
raise DataError("Error parsing the tag passed to read_tag({0},{1})".format(tag, n))
bit_read = False
bit_position = 0
sub_element = 0
if int(res[2]['address_field'] == 3):
bit_read = True
bit_position = int(res[2]['sub_element'])
if not self._target_is_connected:
if not self.forward_open():
self._status = (5, "Target did not connected. read_tag will not be executed.")
self.logger.warning(self._status)
raise Error("Target did not connected. read_tag will not be executed.")
data_size = PCCC_DATA_SIZE[res[2]['file_type']]
# Creating the Message Request Packet
self._last_sequence = pack_uint(Base._get_sequence())
message_request = [
self._last_sequence,
'\x4b',
'\x02',
CLASS_ID["8-bit"],
PATH["PCCC"],
'\x07',
self.attribs['vid'],
self.attribs['vsn'],
'\x0f',
'\x00',
self._last_sequence[1],
self._last_sequence[0],
res[2]['read_func'],
pack_usint(data_size * n),
pack_usint(int(res[2]['file_number'])),
PCCC_DATA_TYPE[res[2]['file_type']],
pack_usint(int(res[2]['element_number'])),
pack_usint(sub_element)
]
self.logger.debug("SLC read_tag({0},{1})".format(tag, n))
if self.send_unit_data(
build_common_packet_format(
DATA_ITEM['Connected'],
''.join(message_request),
ADDRESS_ITEM['Connection Based'],
addr_data=self._target_cid,)):
sts = int(unpack_usint(self._reply[58]))
try:
if sts != 0:
sts_txt = PCCC_ERROR_CODE[sts]
self._status = (1000, "Error({0}) returned from read_tag({1},{2})".format(sts_txt, tag, n))
self.logger.warning(self._status)
raise DataError("Error({0}) returned from read_tag({1},{2})".format(sts_txt, tag, n))
new_value = 61
if bit_read:
if res[2]['file_type'] == 'T' or res[2]['file_type'] == 'C':
if bit_position == PCCC_CT['PRE']:
return UNPACK_PCCC_DATA_FUNCTION[res[2]['file_type']](
self._reply[new_value+2:new_value+2+data_size])
elif bit_position == PCCC_CT['ACC']:
return UNPACK_PCCC_DATA_FUNCTION[res[2]['file_type']](
self._reply[new_value+4:new_value+4+data_size])
tag_value = UNPACK_PCCC_DATA_FUNCTION[res[2]['file_type']](
self._reply[new_value:new_value+data_size])
return get_bit(tag_value, bit_position)
else:
values_list = []
while len(self._reply[new_value:]) >= data_size:
values_list.append(
UNPACK_PCCC_DATA_FUNCTION[res[2]['file_type']](self._reply[new_value:new_value+data_size])
)
new_value = new_value+data_size
if len(values_list) > 1:
return values_list
else:
return values_list[0]
except Exception as e:
self._status = (1000, "Error({0}) parsing the data returned from read_tag({1},{2})".format(e, tag, n))
self.logger.warning(self._status)
raise DataError("Error({0}) parsing the data returned from read_tag({1},{2})".format(e, tag, n))
else:
raise DataError("send_unit_data returned not valid data")
def write_tag(self, tag, value):
""" write tag from a connected plc
Possible combination can be passed to this method:
c.write_tag('N7:0', [-30, 32767, -32767])
c.write_tag('N7:0', 21)
c.read_tag('N7:0', 10)
It is not possible to write status bit
:return: None is returned in case of error
"""
res = parse_tag(tag)
if not res[0]:
self._status = (1000, "Error parsing the tag passed to read_tag({0},{1})".format(tag, value))
self.logger.warning(self._status)
raise DataError("Error parsing the tag passed to read_tag({0},{1})".format(tag, value))
if isinstance(value, list) and int(res[2]['address_field'] == 3):
self._status = (1000, "Function's parameters error. read_tag({0},{1})".format(tag, value))
self.logger.warning(self._status)
raise DataError("Function's parameters error. read_tag({0},{1})".format(tag, value))
if isinstance(value, list) and int(res[2]['address_field'] == 3):
self._status = (1000, "Function's parameters error. read_tag({0},{1})".format(tag, value))
self.logger.warning(self._status)
raise DataError("Function's parameters error. read_tag({0},{1})".format(tag, value))
bit_field = False
bit_position = 0
sub_element = 0
if int(res[2]['address_field'] == 3):
bit_field = True
bit_position = int(res[2]['sub_element'])
values_list = ''
else:
values_list = '\xff\xff'
multi_requests = False
if isinstance(value, list):
multi_requests = True
if not self._target_is_connected:
if not self.forward_open():
self._status = (1000, "Target did not connected. write_tag will not be executed.")
self.logger.warning(self._status)
raise Error("Target did not connected. write_tag will not be executed.")
try:
n = 0
if multi_requests:
data_size = PCCC_DATA_SIZE[res[2]['file_type']]
for v in value:
values_list += PACK_PCCC_DATA_FUNCTION[res[2]['file_type']](v)
n += 1
else:
n = 1
if bit_field:
data_size = 2
if (res[2]['file_type'] == 'T' or res[2]['file_type'] == 'C') \
and (bit_position == PCCC_CT['PRE'] or bit_position == PCCC_CT['ACC']):
sub_element = bit_position
values_list = '\xff\xff' + PACK_PCCC_DATA_FUNCTION[res[2]['file_type']](value)
else:
sub_element = 0
if value > 0:
values_list = pack_uint(math.pow(2, bit_position)) + pack_uint(math.pow(2, bit_position))
else:
values_list = pack_uint(math.pow(2, bit_position)) + pack_uint(0)
else:
values_list += PACK_PCCC_DATA_FUNCTION[res[2]['file_type']](value)
data_size = PCCC_DATA_SIZE[res[2]['file_type']]
except Exception as e:
self._status = (1000, "Error({0}) packing the values to write to the"
"SLC write_tag({1},{2})".format(e, tag, value))
self.logger.warning(self._status)
raise DataError("Error({0}) packing the values to write to the "
"SLC write_tag({1},{2})".format(e, tag, value))
data_to_write = values_list
# Creating the Message Request Packet
self._last_sequence = pack_uint(Base._get_sequence())
message_request = [
self._last_sequence,
'\x4b',
'\x02',
CLASS_ID["8-bit"],
PATH["PCCC"],
'\x07',
self.attribs['vid'],
self.attribs['vsn'],
'\x0f',
'\x00',
self._last_sequence[1],
self._last_sequence[0],
res[2]['write_func'],
pack_usint(data_size * n),
pack_usint(int(res[2]['file_number'])),
PCCC_DATA_TYPE[res[2]['file_type']],
pack_usint(int(res[2]['element_number'])),
pack_usint(sub_element)
]
self.logger.debug("SLC write_tag({0},{1})".format(tag, value))
if self.send_unit_data(
build_common_packet_format(
DATA_ITEM['Connected'],
''.join(message_request) + data_to_write,
ADDRESS_ITEM['Connection Based'],
addr_data=self._target_cid,)):
sts = int(unpack_usint(self._reply[58]))
try:
if sts != 0:
sts_txt = PCCC_ERROR_CODE[sts]
self._status = (1000, "Error({0}) returned from SLC write_tag({1},{2})".format(sts_txt, tag, value))
self.logger.warning(self._status)
raise DataError("Error({0}) returned from SLC write_tag({1},{2})".format(sts_txt, tag, value))
return True
except Exception as e:
self._status = (1000, "Error({0}) parsing the data returned from "
"SLC write_tag({1},{2})".format(e, tag, value))
self.logger.warning(self._status)
raise DataError("Error({0}) parsing the data returned from "
"SLC write_tag({1},{2})".format(e, tag, value))
else:
raise DataError("send_unit_data returned not valid data")

View File

@@ -0,0 +1 @@
__author__ = 'agostino'

View File

@@ -0,0 +1,827 @@
# -*- coding: utf-8 -*-
#
# cip_base.py - A set of classes methods and structures used to implement Ethernet/IP
#
#
# Copyright (c) 2014 Agostino Ruscito <ruscito@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
import struct
import socket
from os import getpid
from pycomm_micro.cip.cip_const import *
from pycomm_micro.common import PycommError
class CommError(PycommError):
pass
class DataError(PycommError):
pass
def pack_sint(n):
return struct.pack('b', n)
def pack_usint(n):
return struct.pack('B', n)
def pack_int(n):
"""pack 16 bit into 2 bytes little endian"""
return struct.pack('<h', n)
def pack_uint(n):
"""pack 16 bit into 2 bytes little endian"""
# print("N: {0}".format(n))
return struct.pack('<H', n)
def pack_dint(n):
"""pack 32 bit into 4 bytes little endian"""
return struct.pack('<i', n)
def pack_real(r):
"""unpack 4 bytes little endian to int"""
return struct.pack('<f', r)
def pack_lint(l):
"""unpack 4 bytes little endian to int"""
return struct.unpack('<q', l)
def unpack_bool(st):
if int(struct.unpack('B', st[0])[0]) == 0:
return 0
return 1
def unpack_sint(st):
return int(struct.unpack('b', st[0])[0])
def unpack_usint(st):
return int(struct.unpack('B', st[0])[0])
def unpack_int(st):
"""unpack 2 bytes little endian to int"""
return int(struct.unpack('<h', st[0:2])[0])
def unpack_uint(st):
"""unpack 2 bytes little endian to int"""
return int(struct.unpack('<H', st[0:2])[0])
def unpack_dint(st):
"""unpack 4 bytes little endian to int"""
return int(struct.unpack('<i', st[0:4])[0])
def unpack_real(st):
"""unpack 4 bytes little endian to int"""
return float(struct.unpack('<f', st[0:4])[0])
def unpack_lreal(st):
"""unpack 8 bytes little endian to int"""
return float(struct.unpack('<f', st[0:8])[0])
def unpack_lint(st):
"""unpack 4 bytes little endian to int"""
return int(struct.unpack('<q', st[0:8])[0])
def get_bit(value, idx):
""":returns value of bit at position idx"""
return (value & (1 << idx)) != 0
PACK_DATA_FUNCTION = {
'BOOL': pack_sint,
'SINT': pack_sint, # Signed 8-bit integer
'INT': pack_int, # Signed 16-bit integer
'UINT': pack_uint, # Unsigned 16-bit integer
'USINT': pack_usint, # Unsigned 8-bit integer
'DINT': pack_dint, # Signed 32-bit integer
'REAL': pack_real, # 32-bit floating point
'LREAL': pack_real, # 32-bit floating point
'LINT': pack_lint,
'BYTE': pack_sint, # byte string 8-bits
'WORD': pack_uint, # byte string 16-bits
'DWORD': pack_dint, # byte string 32-bits
'LWORD': pack_lint # byte string 64-bits
}
UNPACK_DATA_FUNCTION = {
'BOOL': unpack_bool,
'SINT': unpack_sint, # Signed 8-bit integer
'INT': unpack_int, # Signed 16-bit integer
'UINT': unpack_uint, # Unsigned 16-bit
'USINT': unpack_usint, # Unsigned 8-bit integer
'DINT': unpack_dint, # Signed 32-bit integer
'UDINT': unpack_dint, # Signed 32-bit integer
'REAL': unpack_real, # 32-bit floating point,
'LREAL': unpack_lreal, # 32-bit floating point,
'LINT': unpack_lint,
'BYTE': unpack_sint, # byte string 8-bits
'WORD': unpack_uint, # byte string 16-bits
'DWORD': unpack_dint, # byte string 32-bits
'LWORD': unpack_lint # byte string 64-bits
}
DATA_FUNCTION_SIZE = {
'BOOL': 1,
'SINT': 1, # Signed 8-bit integer
'INT': 2, # Signed 16-bit integer
'UINT': 2, # Unsigned 16-bit integer
'DINT': 4, # Signed 32-bit integer
'REAL': 4, # 32-bit floating point
'LINT': 8,
'BYTE': 1, # byte string 8-bits
'WORD': 2, # byte string 16-bits
'DWORD': 4, # byte string 32-bits
'LWORD': 8 # byte string 64-bits
}
UNPACK_PCCC_DATA_FUNCTION = {
'N': unpack_int,
'B': unpack_int,
'T': unpack_int,
'C': unpack_int,
'S': unpack_int,
'F': unpack_real,
'A': unpack_sint,
'R': unpack_dint,
'O': unpack_int,
'I': unpack_int
}
PACK_PCCC_DATA_FUNCTION = {
'N': pack_int,
'B': pack_int,
'T': pack_int,
'C': pack_int,
'S': pack_int,
'F': pack_real,
'A': pack_sint,
'R': pack_dint,
'O': pack_int,
'I': pack_int
}
def print_bytes_line(msg):
out = ''
for ch in msg:
out += "{:0>2x}".format(ord(ch))
return out
def print_bytes_msg(msg, info=''):
out = info
new_line = True
line = 0
column = 0
for idx, ch in enumerate(msg):
if new_line:
out += "\n({:0>4d}) ".format(line * 10)
new_line = False
out += "{:0>2x} ".format(ord(ch))
if column == 9:
new_line = True
column = 0
line += 1
else:
column += 1
return out
def get_extended_status(msg, start):
status = unpack_usint(msg[start:start+1])
# send_rr_data
# 42 General Status
# 43 Size of additional status
# 44..n additional status
# send_unit_data
# 48 General Status
# 49 Size of additional status
# 50..n additional status
extended_status_size = (unpack_usint(msg[start+1:start+2]))*2
extended_status = 0
if extended_status_size != 0:
# There is an additional status
if extended_status_size == 1:
extended_status = unpack_usint(msg[start+2:start+3])
elif extended_status_size == 2:
extended_status = unpack_uint(msg[start+2:start+4])
elif extended_status_size == 4:
extended_status = unpack_dint(msg[start+2:start+6])
else:
return 'Extended Status Size Unknown'
try:
return '{0}'.format(EXTEND_CODES[status][extended_status])
except LookupError:
return "Extended Status info not present"
def create_tag_rp(tag, multi_requests=False):
""" Create tag Request Packet
It returns the request packed wrapped around the tag passed.
If any error it returns none
"""
tags = tag.split('.')
rp = []
index = []
for tag in tags:
add_index = False
# Check if is an array tag
if tag.find('[') != -1:
# Remove the last square bracket
tag = tag[:len(tag)-1]
# Isolate the value inside bracket
inside_value = tag[tag.find('[')+1:]
# Now split the inside value in case part of multidimensional array
index = inside_value.split(',')
# Flag the existence of one o more index
add_index = True
# Get only the tag part
tag = tag[:tag.find('[')]
tag_length = len(tag)
# Create the request path
rp.append(EXTENDED_SYMBOL) # ANSI Ext. symbolic segment
rp.append(chr(tag_length)) # Length of the tag
# Add the tag to the Request path
for char in tag:
rp.append(char)
# Add pad byte because total length of Request path must be word-aligned
if tag_length % 2:
rp.append(PADDING_BYTE)
# Add any index
if add_index:
for idx in index:
val = int(idx)
if val <= 0xff:
rp.append(ELEMENT_ID["8-bit"])
rp.append(pack_usint(val))
elif val <= 0xffff:
rp.append(ELEMENT_ID["16-bit"]+PADDING_BYTE)
rp.append(pack_uint(val))
elif val <= 0xfffffffff:
rp.append(ELEMENT_ID["32-bit"]+PADDING_BYTE)
rp.append(pack_dint(val))
else:
# Cannot create a valid request packet
return None
# At this point the Request Path is completed,
if multi_requests:
request_path = chr(len(rp)/2) + ''.join(rp)
else:
request_path = ''.join(rp)
return request_path
def build_common_packet_format(message_type, message, addr_type, addr_data=None, timeout=10):
""" build_common_packet_format
It creates the common part for a CIP message. Check Volume 2 (page 2.22) of CIP specification for reference
"""
msg = pack_dint(0) # Interface Handle: shall be 0 for CIP
msg += pack_uint(timeout) # timeout
msg += pack_uint(2) # Item count: should be at list 2 (Address and Data)
msg += addr_type # Address Item Type ID
if addr_data is not None:
msg += pack_uint(len(addr_data)) # Address Item Length
msg += addr_data
else:
msg += pack_uint(0) # Address Item Length
msg += message_type # Data Type ID
msg += pack_uint(len(message)) # Data Item Length
msg += message
return msg
def build_multiple_service(rp_list, sequence=None):
mr = []
if sequence is not None:
mr.append(pack_uint(sequence))
mr.append(chr(TAG_SERVICES_REQUEST["Multiple Service Packet"])) # the Request Service
mr.append(pack_usint(2)) # the Request Path Size length in word
mr.append(CLASS_ID["8-bit"])
mr.append(CLASS_CODE["Message Router"])
mr.append(INSTANCE_ID["8-bit"])
mr.append(pack_usint(1)) # Instance 1
mr.append(pack_uint(len(rp_list))) # Number of service contained in the request
# Offset calculation
offset = (len(rp_list) * 2) + 2
for index, rp in enumerate(rp_list):
if index == 0:
mr.append(pack_uint(offset)) # Starting offset
else:
mr.append(pack_uint(offset))
offset += len(rp)
for rp in rp_list:
mr.append(rp)
return mr
def parse_multiple_request(message, tags, typ):
""" parse_multi_request
This function should be used to parse the message replayed to a multi request service rapped around the
send_unit_data message.
:param message: the full message returned from the PLC
:param tags: The list of tags to be read
:param typ: to specify if multi request service READ or WRITE
:return: a list of tuple in the format [ (tag name, value, data type), ( tag name, value, data type) ].
In case of error the tuple will be (tag name, None, None)
"""
offset = 50
position = 50
number_of_service_replies = unpack_uint(message[offset:offset+2])
tag_list = []
for index in range(number_of_service_replies):
position += 2
start = offset + unpack_uint(message[position:position+2])
general_status = unpack_usint(message[start+2:start+3])
if general_status == 0:
if typ == "READ":
data_type = unpack_uint(message[start+4:start+6])
try:
value_begin = start + 6
value_end = value_begin + DATA_FUNCTION_SIZE[I_DATA_TYPE[data_type]]
value = message[value_begin:value_end]
tag_list.append((tags[index],
UNPACK_DATA_FUNCTION[I_DATA_TYPE[data_type]](value),
I_DATA_TYPE[data_type]))
except LookupError:
tag_list.append((tags[index], None, None))
else:
tag_list.append((tags[index] + ('GOOD',)))
else:
if typ == "READ":
tag_list.append((tags[index], None, None))
else:
tag_list.append((tags[index] + ('BAD',)))
return tag_list
class Socket:
def __init__(self, timeout=5.0):
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.settimeout(timeout)
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
def connect(self, host, port):
try:
self.sock.connect((host, port))
except socket.timeout:
raise CommError("Socket timeout during connection.")
def send(self, msg, timeout=0):
if timeout != 0:
self.sock.settimeout(timeout)
total_sent = 0
while total_sent < len(msg):
try:
sent = self.sock.send(msg[total_sent:])
if sent == 0:
raise CommError("socket connection broken.")
total_sent += sent
except socket.error:
raise CommError("socket connection broken.")
return total_sent
def receive(self, timeout=0):
if timeout != 0:
self.sock.settimeout(timeout)
msg_len = 28
chunks = []
bytes_recd = 0
one_shot = True
while bytes_recd < msg_len:
try:
chunk = self.sock.recv(min(msg_len - bytes_recd, 2048))
if chunk == '':
raise CommError("socket connection broken.")
if one_shot:
data_size = int(struct.unpack('<H', chunk[2:4])[0]) # Length
msg_len = HEADER_SIZE + data_size
one_shot = False
chunks.append(chunk)
bytes_recd += len(chunk)
except socket.error as e:
raise CommError(e)
return ''.join(chunks)
def close(self):
self.sock.close()
def parse_symbol_type(symbol):
""" parse_symbol_type
It parse the symbol to Rockwell Spec
:param symbol: the symbol associated to a tag
:return: A tuple containing information about the tag
"""
pass
return None
class Base(object):
_sequence = 0
def __init__(self, logging):
if Base._sequence == 0:
Base._sequence = getpid()
else:
Base._sequence = Base._get_sequence()
self.logger = logging
self.__version__ = '0.1'
self.__sock = None
self._session = 0
self._connection_opened = False
self._reply = None
self._message = None
self._target_cid = None
self._target_is_connected = False
self._tag_list = []
self._buffer = {}
self._device_description = "Device Unknown"
self._last_instance = 0
self._byte_offset = 0
self._last_position = 0
self._more_packets_available = False
self._last_tag_read = ()
self._last_tag_write = ()
self._status = (0, "")
# self.attribs = {'context': '_pycomm_', 'protocol version': 1, 'rpi': 5000, 'port': 0xAF12, 'timeout': 10,
# 'backplane': 1, 'cpu slot': 0, 'option': 0, 'cid': '\x27\x04\x19\x71', 'csn': '\x27\x04',
# 'vid': '\x09\x10', 'vsn': '\x09\x10\x19\x71', 'name': 'Base', 'ip address': None}
self.attribs = {'context': '_pycomm_', 'protocol version': 1, 'rpi': 5000, 'port': 0xAF12, 'timeout': 10,
'backplane': 0, 'cpu slot': 0, 'option': 0, 'cid': '\x27\x04\x19\x71', 'csn': '\x27\x04',
'vid': '\x09\x10', 'vsn': '\x09\x10\x19\x71', 'name': 'Base', 'ip address': None}
def __len__(self):
return len(self.attribs)
def __getitem__(self, key):
return self.attribs[key]
def __setitem__(self, key, value):
self.attribs[key] = value
def __delitem__(self, key):
try:
del self.attribs[key]
except LookupError:
pass
def __iter__(self):
return iter(self.attribs)
def __contains__(self, item):
return item in self.attribs
def _check_reply(self):
raise Socket.ImplementationError("The method has not been implemented")
@staticmethod
def _get_sequence():
""" Increase and return the sequence used with connected messages
:return: The New sequence
"""
if Base._sequence < 65535:
Base._sequence += 1
else:
Base._sequence = getpid()
return Base._sequence
def nop(self):
""" No replay command
A NOP provides a way for either an originator or target to determine if the TCP connection is still open.
"""
self._message = self.build_header(ENCAPSULATION_COMMAND['nop'], 0)
self._send()
def __repr__(self):
return self._device_description
def description(self):
return self._device_description
def list_identity(self):
""" ListIdentity command to locate and identify potential target
return true if the replay contains the device description
"""
self._message = self.build_header(ENCAPSULATION_COMMAND['list_identity'], 0)
self._send()
self._receive()
if self._check_reply():
try:
self._device_description = self._reply[63:-1]
return True
except Exception as e:
raise CommError(e)
return False
def send_rr_data(self, msg):
""" SendRRData transfer an encapsulated request/reply packet between the originator and target
:param msg: The message to be send to the target
:return: the replay received from the target
"""
self._message = self.build_header(ENCAPSULATION_COMMAND["send_rr_data"], len(msg))
self._message += msg
self._send()
self._receive()
return self._check_reply()
def send_unit_data(self, msg):
""" SendUnitData send encapsulated connected messages.
:param msg: The message to be send to the target
:return: the replay received from the target
"""
self._message = self.build_header(ENCAPSULATION_COMMAND["send_unit_data"], len(msg))
self._message += msg
self._send()
self._receive()
return self._check_reply()
def get_status(self):
""" Get the last status/error
This method can be used after any call to get any details in case of error
:return: A tuple containing (error group, error message)
"""
return self._status
def clear(self):
""" Clear the last status/error
:return: return am empty tuple
"""
self._status = (0, "")
def build_header(self, command, length):
""" Build the encapsulate message header
The header is 24 bytes fixed length, and includes the command and the length of the optional data portion.
:return: the headre
"""
try:
h = command # Command UINT
h += pack_uint(length) # Length UINT
h += pack_dint(self._session) # Session Handle UDINT
h += pack_dint(0) # Status UDINT
h += self.attribs['context'] # Sender Context 8 bytes
h += pack_dint(self.attribs['option']) # Option UDINT
return h
except Exception as e:
raise CommError(e)
def register_session(self):
""" Register a new session with the communication partner
:return: None if any error, otherwise return the session number
"""
if self._session:
return self._session
self._session = 0
self._message = self.build_header(ENCAPSULATION_COMMAND['register_session'], 4)
self._message += pack_uint(self.attribs['protocol version'])
self._message += pack_uint(0)
self._send()
self._receive()
if self._check_reply():
self._session = unpack_dint(self._reply[4:8])
self.logger.debug("Session ={0} has been registered.".format(print_bytes_line(self._reply[4:8])))
return self._session
self._status = 'Warning ! the session has not been registered.'
self.logger.warning(self._status)
return None
def forward_open(self):
""" CIP implementation of the forward open message
Refer to ODVA documentation Volume 1 3-5.5.2
:return: False if any error in the replayed message
"""
if self._session == 0:
self._status = (4, "A session need to be registered before to call forward_open.")
raise CommError("A session need to be registered before to call forward open")
forward_open_msg = [
FORWARD_OPEN,
pack_usint(2),
CLASS_ID["8-bit"],
CLASS_CODE["Connection Manager"], # Volume 1: 5-1
INSTANCE_ID["8-bit"],
CONNECTION_MANAGER_INSTANCE['Open Request'],
PRIORITY,
TIMEOUT_TICKS,
pack_dint(0),
self.attribs['cid'],
self.attribs['csn'],
self.attribs['vid'],
self.attribs['vsn'],
TIMEOUT_MULTIPLIER,
'\x00\x00\x00',
pack_dint(self.attribs['rpi'] * 1000),
pack_uint(CONNECTION_PARAMETER['Default']),
pack_dint(self.attribs['rpi'] * 1000),
pack_uint(CONNECTION_PARAMETER['Default']),
TRANSPORT_CLASS, # Transport Class
# CONNECTION_SIZE['Backplane'],
CONNECTION_SIZE['Direct Network'],
# pack_usint(self.attribs['backplane']),
# pack_usint(self.attribs['cpu slot']),
CLASS_ID["8-bit"],
CLASS_CODE["Message Router"],
INSTANCE_ID["8-bit"],
pack_usint(1)
]
if self.send_rr_data(
build_common_packet_format(DATA_ITEM['Unconnected'], ''.join(forward_open_msg), ADDRESS_ITEM['UCMM'],)):
self._target_cid = self._reply[44:48]
self._target_is_connected = True
return True
self._status = (4, "forward_open returned False")
return False
def forward_close(self):
""" CIP implementation of the forward close message
Each connection opened with the froward open message need to be closed.
Refer to ODVA documentation Volume 1 3-5.5.3
:return: False if any error in the replayed message
"""
if self._session == 0:
self._status = (5, "A session need to be registered before to call forward_close.")
raise CommError("A session need to be registered before to call forward_close.")
# print ("Backplane:{0}\nCPU:{1}".format(self.attribs['backplane'], self.attribs['cpu slot']))
forward_close_msg = [
FORWARD_CLOSE,
pack_usint(2),
CLASS_ID["8-bit"],
CLASS_CODE["Connection Manager"], # Volume 1: 5-1
INSTANCE_ID["8-bit"],
CONNECTION_MANAGER_INSTANCE['Open Request'],
PRIORITY,
TIMEOUT_TICKS,
self.attribs['csn'],
self.attribs['vid'],
self.attribs['vsn'],
CONNECTION_SIZE['Direct Network'],
# CONNECTION_SIZE['Backplane'],
'\x00', # Reserved
# pack_usint(self.attribs['backplane']),
# pack_usint(self.attribs['cpu slot']),
CLASS_ID["8-bit"],
CLASS_CODE["Message Router"],
INSTANCE_ID["8-bit"],
pack_usint(1)
]
if self.send_rr_data(
build_common_packet_format(DATA_ITEM['Unconnected'], ''.join(forward_close_msg), ADDRESS_ITEM['UCMM'])):
self._target_is_connected = False
return True
self._status = (5, "forward_close returned False")
self.logger.warning(self._status)
return False
def un_register_session(self):
""" Un-register a connection
"""
self._message = self.build_header(ENCAPSULATION_COMMAND['unregister_session'], 0)
self._send()
self._session = None
def _send(self):
"""
socket send
:return: true if no error otherwise false
"""
try:
self.logger.debug(print_bytes_msg(self._message, '-------------- SEND --------------'))
self.__sock.send(self._message)
except Exception as e:
#self.clean_up()
raise CommError(e)
def _receive(self):
"""
socket receive
:return: true if no error otherwise false
"""
try:
self._reply = self.__sock.receive()
self.logger.debug(print_bytes_msg(self._reply, '----------- RECEIVE -----------'))
except Exception as e:
#self.clean_up()
raise CommError(e)
def open(self, ip_address):
"""
socket open
:return: true if no error otherwise false
"""
# handle the socket layer
if not self._connection_opened:
try:
if self.__sock is None:
self.__sock = Socket()
self.__sock.connect(ip_address, self.attribs['port'])
self._connection_opened = True
self.attribs['ip address'] = ip_address
if self.register_session() is None:
self._status = (13, "Session not registered")
return False
self.forward_close()
return True
except Exception as e:
#self.clean_up()
raise CommError(e)
def close(self):
"""
socket close
:return: true if no error otherwise false
"""
try:
if self._target_is_connected:
self.forward_close()
if self._session != 0:
self.un_register_session()
if self.__sock:
self.__sock.close()
except Exception as e:
raise CommError(e)
self.clean_up()
def clean_up(self):
self.__sock = None
self._target_is_connected = False
self._session = 0
self._connection_opened = False
def is_connected(self):
return self._connection_opened

View File

@@ -0,0 +1,482 @@
# -*- coding: utf-8 -*-
#
# cip_const.py - A set of structures and constants used to implement the Ethernet/IP protocol
#
#
# Copyright (c) 2014 Agostino Ruscito <ruscito@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
ELEMENT_ID = {
"8-bit": '\x28',
"16-bit": '\x29',
"32-bit": '\x2a'
}
CLASS_ID = {
"8-bit": '\x20',
"16-bit": '\x21',
}
INSTANCE_ID = {
"8-bit": '\x24',
"16-bit": '\x25'
}
ATTRIBUTE_ID = {
"8-bit": '\x30',
"16-bit": '\x31'
}
# Path are combined as:
# CLASS_ID + PATHS
# For example PCCC path is CLASS_ID["8-bit"]+PATH["PCCC"] -> 0x20, 0x67, 0x24, 0x01.
PATH = {
'Connection Manager': '\x06\x24\x01',
'Router': '\x02\x24\x01',
'Backplane Data Type': '\x66\x24\x01',
'PCCC': '\x67\x24\x01',
'DHCP Channel A': '\xa6\x24\x01\x01\x2c\x01',
'DHCP Channel B': '\xa6\x24\x01\x02\x2c\x01'
}
ENCAPSULATION_COMMAND = { # Volume 2: 2-3.2 Command Field UINT 2 byte
"nop": '\x00\x00',
"list_targets": '\x01\x00',
"list_services": '\x04\x00',
"list_identity": '\x63\x00',
"list_interfaces": '\x64\x00',
"register_session": '\x65\x00',
"unregister_session": '\x66\x00',
"send_rr_data": '\x6F\x00',
"send_unit_data": '\x70\x00'
}
"""
When a tag is created, an instance of the Symbol Object (Class ID 0x6B) is created
inside the controller.
When a UDT is created, an instance of the Template object (Class ID 0x6C) is
created to hold information about the structure makeup.
"""
CLASS_CODE = {
"Message Router": '\x02', # Volume 1: 5-1
"Symbol Object": '\x6b',
"Template Object": '\x6c',
"Connection Manager": '\x06' # Volume 1: 3-5
}
CONNECTION_MANAGER_INSTANCE = {
'Open Request': '\x01',
'Open Format Rejected': '\x02',
'Open Resource Rejected': '\x03',
'Open Other Rejected': '\x04',
'Close Request': '\x05',
'Close Format Request': '\x06',
'Close Other Request': '\x07',
'Connection Timeout': '\x08'
}
TAG_SERVICES_REQUEST = {
"Read Tag": 0x4c,
"Read Tag Fragmented": 0x52,
"Write Tag": 0x4d,
"Write Tag Fragmented": 0x53,
"Read Modify Write Tag": 0x4e,
"Multiple Service Packet": 0x0a,
"Get Instance Attributes List": 0x55,
"Get Attributes": 0x03,
"Read Template": 0x4c,
}
TAG_SERVICES_REPLY = {
0xcc: "Read Tag",
0xd2: "Read Tag Fragmented",
0xcd: "Write Tag",
0xd3: "Write Tag Fragmented",
0xce: "Read Modify Write Tag",
0x8a: "Multiple Service Packet",
0xd5: "Get Instance Attributes List",
0x83: "Get Attributes",
0xcc: "Read Template"
}
I_TAG_SERVICES_REPLY = {
"Read Tag": 0xcc,
"Read Tag Fragmented": 0xd2,
"Write Tag": 0xcd,
"Write Tag Fragmented": 0xd3,
"Read Modify Write Tag": 0xce,
"Multiple Service Packet": 0x8a,
"Get Instance Attributes List": 0xd5,
"Get Attributes": 0x83,
"Read Template": 0xcc
}
"""
EtherNet/IP Encapsulation Error Codes
Standard CIP Encapsulation Error returned in the cip message header
"""
STATUS = {
0x0000: "Success",
0x0001: "The sender issued an invalid or unsupported encapsulation command",
0x0002: "Insufficient memory",
0x0003: "Poorly formed or incorrect data in the data portion",
0x0064: "An originator used an invalid session handle when sending an encapsulation message to the target",
0x0065: "The target received a message of invalid length",
0x0069: "Unsupported Protocol Version"
}
"""
MSG Error Codes:
The following error codes have been taken from:
Rockwell Automation Publication
1756-RM003P-EN-P - December 2014
"""
SERVICE_STATUS = {
0x01: "Connection failure (see extended status)",
0x02: "Insufficient resource",
0x03: "Invalid value",
0x04: "IOI syntax error. A syntax error was detected decoding the Request Path (see extended status)",
0x05: "Destination unknown, class unsupported, instance \nundefined or structure element undefined (see extended status)",
0x06: "Insufficient Packet Space",
0x07: "Connection lost",
0x08: "Service not supported",
0x09: "Error in data segment or invalid attribute value",
0x0A: "Attribute list error",
0x0B: "State already exist",
0x0C: "Object state conflict",
0x0D: "Object already exist",
0x0E: "Attribute not settable",
0x0F: "Permission denied",
0x10: "Device state conflict",
0x11: "Reply data too large",
0x12: "Fragmentation of a primitive value",
0x13: "Insufficient command data",
0x14: "Attribute not supported",
0x15: "Too much data",
0x1A: "Bridge request too large",
0x1B: "Bridge response too large",
0x1C: "Attribute list shortage",
0x1D: "Invalid attribute list",
0x1E: "Request service error",
0x1F: "Connection related failure (see extended status)",
0x22: "Invalid reply received",
0x25: "Key segment error",
0x26: "Invalid IOI error",
0x27: "Unexpected attribute in list",
0x28: "DeviceNet error - invalid member ID",
0x29: "DeviceNet error - member not settable",
0xD1: "Module not in run state",
0xFB: "Message port not supported",
0xFC: "Message unsupported data type",
0xFD: "Message uninitialized",
0xFE: "Message timeout",
0xff: "General Error (see extended status)"
}
EXTEND_CODES = {
0x01: {
0x0100: "Connection in use",
0x0103: "Transport not supported",
0x0106: "Ownership conflict",
0x0107: "Connection not found",
0x0108: "Invalid connection type",
0x0109: "Invalid connection size",
0x0110: "Module not configured",
0x0111: "EPR not supported",
0x0114: "Wrong module",
0x0115: "Wrong device type",
0x0116: "Wrong revision",
0x0118: "Invalid configuration format",
0x011A: "Application out of connections",
0x0203: "Connection timeout",
0x0204: "Unconnected message timeout",
0x0205: "Unconnected send parameter error",
0x0206: "Message too large",
0x0301: "No buffer memory",
0x0302: "Bandwidth not available",
0x0303: "No screeners available",
0x0305: "Signature match",
0x0311: "Port not available",
0x0312: "Link address not available",
0x0315: "Invalid segment type",
0x0317: "Connection not scheduled"
},
0x04: {
0x0000: "Extended status out of memory",
0x0001: "Extended status out of instances"
},
0x05: {
0x0000: "Extended status out of memory",
0x0001: "Extended status out of instances"
},
0x1F: {
0x0203: "Connection timeout"
},
0xff: {
0x7: "Wrong data type",
0x2001: "Excessive IOI",
0x2002: "Bad parameter value",
0x2018: "Semaphore reject",
0x201B: "Size too small",
0x201C: "Invalid size",
0x2100: "Privilege failure",
0x2101: "Invalid keyswitch position",
0x2102: "Password invalid",
0x2103: "No password issued",
0x2104: "Address out of range",
0x2105: "Address and how many out of range",
0x2106: "Data in use",
0x2107: "Type is invalid or not supported",
0x2108: "Controller in upload or download mode",
0x2109: "Attempt to change number of array dimensions",
0x210A: "Invalid symbol name",
0x210B: "Symbol does not exist",
0x210E: "Search failed",
0x210F: "Task cannot start",
0x2110: "Unable to write",
0x2111: "Unable to read",
0x2112: "Shared routine not editable",
0x2113: "Controller in faulted mode",
0x2114: "Run mode inhibited"
}
}
DATA_ITEM = {
'Connected': '\xb1\x00',
'Unconnected': '\xb2\x00'
}
ADDRESS_ITEM = {
'Connection Based': '\xa1\x00',
'Null': '\x00\x00',
'UCMM': '\x00\x00'
}
UCMM = {
'Interface Handle': 0,
'Item Count': 2,
'Address Type ID': 0,
'Address Length': 0,
'Data Type ID': 0x00b2
}
CONNECTION_SIZE = {
'Backplane': '\x03', # CLX
'Direct Network': '\x02'
}
HEADER_SIZE = 24
EXTENDED_SYMBOL = '\x91'
BOOL_ONE = 0xff
REQUEST_SERVICE = 0
REQUEST_PATH_SIZE = 1
REQUEST_PATH = 2
SUCCESS = 0
INSUFFICIENT_PACKETS = 6
OFFSET_MESSAGE_REQUEST = 40
FORWARD_CLOSE = '\x4e'
UNCONNECTED_SEND = '\x52'
FORWARD_OPEN = '\x54'
LARGE_FORWARD_OPEN = '\x5b'
GET_CONNECTION_DATA = '\x56'
SEARCH_CONNECTION_DATA = '\x57'
GET_CONNECTION_OWNER = '\x5a'
MR_SERVICE_SIZE = 2
PADDING_BYTE = '\x00'
PRIORITY = '\x0a'
TIMEOUT_TICKS = '\x05'
TIMEOUT_MULTIPLIER = '\x01'
TRANSPORT_CLASS = '\xa3'
CONNECTION_PARAMETER = {
'PLC5': 0x4302,
'SLC500': 0x4302,
'CNET': 0x4320,
'DHP': 0x4302,
'Default': 0x43f8,
}
"""
Atomic Data Type:
Bit = Bool
Bit array = DWORD (32-bit boolean aray)
8-bit integer = SINT
16-bit integer = UINT
32-bit integer = DINT
32-bit float = REAL
64-bit integer = LINT
From Rockwell Automation Publication 1756-PM020C-EN-P November 2012:
When reading a BOOL tag, the values returned for 0 and 1 are 0 and 0xff, respectively.
"""
S_DATA_TYPE = {
'BOOL': 0xc1,
'SINT': 0xc2, # Signed 8-bit integer
'INT': 0xc3, # Signed 16-bit integer
'DINT': 0xc4, # Signed 32-bit integer
'LINT': 0xc5, # Signed 64-bit integer
'USINT': 0xc6, # Unsigned 8-bit integer
'UINT': 0xc7, # Unsigned 16-bit integer
'UDINT': 0xc8, # Unsigned 32-bit integer
'ULINT': 0xc9, # Unsigned 64-bit integer
'REAL': 0xca, # 32-bit floating point
'LREAL': 0xcb, # 64-bit floating point
'STIME': 0xcc, # Synchronous time
'DATE': 0xcd,
'TIME_OF_DAY': 0xce,
'DATE_AND_TIME': 0xcf,
'STRING': 0xd0, # character string (1 byte per character)
'BYTE': 0xd1, # byte string 8-bits
'WORD': 0xd2, # byte string 16-bits
'DWORD': 0xd3, # byte string 32-bits
'LWORD': 0xd4, # byte string 64-bits
'STRING2': 0xd5, # character string (2 byte per character)
'FTIME': 0xd6, # Duration high resolution
'LTIME': 0xd7, # Duration long
'ITIME': 0xd8, # Duration short
'STRINGN': 0xd9, # character string (n byte per character)
'SHORT_STRING': 0xda, # character string (1 byte per character, 1 byte length indicator)
'TIME': 0xdb, # Duration in milliseconds
'EPATH': 0xdc, # CIP Path segment
'ENGUNIT': 0xdd, # Engineering Units
'STRINGI': 0xde # International character string
}
I_DATA_TYPE = {
0xc1: 'BOOL',
0xc2: 'SINT', # Signed 8-bit integer
0xc3: 'INT', # Signed 16-bit integer
0xc4: 'DINT', # Signed 32-bit integer
0xc5: 'LINT', # Signed 64-bit integer
0xc6: 'USINT', # Unsigned 8-bit integer
0xc7: 'UINT', # Unsigned 16-bit integer
0xc8: 'UDINT', # Unsigned 32-bit integer
0xc9: 'ULINT', # Unsigned 64-bit integer
0xca: 'REAL', # 32-bit floating point
0xcb: 'LREAL', # 64-bit floating point
0xcc: 'STIME', # Synchronous time
0xcd: 'DATE',
0xce: 'TIME_OF_DAY',
0xcf: 'DATE_AND_TIME',
0xd0: 'STRING', # character string (1 byte per character)
0xd1: 'BYTE', # byte string 8-bits
0xd2: 'WORD', # byte string 16-bits
0xd3: 'DWORD', # byte string 32-bits
0xd4: 'LWORD', # byte string 64-bits
0xd5: 'STRING2', # character string (2 byte per character)
0xd6: 'FTIME', # Duration high resolution
0xd7: 'LTIME', # Duration long
0xd8: 'ITIME', # Duration short
0xd9: 'STRINGN', # character string (n byte per character)
0xda: 'SHORT_STRING', # character string (1 byte per character, 1 byte length indicator)
0xdb: 'TIME', # Duration in milliseconds
0xdc: 'EPATH', # CIP Path segment
0xdd: 'ENGUNIT', # Engineering Units
0xde: 'STRINGI' # International character string
}
REPLAY_INFO = {
0x4e: 'FORWARD_CLOSE (4E,00)',
0x52: 'UNCONNECTED_SEND (52,00)',
0x54: 'FORWARD_OPEN (54,00)',
0x6f: 'send_rr_data (6F,00)',
0x70: 'send_unit_data (70,00)',
0x00: 'nop',
0x01: 'list_targets',
0x04: 'list_services',
0x63: 'list_identity',
0x64: 'list_interfaces',
0x65: 'register_session',
0x66: 'unregister_session',
}
PCCC_DATA_TYPE = {
'N': '\x89',
'B': '\x85',
'T': '\x86',
'C': '\x87',
'S': '\x84',
'F': '\x8a',
'ST': '\x8d',
'A': '\x8e',
'R': '\x88',
'O': '\x8b',
'I': '\x8c'
}
PCCC_DATA_SIZE = {
'N': 2,
'B': 2,
'T': 6,
'C': 6,
'S': 2,
'F': 4,
'ST': 84,
'A': 2,
'R': 6,
'O': 2,
'I': 2
}
PCCC_CT = {
'PRE': 1,
'ACC': 2,
'EN': 15,
'TT': 14,
'DN': 13,
'CU': 15,
'CD': 14,
'OV': 12,
'UN': 11,
'UA': 10
}
PCCC_ERROR_CODE = {
-2: "Not Acknowledged (NAK)",
-3: "No Reponse, Check COM Settings",
-4: "Unknown Message from DataLink Layer",
-5: "Invalid Address",
-6: "Could Not Open Com Port",
-7: "No data specified to data link layer",
-8: "No data returned from PLC",
-20: "No Data Returned",
16: "Illegal Command or Format, Address may not exist or not enough elements in data file",
32: "PLC Has a Problem and Will Not Communicate",
48: "Remote Node Host is Missing, Disconnected, or Shut Down",
64: "Host Could Not Complete Function Due To Hardware Fault",
80: "Addressing problem or Memory Protect Rungs",
96: "Function not allows due to command protection selection",
112: "Processor is in Program mode",
128: "Compatibility mode file missing or communication zone problem",
144: "Remote node cannot buffer command",
240: "Error code in EXT STS Byte"
}

View File

@@ -0,0 +1,32 @@
__author__ = 'Agostino Ruscito'
__version__ = "1.0.7"
__date__ = "08 03 2015"
import logging
logging.basicConfig(
filename="pycomm.log",
filemode='w',
level=logging.INFO,
format="%(name)-13s %(levelname)-10s %(asctime)s %(message)s",
# propagate=0,
)
LOGGER = logging.getLogger('pycomm')
class PycommError(Exception):
pass
def setup_logger(name, level, filename=None):
log = logging.getLogger('pycomm.'+name)
log.setLevel(level)
if filename:
fh = logging.FileHandler(filename, mode='w')
fh.setFormatter(logging.Formatter("%(levelname)-10s %(asctime)s %(message)s"))
log.addHandler(fh)
log.propagate = False
return log

3485
POCloud_Driver/tags.xml Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,727 @@
import pickle
channels = {
'vfd_nameplatehz':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'cfg_NameplateHz',
'last_time_uploaded':0,
'last_value':''
},
'mode_test':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'Test_Mode',
'last_time_uploaded':0,
'last_value':''
},
'alarm_temperature':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'ALARM_Temperature',
'last_time_uploaded':0,
'last_value':''
},
'dh_pressure_shutdown_limit':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_Pressure_Shutdown',
'last_time_uploaded':0,
'last_value':''
},
'dh_temp_startup_limit':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_Temp_Startup',
'last_time_uploaded':0,
'last_value':''
},
'vfd_fault_code':{
'data_type':'UINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'VFD_Fault_DriveFault_Code',
'last_time_uploaded':0,
'last_value':''
},
'dh_dischargetemperature':{
'data_type':'UINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_DischargeTemperature',
'last_time_uploaded':0,
'last_value':''
},
'dh_maxintakepressure_forever':{
'data_type':'UINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_MaxIntakePressure_Forever',
'last_time_uploaded':0,
'last_value':''
},
'alarm_pressure':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'ALARM_Pressure',
'last_time_uploaded':0,
'last_value':''
},
'dh_temp_startup_enabled':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'DH_Temp_Startup_Enabled',
'last_time_uploaded':0,
'last_value':''
},
'vfd_torqueperfmode':{
'data_type':'UINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'cfg_TorquePerfMode',
'last_time_uploaded':0,
'last_value':''
},
'dh_windingtemperature':{
'data_type':'UINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_WindingTemperature',
'last_time_uploaded':0,
'last_value':''
},
'vfd_motorpoles':{
'data_type':'UINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'cfg_MotorPoles',
'last_time_uploaded':0,
'last_value':''
},
'vfd_nameplatehp':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'cfg_NameplateHP',
'last_time_uploaded':0,
'last_value':''
},
'vfd_active':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'VFD_Active',
'last_time_uploaded':0,
'last_value':''
},
'rp_tubingpressure':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'RP_TubingPressure',
'last_time_uploaded':0,
'last_value':''
},
'vfd_speedfdbk':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'VFD_SpeedFdbk',
'last_time_uploaded':0,
'last_value':''
},
'rp_mode':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'RP_Mode',
'last_time_uploaded':0,
'last_value':''
},
'dh_maxintaketemperature_forever':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_MaxIntakeTemperature_Forever',
'last_time_uploaded':0,
'last_value':''
},
'sp_mode':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'SP_Mode',
'last_time_uploaded':0,
'last_value':''
},
'vfd_acceltime':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'VFD_AccelTime',
'last_time_uploaded':0,
'last_value':''
},
'tubingpressure_alarm_delay':{
'data_type':'UDINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'TubingPressure_Alarm_Delay',
'last_time_uploaded':0,
'last_value':''
},
'dh_tooltype':{
'data_type':'UINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_ToolType',
'last_time_uploaded':0,
'last_value':''
},
'sp_pressure':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'SP_Pressure',
'last_time_uploaded':0,
'last_value':''
},
'rp_trip':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'RP_Trip',
'last_time_uploaded':0,
'last_value':''
},
'alarm_mode':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'ALARM_Mode',
'last_time_uploaded':0,
'last_value':''
},
'stop_command':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'Stop_Command',
'last_time_uploaded':0,
'last_value':''
},
'tubingpressure_eu_min':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'TubingPressure_EU_Min',
'last_time_uploaded':0,
'last_value':''
},
'vfd_stopmode':{
'data_type':'UINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'cfg_StopMode',
'last_time_uploaded':0,
'last_value':''
},
'vfd_ready':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'VFD_Ready',
'last_time_uploaded':0,
'last_value':''
},
'off_mode':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'Off_Mode',
'last_time_uploaded':0,
'last_value':''
},
'alarm_tubingpressure':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'ALARM_TubingPressure',
'last_time_uploaded':0,
'last_value':''
},
'vfd_disabled':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'VFD_Disabled',
'last_time_uploaded':0,
'last_value':''
},
'tubingpressure_eu_max':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'TubingPressure_EU_Max',
'last_time_uploaded':0,
'last_value':''
},
'vfd_speedref':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'VFD_SpeedRef',
'last_time_uploaded':0,
'last_value':''
},
'dh_maxintaketemperature_startup':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_MaxIntakeTemperature_Startup',
'last_time_uploaded':0,
'last_value':''
},
'sp_temperature':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'SP_Temperature',
'last_time_uploaded':0,
'last_value':''
},
'dh_pressure_startup':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_Pressure_Startup',
'last_time_uploaded':0,
'last_value':''
},
'hand_mode':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'Hand_Mode',
'last_time_uploaded':0,
'last_value':''
},
'tubingpressure_lo':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'TubingPressure_Lo',
'last_time_uploaded':0,
'last_value':''
},
'run_permissive':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'Run_Permissive',
'last_time_uploaded':0,
'last_value':''
},
'vfd_maxfreq':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'cfg_MaxFreq',
'last_time_uploaded':0,
'last_value':''
},
'dh_temp_shutdown_enabled':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'DH_Temp_Shutdown_Enabled',
'last_time_uploaded':0,
'last_value':''
},
'sp_voltage':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'SP_Voltage',
'last_time_uploaded':0,
'last_value':''
},
'dh_pressure_shutdown_enabled':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'DH_Pressure_Shutdown_Enabled',
'last_time_uploaded':0,
'last_value':''
},
'sp_trip':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'SP_Trip',
'last_time_uploaded':0,
'last_value':''
},
'dh_intakepressure':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_IntakePressure',
'last_time_uploaded':0,
'last_value':''
},
'rp_pressure':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'RP_Pressure',
'last_time_uploaded':0,
'last_value':''
},
'rp_remote':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'RP_Remote',
'last_time_uploaded':0,
'last_value':''
},
'vfd_nameplatefla':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'cfg_NameplateFLA',
'last_time_uploaded':0,
'last_value':''
},
'vfd_fault_commerror':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'VFD_Fault_CommError',
'last_time_uploaded':0,
'last_value':''
},
'vfd_fault':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'VFD_Fault',
'last_time_uploaded':0,
'last_value':''
},
'dh_psirating':{
'data_type':'UINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_PSIRating',
'last_time_uploaded':0,
'last_value':''
},
'vfd_atspeedref':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'VFD_AtSpeedRef',
'last_time_uploaded':0,
'last_value':''
},
'dh_numchannels':{
'data_type':'UINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_NumChannels',
'last_time_uploaded':0,
'last_value':''
},
'vfd_dcbusvoltage':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'VFD_DCBusVoltage',
'last_time_uploaded':0,
'last_value':''
},
'tubingpressure_transducer_enabled':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'TubingPressure_Transducer_Enabled',
'last_time_uploaded':0,
'last_value':''
},
'dh_downholestatus_int':{
'data_type':'UINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_DownholeStatus_INT',
'last_time_uploaded':0,
'last_value':''
},
'dh_toolvoltage':{
'data_type':'UINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_ToolVoltage',
'last_time_uploaded':0,
'last_value':''
},
'tubingpressure_alarm_startup_delay':{
'data_type':'DINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'TubingPressure_Alarm_Startup_Delay',
'last_time_uploaded':0,
'last_value':''
},
'alarm_vfd':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'ALARM_VFD',
'last_time_uploaded':0,
'last_value':''
},
'auto_mode':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'Auto_Mode',
'last_time_uploaded':0,
'last_value':''
},
'dh_fluid_level':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_Fluid_Level',
'last_time_uploaded':0,
'last_value':''
},
'alarm_remote':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'ALARM_Remote',
'last_time_uploaded':0,
'last_value':''
},
'sp_remote':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'SP_Remote',
'last_time_uploaded':0,
'last_value':''
},
'sp_vfd':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'SP_VFD',
'last_time_uploaded':0,
'last_value':''
},
'vfd_nameplaterpm':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'cfg_NameplateRPM',
'last_time_uploaded':0,
'last_value':''
},
'vfd_nameplateolcurrent':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'cfg_NameplateOLCurrent',
'last_time_uploaded':0,
'last_value':''
},
'start_permissive':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'Start_Permissive',
'last_time_uploaded':0,
'last_value':''
},
'dh_dischargepressure':{
'data_type':'UINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_DischargePressure',
'last_time_uploaded':0,
'last_value':''
},
'tubingpressure_ok':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'TubingPressure_OK',
'last_time_uploaded':0,
'last_value':''
},
'rp_temperature':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'RP_Temperature',
'last_time_uploaded':0,
'last_value':''
},
'vfd_cmdfwd':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'VFD_CmdFwd',
'last_time_uploaded':0,
'last_value':''
},
'vfd_outputcurrent':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'VFD_OutputCurrent',
'last_time_uploaded':0,
'last_value':''
},
'rp_vfd':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'RP_VFD',
'last_time_uploaded':0,
'last_value':''
},
'tubingpressure_hi_sp':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'TubingPressure_Hi_SP',
'last_time_uploaded':0,
'last_value':''
},
'vfd_outputvoltage':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'VFD_OutputVoltage',
'last_time_uploaded':0,
'last_value':''
},
'downhole_tool_enabled':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'Downhole_Tool_Enabled',
'last_time_uploaded':0,
'last_value':''
},
'vfd_fault_drivefault':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'VFD_Fault_DriveFault',
'last_time_uploaded':0,
'last_value':''
},
'vfd_nameplatevolts':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'cfg_NameplateVolts',
'last_time_uploaded':0,
'last_value':''
},
'dh_intaketemperature':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_IntakeTemperature',
'last_time_uploaded':0,
'last_value':''
},
'vfd_MinFreq':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'cfg_MinFreq',
'last_time_uploaded':0,
'last_value':''
},
'dh_temp_shutdown':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_Temp_Shutdown',
'last_time_uploaded':0,
'last_value':''
},
'tubingpressure_hi':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'TubingPressure_Hi',
'last_time_uploaded':0,
'last_value':''
},
'vfd_deceltime':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'VFD_DecelTime',
'last_time_uploaded':0,
'last_value':''
},
'dh_pressure_startup_enabled':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'DH_Pressure_Startup_Enabled',
'last_time_uploaded':0,
'last_value':''
},
'dh_maxintakepressure_startup':{
'data_type':'UINT',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'DH_MaxIntakePressure_Startup',
'last_time_uploaded':0,
'last_value':''
},
'tubingpressure_lo_sp':{
'data_type':'REAL',
'change_amount':0.5,
'min_time_between_uploads':360,
'tag':'TubingPressure_Lo_SP',
'last_time_uploaded':0,
'last_value':''
},
'vfd_clearfault':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'VFD_ClearFault',
'last_time_uploaded':0,
'last_value':''
},
'remote_shutdown_disabled':{
'data_type':'BOOL',
'change_amount':None,
'min_time_between_uploads':360,
'tag':'Remote_Shutdown_Disabled',
'last_time_uploaded':0,
'last_value':''
}
}
with open('vfd_ipp_channels.p', 'wb') as ch_f:
pickle.dump(channels, ch_f)

131
POCloud_Driver/vfdipp.py Normal file
View File

@@ -0,0 +1,131 @@
#!/usr/bin/python
import types
import traceback
import binascii
import threading
import time
import thread
import os
import struct
import sys
import serial
import minimalmodbus
import pickle
import re
from device_base import deviceBase
import micro800 as u800
import requests
try:
import json
except:
import simplejson as json
channels = {}
min_upload_time = 30
addr = '10.10.10.31'
class start(threading.Thread, deviceBase):
def updateGPS(self):
gps = self.mcu.gps
print("GPS found me at {0}".format(gps))
self.sendtodb("gps", gps, 0)
def setupChannels(self):
with open('vfd_ipp_channels.p', 'rb') as ch_f:
self.channels = pickle.load(ch_f)
print("Channel List\n================")
for x in self.channels.keys():
print x
print("================")
def __init__(self, name=None, number=None, mac=None, Q=None, mcu=None, companyId=None, offset=None, mqtt=None, Nodes=None):
threading.Thread.__init__(self)
deviceBase.__init__(self, name=name, number=number, mac=mac, Q=Q, mcu=mcu, companyId=companyId, offset=offset, mqtt=mqtt, Nodes=Nodes)
self.daemon = True
self.version = "1"
self.device_address = addr
self.finished = threading.Event()
threading.Thread.start(self)
self.sendtodbJSON("device_address", self.device_address, 0)
self.setupChannels()
self.run()
# self.updateGPS()
# this is a required function for all drivers, its goal is to upload some piece of data
# about your device so it can be seen on the web
def register(self):
self.channels["status"]["last_value"] = ""
def run(self):
self.runLoopStatus = ""
last_OK_state = 0
while True:
if len(self.channels) > 0:
try:
for i in self.channels:
runLoopStatus = i
print("reading {0}".format(i))
valData = u800.readMicroTag(self.device_address, self.channels[i]['tag'])
print(valData)
if valData:
nowVal = valData[0]
ch = self.channels[i]
if ch['data_type'] == "BOOL":
if ch['last_value'] == "":
self.sendtodbJSON(i, nowVal, 0)
ch['last_time_uploaded'] = time.time()
ch['last_value'] = nowVal
elif (not (ch['last_value'] == nowVal)) or ((time.time() - ch['last_time_uploaded']) > ch['min_time_between_uploads']):
self.sendtodbJSON(i, nowVal, 0)
ch['last_time_uploaded'] = time.time()
ch['last_value'] = nowVal
if (ch['data_type'] == "REAL") or (ch['data_type'][-3:] == "INT"):
if ch['last_value'] == "":
self.sendtodbJSON(i, nowVal, 0)
ch['last_time_uploaded'] = time.time()
ch['last_value'] = nowVal
elif (abs(ch['last_value'] - nowVal) > ch['change_amount']) or ((time.time() - ch['last_time_uploaded']) > ch['min_time_between_uploads']):
self.sendtodbJSON(i, nowVal, 0)
ch['last_time_uploaded'] = time.time()
ch['last_value'] = nowVal
runLoopStatus = "Complete"
OK_state = 1
if not OK_state == last_OK_state:
self.sendtodbJSON("driver_ok", OK_state, 0)
last_OK_state = OK_state
time.sleep(3)
except Exception, e:
OK_state = 0
if not OK_state == last_OK_state:
self.sendtodbJSON("driver_ok", OK_state, 0)
last_OK_state = OK_state
sleep_timer = 30
print "Error during {0} of run loop: {1}\nWill try again in {2} seconds...".format(runLoopStatus, e, sleep_timer)
time.sleep(sleep_timer)
else:
print("Apparently no channels... length shows {0}".format(len(self.channels)))
print self.channels
setupChannels()
time.sleep(30)
def vfdipp_sync(self, name, value):
self.sendtodb("connected", "true", 0)
return True
def vfdipp_address(self, name, value):
self.device_address = value
return True
def vfdipp_gpsUpdate(self, name, value):
updateGPS()
return True