├── .gitignore ├── CHANGES.txt ├── LICENSE.txt ├── OpenOPC ├── OpenOPCService.py ├── SystemHealth.py ├── __init__.py └── opc.py ├── README-UNIX.txt ├── README.md ├── README.txt ├── doc └── OpenOPC.pdf └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/* 3 | *.egg* 4 | -------------------------------------------------------------------------------- /CHANGES.txt: -------------------------------------------------------------------------------- 1 | OpenOPC for Python 1.2.0 2 | Copyright (c) 2008-2012 by Barry Barnreiter (barry_b@users.sourceforge.net) 3 | 4 | http://openopc.sourceforge.net/ 5 | 6 | 7 | RELEASE NOTES 8 | February 11, 2012 9 | 10 | 11 | 1. In previous versions of OpenOPC the Gateway Service would sometimes fail 12 | to bind to a TCP port, thus causing the service to not listen for incoming 13 | connections. This would only occur on systems containing multiple ethernet 14 | interfaces and the failure would not be consistent or predictible. 15 | 16 | In such situations you can now tell the Gateway Service which ethernet 17 | interface to bind to by setting the OPC_GATE_HOST system enviornment 18 | variable equal to the IP address of the desired ethernet interface. 19 | 20 | It is recommended that this setting be made on all computers with more 21 | than one active ethernet interface. System enviornment variables can 22 | be set using the "System" applet inside the Windows Control Panel. 23 | 24 | 2. In some cases the opc.read() function would attempt to automatically 25 | destoy an OPC group that no longer existed. This would result in an 26 | exception being thrown. This bug has been fixed. 27 | 28 | 3. The OpenOPC installer has been updated to check for multiple versions 29 | of Python when installing the Development Library. Previous versions 30 | of the installer only checked for the older Python 2.5. 31 | 32 | 4. The included opc.exe and OpenOPCService.exe executibles have been 33 | built using Python 2.7.2 and Pyro 3.15. Older versions were built using 34 | Pyro 3.10 which is not protocol compatible with the newer Pyro 3.15. 35 | Thus you cannot use a prior version of the client with the newer gateway 36 | service or vice versa. Please update all your instances of OpenOPC 37 | at the same time in order to avoid this compatibility issue. 38 | 39 | 5. OpenOPC has been tested with Python 2.7. Previous versions were 40 | only tested using Python 2.5 and 2.6. 41 | 42 | 6. OpenOPC has been tested with Windows 7 (64-bit). Previous versions 43 | were only tested using Windows Server 2003 and XP. 44 | 45 | 7. The opc.py and OpenOPCService.py code has been modifed to work with 46 | PyInstaller. Previous versions could only be built using py2exe. 47 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | OpenOPC for Python 2 | Copyright (c) 2008-2012 by Barry Barnreiter (barry_b@users.sourceforge.net) 3 | 4 | This software is licensed under the terms of the GNU GPL v2 license plus 5 | a special linking exception for portions of the package. 6 | 7 | GNU GENERAL PUBLIC LICENSE 8 | Version 2, June 1991 9 | 10 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 11 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 12 | Everyone is permitted to copy and distribute verbatim copies 13 | of this license document, but changing it is not allowed. 14 | 15 | Preamble 16 | 17 | The licenses for most software are designed to take away your 18 | freedom to share and change it. By contrast, the GNU General Public 19 | License is intended to guarantee your freedom to share and change free 20 | software--to make sure the software is free for all its users. This 21 | General Public License applies to most of the Free Software 22 | Foundation's software and to any other program whose authors commit to 23 | using it. (Some other Free Software Foundation software is covered by 24 | the GNU Lesser General Public License instead.) You can apply it to 25 | your programs, too. 26 | 27 | When we speak of free software, we are referring to freedom, not 28 | price. Our General Public Licenses are designed to make sure that you 29 | have the freedom to distribute copies of free software (and charge for 30 | this service if you wish), that you receive source code or can get it 31 | if you want it, that you can change the software or use pieces of it 32 | in new free programs; and that you know you can do these things. 33 | 34 | To protect your rights, we need to make restrictions that forbid 35 | anyone to deny you these rights or to ask you to surrender the rights. 36 | These restrictions translate to certain responsibilities for you if you 37 | distribute copies of the software, or if you modify it. 38 | 39 | For example, if you distribute copies of such a program, whether 40 | gratis or for a fee, you must give the recipients all the rights that 41 | you have. You must make sure that they, too, receive or can get the 42 | source code. And you must show them these terms so they know their 43 | rights. 44 | 45 | We protect your rights with two steps: (1) copyright the software, and 46 | (2) offer you this license which gives you legal permission to copy, 47 | distribute and/or modify the software. 48 | 49 | Also, for each author's protection and ours, we want to make certain 50 | that everyone understands that there is no warranty for this free 51 | software. If the software is modified by someone else and passed on, we 52 | want its recipients to know that what they have is not the original, so 53 | that any problems introduced by others will not reflect on the original 54 | authors' reputations. 55 | 56 | Finally, any free program is threatened constantly by software 57 | patents. We wish to avoid the danger that redistributors of a free 58 | program will individually obtain patent licenses, in effect making the 59 | program proprietary. To prevent this, we have made it clear that any 60 | patent must be licensed for everyone's free use or not licensed at all. 61 | 62 | The precise terms and conditions for copying, distribution and 63 | modification follow. 64 | 65 | GNU GENERAL PUBLIC LICENSE 66 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 67 | 68 | 0. This License applies to any program or other work which contains 69 | a notice placed by the copyright holder saying it may be distributed 70 | under the terms of this General Public License. The "Program", below, 71 | refers to any such program or work, and a "work based on the Program" 72 | means either the Program or any derivative work under copyright law: 73 | that is to say, a work containing the Program or a portion of it, 74 | either verbatim or with modifications and/or translated into another 75 | language. (Hereinafter, translation is included without limitation in 76 | the term "modification".) Each licensee is addressed as "you". 77 | 78 | Activities other than copying, distribution and modification are not 79 | covered by this License; they are outside its scope. The act of 80 | running the Program is not restricted, and the output from the Program 81 | is covered only if its contents constitute a work based on the 82 | Program (independent of having been made by running the Program). 83 | Whether that is true depends on what the Program does. 84 | 85 | 1. You may copy and distribute verbatim copies of the Program's 86 | source code as you receive it, in any medium, provided that you 87 | conspicuously and appropriately publish on each copy an appropriate 88 | copyright notice and disclaimer of warranty; keep intact all the 89 | notices that refer to this License and to the absence of any warranty; 90 | and give any other recipients of the Program a copy of this License 91 | along with the Program. 92 | 93 | You may charge a fee for the physical act of transferring a copy, and 94 | you may at your option offer warranty protection in exchange for a fee. 95 | 96 | 2. You may modify your copy or copies of the Program or any portion 97 | of it, thus forming a work based on the Program, and copy and 98 | distribute such modifications or work under the terms of Section 1 99 | above, provided that you also meet all of these conditions: 100 | 101 | a) You must cause the modified files to carry prominent notices 102 | stating that you changed the files and the date of any change. 103 | 104 | b) You must cause any work that you distribute or publish, that in 105 | whole or in part contains or is derived from the Program or any 106 | part thereof, to be licensed as a whole at no charge to all third 107 | parties under the terms of this License. 108 | 109 | c) If the modified program normally reads commands interactively 110 | when run, you must cause it, when started running for such 111 | interactive use in the most ordinary way, to print or display an 112 | announcement including an appropriate copyright notice and a 113 | notice that there is no warranty (or else, saying that you provide 114 | a warranty) and that users may redistribute the program under 115 | these conditions, and telling the user how to view a copy of this 116 | License. (Exception: if the Program itself is interactive but 117 | does not normally print such an announcement, your work based on 118 | the Program is not required to print an announcement.) 119 | 120 | These requirements apply to the modified work as a whole. If 121 | identifiable sections of that work are not derived from the Program, 122 | and can be reasonably considered independent and separate works in 123 | themselves, then this License, and its terms, do not apply to those 124 | sections when you distribute them as separate works. But when you 125 | distribute the same sections as part of a whole which is a work based 126 | on the Program, the distribution of the whole must be on the terms of 127 | this License, whose permissions for other licensees extend to the 128 | entire whole, and thus to each and every part regardless of who wrote it. 129 | 130 | Thus, it is not the intent of this section to claim rights or contest 131 | your rights to work written entirely by you; rather, the intent is to 132 | exercise the right to control the distribution of derivative or 133 | collective works based on the Program. 134 | 135 | In addition, mere aggregation of another work not based on the Program 136 | with the Program (or with a work based on the Program) on a volume of 137 | a storage or distribution medium does not bring the other work under 138 | the scope of this License. 139 | 140 | 3. You may copy and distribute the Program (or a work based on it, 141 | under Section 2) in object code or executable form under the terms of 142 | Sections 1 and 2 above provided that you also do one of the following: 143 | 144 | a) Accompany it with the complete corresponding machine-readable 145 | source code, which must be distributed under the terms of Sections 146 | 1 and 2 above on a medium customarily used for software interchange; or, 147 | 148 | b) Accompany it with a written offer, valid for at least three 149 | years, to give any third party, for a charge no more than your 150 | cost of physically performing source distribution, a complete 151 | machine-readable copy of the corresponding source code, to be 152 | distributed under the terms of Sections 1 and 2 above on a medium 153 | customarily used for software interchange; or, 154 | 155 | c) Accompany it with the information you received as to the offer 156 | to distribute corresponding source code. (This alternative is 157 | allowed only for noncommercial distribution and only if you 158 | received the program in object code or executable form with such 159 | an offer, in accord with Subsection b above.) 160 | 161 | The source code for a work means the preferred form of the work for 162 | making modifications to it. For an executable work, complete source 163 | code means all the source code for all modules it contains, plus any 164 | associated interface definition files, plus the scripts used to 165 | control compilation and installation of the executable. However, as a 166 | special exception, the source code distributed need not include 167 | anything that is normally distributed (in either source or binary 168 | form) with the major components (compiler, kernel, and so on) of the 169 | operating system on which the executable runs, unless that component 170 | itself accompanies the executable. 171 | 172 | If distribution of executable or object code is made by offering 173 | access to copy from a designated place, then offering equivalent 174 | access to copy the source code from the same place counts as 175 | distribution of the source code, even though third parties are not 176 | compelled to copy the source along with the object code. 177 | 178 | 4. You may not copy, modify, sublicense, or distribute the Program 179 | except as expressly provided under this License. Any attempt 180 | otherwise to copy, modify, sublicense or distribute the Program is 181 | void, and will automatically terminate your rights under this License. 182 | However, parties who have received copies, or rights, from you under 183 | this License will not have their licenses terminated so long as such 184 | parties remain in full compliance. 185 | 186 | 5. You are not required to accept this License, since you have not 187 | signed it. However, nothing else grants you permission to modify or 188 | distribute the Program or its derivative works. These actions are 189 | prohibited by law if you do not accept this License. Therefore, by 190 | modifying or distributing the Program (or any work based on the 191 | Program), you indicate your acceptance of this License to do so, and 192 | all its terms and conditions for copying, distributing or modifying 193 | the Program or works based on it. 194 | 195 | 6. Each time you redistribute the Program (or any work based on the 196 | Program), the recipient automatically receives a license from the 197 | original licensor to copy, distribute or modify the Program subject to 198 | these terms and conditions. You may not impose any further 199 | restrictions on the recipients' exercise of the rights granted herein. 200 | You are not responsible for enforcing compliance by third parties to 201 | this License. 202 | 203 | 7. If, as a consequence of a court judgment or allegation of patent 204 | infringement or for any other reason (not limited to patent issues), 205 | conditions are imposed on you (whether by court order, agreement or 206 | otherwise) that contradict the conditions of this License, they do not 207 | excuse you from the conditions of this License. If you cannot 208 | distribute so as to satisfy simultaneously your obligations under this 209 | License and any other pertinent obligations, then as a consequence you 210 | may not distribute the Program at all. For example, if a patent 211 | license would not permit royalty-free redistribution of the Program by 212 | all those who receive copies directly or indirectly through you, then 213 | the only way you could satisfy both it and this License would be to 214 | refrain entirely from distribution of the Program. 215 | 216 | If any portion of this section is held invalid or unenforceable under 217 | any particular circumstance, the balance of the section is intended to 218 | apply and the section as a whole is intended to apply in other 219 | circumstances. 220 | 221 | It is not the purpose of this section to induce you to infringe any 222 | patents or other property right claims or to contest validity of any 223 | such claims; this section has the sole purpose of protecting the 224 | integrity of the free software distribution system, which is 225 | implemented by public license practices. Many people have made 226 | generous contributions to the wide range of software distributed 227 | through that system in reliance on consistent application of that 228 | system; it is up to the author/donor to decide if he or she is willing 229 | to distribute software through any other system and a licensee cannot 230 | impose that choice. 231 | 232 | This section is intended to make thoroughly clear what is believed to 233 | be a consequence of the rest of this License. 234 | 235 | 8. If the distribution and/or use of the Program is restricted in 236 | certain countries either by patents or by copyrighted interfaces, the 237 | original copyright holder who places the Program under this License 238 | may add an explicit geographical distribution limitation excluding 239 | those countries, so that distribution is permitted only in or among 240 | countries not thus excluded. In such case, this License incorporates 241 | the limitation as if written in the body of this License. 242 | 243 | 9. The Free Software Foundation may publish revised and/or new versions 244 | of the General Public License from time to time. Such new versions will 245 | be similar in spirit to the present version, but may differ in detail to 246 | address new problems or concerns. 247 | 248 | Each version is given a distinguishing version number. If the Program 249 | specifies a version number of this License which applies to it and "any 250 | later version", you have the option of following the terms and conditions 251 | either of that version or of any later version published by the Free 252 | Software Foundation. If the Program does not specify a version number of 253 | this License, you may choose any version ever published by the Free Software 254 | Foundation. 255 | 256 | 10. If you wish to incorporate parts of the Program into other free 257 | programs whose distribution conditions are different, write to the author 258 | to ask for permission. For software which is copyrighted by the Free 259 | Software Foundation, write to the Free Software Foundation; we sometimes 260 | make exceptions for this. Our decision will be guided by the two goals 261 | of preserving the free status of all derivatives of our free software and 262 | of promoting the sharing and reuse of software generally. 263 | 264 | NO WARRANTY 265 | 266 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 267 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 268 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 269 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 270 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 271 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 272 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 273 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 274 | REPAIR OR CORRECTION. 275 | 276 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 277 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 278 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 279 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 280 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 281 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 282 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 283 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 284 | POSSIBILITY OF SUCH DAMAGES. 285 | 286 | 13. Linking Exception (additional terms provided by OpenOPC project) 287 | 288 | The following linking exception applies only to the OpenOPC for Python 289 | library module (OpenOPC.py). It does not apply to any other files or 290 | or programs included as part of the OpenOPC project or this installation. 291 | 292 | As a special exception, the copyright holders of this library give you 293 | permission to link this library with independent modules to produce an 294 | executable, regardless of the license terms of these independent modules, 295 | and to copy and distribute the resulting executable under terms of your 296 | choice, provided that you also meet, for each linked independent module, 297 | the terms and conditions of the license of that module. An independent 298 | module is a module which is not derived from or based on this library. 299 | If you modify this library, you may extend this exception to your version 300 | of the library, but you are not obliged to do so. If you do not wish to 301 | do so, delete this exception statement from your version. 302 | 303 | END OF TERMS AND CONDITIONS 304 | -------------------------------------------------------------------------------- /OpenOPC/OpenOPCService.py: -------------------------------------------------------------------------------- 1 | ########################################################################### 2 | # 3 | # OpenOPC Gateway Service 4 | # 5 | # A Windows service providing remote access to the OpenOPC library. 6 | # 7 | # Copyright (c) 2007-2012 Barry Barnreiter (barry_b@users.sourceforge.net) 8 | # 9 | ########################################################################### 10 | 11 | import win32serviceutil 12 | import win32service 13 | import win32event 14 | import servicemanager 15 | import winerror 16 | import _winreg 17 | import select 18 | import socket 19 | import os 20 | import time 21 | import OpenOPC 22 | 23 | try: 24 | import Pyro.core 25 | import Pyro.protocol 26 | except ImportError: 27 | print 'Pyro module required (http://pyro.sourceforge.net/)' 28 | exit() 29 | 30 | Pyro.config.PYRO_MULTITHREADED = 1 31 | 32 | opc_class = OpenOPC.OPC_CLASS 33 | opc_gate_host = '' 34 | opc_gate_port = 7766 35 | 36 | def getvar(env_var): 37 | """Read system enviornment variable from registry""" 38 | try: 39 | key = _winreg.OpenKey(_winreg.HKEY_LOCAL_MACHINE, 'SYSTEM\\CurrentControlSet\\Control\Session Manager\Environment',0,_winreg.KEY_READ) 40 | value, valuetype = _winreg.QueryValueEx(key, env_var) 41 | return value 42 | except: 43 | return None 44 | 45 | # Get env vars directly from the Registry since a reboot is normally required 46 | # for the Local System account to inherit these. 47 | 48 | if getvar('OPC_CLASS'): opc_class = getvar('OPC_CLASS') 49 | if getvar('OPC_GATE_HOST'): opc_gate_host = getvar('OPC_GATE_HOST') 50 | if getvar('OPC_GATE_PORT'): opc_gate_port = int(getvar('OPC_GATE_PORT')) 51 | 52 | class opc(Pyro.core.ObjBase): 53 | def __init__(self): 54 | Pyro.core.ObjBase.__init__(self) 55 | self._remote_hosts = {} 56 | self._init_times = {} 57 | self._tx_times = {} 58 | 59 | def get_clients(self): 60 | """Return list of server instances as a list of (GUID,host,time) tuples""" 61 | 62 | reg = self.getDaemon().getRegistered() 63 | hosts = self._remote_hosts 64 | init_times = self._init_times 65 | tx_times = self._tx_times 66 | 67 | hlist = [(k, hosts[k] if hosts.has_key(k) else '', init_times[k], tx_times[k]) for k,v in reg.iteritems() if v == None] 68 | return hlist 69 | 70 | def create_client(self): 71 | """Create a new OpenOPC instance in the Pyro server""" 72 | 73 | opc_obj = OpenOPC.client(opc_class) 74 | base_obj = Pyro.core.ObjBase() 75 | base_obj.delegateTo(opc_obj) 76 | uri = self.getDaemon().connect(base_obj) 77 | 78 | opc_obj._open_serv = self 79 | opc_obj._open_self = base_obj 80 | opc_obj._open_host = self.getDaemon().hostname 81 | opc_obj._open_port = self.getDaemon().port 82 | opc_obj._open_guid = uri.objectID 83 | 84 | remote_ip = self.getLocalStorage().caller.addr[0] 85 | try: 86 | remote_name = socket.gethostbyaddr(remote_ip)[0] 87 | self._remote_hosts[uri.objectID] = '%s (%s)' % (remote_ip, remote_name) 88 | except socket.herror: 89 | self._remote_hosts[uri.objectID] = '%s' % (remote_ip) 90 | self._init_times[uri.objectID] = time.time() 91 | self._tx_times[uri.objectID] = time.time() 92 | return Pyro.core.getProxyForURI(uri) 93 | 94 | def release_client(self, obj): 95 | """Release an OpenOPC instance in the Pyro server""" 96 | 97 | self.getDaemon().disconnect(obj) 98 | del self._remote_hosts[obj.GUID()] 99 | del self._init_times[obj.GUID()] 100 | del self._tx_times[obj.GUID()] 101 | del obj 102 | 103 | class OpcService(win32serviceutil.ServiceFramework): 104 | _svc_name_ = "zzzOpenOPCService" 105 | _svc_display_name_ = "OpenOPC Gateway Service" 106 | 107 | def __init__(self, args): 108 | win32serviceutil.ServiceFramework.__init__(self, args) 109 | self.hWaitStop = win32event.CreateEvent(None, 0, 0, None) 110 | 111 | def SvcStop(self): 112 | servicemanager.LogInfoMsg('\n\nStopping service') 113 | self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) 114 | win32event.SetEvent(self.hWaitStop) 115 | 116 | def SvcDoRun(self): 117 | servicemanager.LogInfoMsg('\n\nStarting service on port %d' % opc_gate_port) 118 | 119 | daemon = Pyro.core.Daemon(host=opc_gate_host, port=opc_gate_port) 120 | daemon.connect(opc(), "opc") 121 | 122 | while win32event.WaitForSingleObject(self.hWaitStop, 0) != win32event.WAIT_OBJECT_0: 123 | socks = daemon.getServerSockets() 124 | ins,outs,exs = select.select(socks,[],[],1) 125 | for s in socks: 126 | if s in ins: 127 | daemon.handleRequests() 128 | break 129 | 130 | daemon.shutdown() 131 | 132 | if __name__ == '__main__': 133 | if len(sys.argv) == 1: 134 | try: 135 | evtsrc_dll = os.path.abspath(servicemanager.__file__) 136 | servicemanager.PrepareToHostSingle(OpcService) 137 | servicemanager.Initialize('zzzOpenOPCService', evtsrc_dll) 138 | servicemanager.StartServiceCtrlDispatcher() 139 | except win32service.error, details: 140 | if details[0] == winerror.ERROR_FAILED_SERVICE_CONTROLLER_CONNECT: 141 | win32serviceutil.usage() 142 | else: 143 | win32serviceutil.HandleCommandLine(OpcService) 144 | -------------------------------------------------------------------------------- /OpenOPC/SystemHealth.py: -------------------------------------------------------------------------------- 1 | import win32com.client 2 | import win32process 3 | import win32pdh 4 | import win32pdhquery 5 | import win32pdhutil 6 | import pywintypes 7 | import ctypes 8 | import time 9 | import os 10 | import math 11 | 12 | __version__ = '1.0.1' 13 | 14 | # NT4 does not have WMI by default 15 | try: 16 | import wmi 17 | except: 18 | wmi_found = False 19 | else: 20 | wmi_found = True 21 | 22 | class CPU: 23 | def __init__(self): 24 | path = win32pdh.MakeCounterPath((None, "Processor", "_Total", None, -1, "% Processor Time")) 25 | self.base = win32pdh.OpenQuery() 26 | self.counter = win32pdh.AddCounter(self.base, path) 27 | self.reset() 28 | 29 | def reset(self): 30 | win32pdh.CollectQueryData(self.base) 31 | 32 | def get_usage(self): 33 | win32pdh.CollectQueryData(self.base) 34 | try: 35 | value = win32pdh.GetFormattedCounterValue(self.counter, win32pdh.PDH_FMT_LONG)[1] 36 | except pywintypes.error: 37 | value = 0 38 | return value 39 | 40 | def _disk_info(): 41 | drive = unicode(os.getenv("SystemDrive")) 42 | freeuser = ctypes.c_int64() 43 | total = ctypes.c_int64() 44 | free = ctypes.c_int64() 45 | ctypes.windll.kernel32.GetDiskFreeSpaceExW(drive, ctypes.byref(freeuser), ctypes.byref(total), ctypes.byref(free)) 46 | return freeuser.value 47 | 48 | def disk_free(): 49 | return int(_disk_info() / 1024) 50 | 51 | def _mem_info(): 52 | kernel32 = ctypes.windll.kernel32 53 | c_ulong = ctypes.c_ulong 54 | class MEMORYSTATUS(ctypes.Structure): 55 | _fields_ = [ 56 | ('dwLength', c_ulong), 57 | ('dwMemoryLoad', c_ulong), 58 | ('dwTotalPhys', c_ulong), 59 | ('dwAvailPhys', c_ulong), 60 | ('dwTotalPageFile', c_ulong), 61 | ('dwAvailPageFile', c_ulong), 62 | ('dwTotalVirtual', c_ulong), 63 | ('dwAvailVirtual', c_ulong) 64 | ] 65 | 66 | memoryStatus = MEMORYSTATUS() 67 | memoryStatus.dwLength = ctypes.sizeof(MEMORYSTATUS) 68 | kernel32.GlobalMemoryStatus(ctypes.byref(memoryStatus)) 69 | return (memoryStatus.dwTotalPhys, memoryStatus.dwAvailPhys) 70 | 71 | def mem_used(): 72 | counter=r'\Memory\Committed Bytes' 73 | machine, object, instance, parentInstance, index, counter = win32pdh.ParseCounterPath(counter) 74 | 75 | instance = None 76 | inum=-1 77 | format = win32pdh.PDH_FMT_DOUBLE 78 | machine=None 79 | 80 | path = win32pdh.MakeCounterPath( (machine,object, instance, None, inum,counter) ) 81 | hq = win32pdh.OpenQuery() 82 | try: 83 | hc = win32pdh.AddCounter(hq, path) 84 | try: 85 | win32pdh.CollectQueryData(hq) 86 | type, val = win32pdh.GetFormattedCounterValue(hc, format) 87 | return int(val / 1024) 88 | except pywintypes.error: 89 | return 0 90 | finally: 91 | win32pdh.RemoveCounter(hc) 92 | finally: 93 | win32pdh.CloseQuery(hq) 94 | 95 | def mem_free(): 96 | total,free = _mem_info() 97 | return int(free / 1024) 98 | 99 | def mem_total(): 100 | total,free = _mem_info() 101 | return int(total / 1024) 102 | 103 | def mem_percent(): 104 | total,free = _mem_info() 105 | return ( float(total - free) / float(total) ) * 100.0 106 | 107 | def _task_list(): 108 | psapi = ctypes.windll.psapi 109 | kernel = ctypes.windll.kernel32 110 | 111 | hModule = ctypes.c_ulong() 112 | count = ctypes.c_ulong() 113 | modname = ctypes.c_buffer(30) 114 | PROCESS_QUERY_INFORMATION = 0x0400 115 | PROCESS_VM_READ = 0x0010 116 | 117 | pid_list = win32process.EnumProcesses() 118 | info_list = [] 119 | 120 | for pid in pid_list: 121 | 122 | hProcess = kernel.OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, pid) 123 | if hProcess: 124 | psapi.EnumProcessModules(hProcess, ctypes.byref(hModule), ctypes.sizeof(hModule), ctypes.byref(count)) 125 | psapi.GetModuleBaseNameA(hProcess, hModule.value, modname, ctypes.sizeof(modname)) 126 | pname = ctypes.string_at(modname) 127 | 128 | procmeminfo = win32process.GetProcessMemoryInfo(hProcess) 129 | procmemusage = (procmeminfo["WorkingSetSize"]/1024) 130 | info_list.append((pid, pname, procmemusage)) 131 | 132 | kernel.CloseHandle(hProcess) 133 | 134 | return info_list 135 | 136 | def task_mem(image_name): 137 | image_name = str.lower(image_name) 138 | if image_name[-4:] != '.exe': image_name = image_name + '.exe' 139 | return sum([mem for pid,name,mem in _task_list() if str.lower(name) == image_name]) 140 | 141 | def task_exists(image_name): 142 | image_name = str.lower(image_name) 143 | if image_name[-4:] != '.exe': image_name = image_name + '.exe' 144 | return len([mem for pid,name,mem in _task_list() if str.lower(name) == image_name]) > 0 145 | 146 | def task_cpu(image_name): 147 | if not wmi_found: return 0.0 148 | 149 | image_name = str.lower(image_name) 150 | if image_name[-4:] == '.exe': image_name = image_name[:-4] 151 | 152 | c = wmi.WMI() 153 | process_info = {} 154 | pct_cpu_time = 0.0 155 | 156 | for i in range(2): 157 | 158 | for p in c.Win32_PerfRawData_PerfProc_Process(name=image_name): 159 | id = long(p.IDProcess) 160 | n1, d1 = long(p.PercentProcessorTime), long(p.Timestamp_Sys100NS) 161 | n0, d0, so_far = process_info.get(id, (0, 0, [])) 162 | 163 | try: 164 | pct_cpu_time += (float (n1 - n0) / float (d1 - d0)) * 100.0 165 | except ZeroDivisionError: 166 | pct_cpu_time += 0.0 167 | 168 | so_far.append(pct_cpu_time) 169 | process_info[id] = (n1, d1, so_far) 170 | 171 | if i == 0: 172 | time.sleep(0.1) 173 | pct_cpu_time = 0.0 174 | 175 | num_cpu = int(os.environ['NUMBER_OF_PROCESSORS']) 176 | return min(pct_cpu_time / num_cpu, 100.0) 177 | 178 | def sine_wave(): 179 | min = float(time.localtime()[4]) 180 | sec = float(time.localtime()[5]) 181 | T = (min + (sec/60.0)) % 10.0 182 | return math.sin(2.0 * math.pi * T/10.0) * 100.0 183 | 184 | def saw_wave(): 185 | min = float(time.localtime()[4]) 186 | sec = float(time.localtime()[5]) 187 | T = (min + (sec/60.0)) % 10.0 188 | return (T/10.0) * 100.0 189 | 190 | -------------------------------------------------------------------------------- /OpenOPC/__init__.py: -------------------------------------------------------------------------------- 1 | ########################################################################### 2 | # 3 | # OpenOPC for Python Library Module 4 | # 5 | # Copyright (c) 2007-2012 Barry Barnreiter (barry_b@users.sourceforge.net) 6 | # 7 | ########################################################################### 8 | 9 | import os 10 | import sys 11 | import time 12 | import types 13 | import string 14 | import socket 15 | import re 16 | import Queue 17 | 18 | __version__ = '1.2.0' 19 | 20 | current_client = None 21 | 22 | # Win32 only modules not needed for 'open' protocol mode 23 | if os.name == 'nt': 24 | try: 25 | import win32com.client 26 | import win32com.server.util 27 | import win32event 28 | import pythoncom 29 | import pywintypes 30 | import SystemHealth 31 | 32 | # Win32 variant types 33 | vt = dict([(pythoncom.__dict__[vtype], vtype) for vtype in pythoncom.__dict__.keys() if vtype[:2] == "VT"]) 34 | 35 | # Allow gencache to create the cached wrapper objects 36 | win32com.client.gencache.is_readonly = False 37 | 38 | # Under p2exe the call in gencache to __init__() does not happen 39 | # so we use Rebuild() to force the creation of the gen_py folder 40 | win32com.client.gencache.Rebuild(verbose=0) 41 | 42 | # So we can work on Windows in "open" protocol mode without the need for the win32com modules 43 | except ImportError: 44 | win32com_found = False 45 | else: 46 | win32com_found = True 47 | else: 48 | win32com_found = False 49 | 50 | # OPC Constants 51 | 52 | SOURCE_CACHE = 1 53 | SOURCE_DEVICE = 2 54 | OPC_STATUS = (0, 'Running', 'Failed', 'NoConfig', 'Suspended', 'Test') 55 | BROWSER_TYPE = (0, 'Hierarchical', 'Flat') 56 | ACCESS_RIGHTS = (0, 'Read', 'Write', 'Read/Write') 57 | OPC_QUALITY = ('Bad', 'Uncertain', 'Unknown', 'Good') 58 | OPC_CLASS = 'Matrikon.OPC.Automation;Graybox.OPC.DAWrapper;HSCOPC.Automation;RSI.OPCAutomation;OPC.Automation' 59 | OPC_SERVER = 'Hci.TPNServer;HwHsc.OPCServer;opc.deltav.1;AIM.OPC.1;Yokogawa.ExaopcDAEXQ.1;OSI.DA.1;OPC.PHDServerDA.1;Aspen.Infoplus21_DA.1;National Instruments.OPCLabVIEW;RSLinx OPC Server;KEPware.KEPServerEx.V4;Matrikon.OPC.Simulation;Prosys.OPC.Simulation' 60 | OPC_CLIENT = 'OpenOPC' 61 | 62 | def quality_str(quality_bits): 63 | """Convert OPC quality bits to a descriptive string""" 64 | 65 | quality = (quality_bits >> 6) & 3 66 | return OPC_QUALITY[quality] 67 | 68 | def type_check(tags): 69 | """Perform a type check on a list of tags""" 70 | 71 | if type(tags) in (types.ListType, types.TupleType): 72 | single = False 73 | elif tags == None: 74 | tags = [] 75 | single = False 76 | else: 77 | tags = [tags] 78 | single = True 79 | 80 | if len([t for t in tags if type(t) not in types.StringTypes]) == 0: 81 | valid = True 82 | else: 83 | valid = False 84 | 85 | return tags, single, valid 86 | 87 | def wild2regex(string): 88 | """Convert a Unix wildcard glob into a regular expression""" 89 | return string.replace('.','\.').replace('*','.*').replace('?','.').replace('!','^') 90 | 91 | def tags2trace(tags): 92 | """Convert a list tags into a formatted string suitable for the trace callback log""" 93 | arg_str = '' 94 | for i,t in enumerate(tags[1:]): 95 | if i > 0: arg_str += ',' 96 | arg_str += '%s' % t 97 | return arg_str 98 | 99 | def exceptional(func, alt_return=None, alt_exceptions=(Exception,), final=None, catch=None): 100 | """Turns exceptions into an alternative return value""" 101 | 102 | def _exceptional(*args, **kwargs): 103 | try: 104 | try: 105 | return func(*args, **kwargs) 106 | except alt_exceptions: 107 | return alt_return 108 | except: 109 | if catch: return catch(sys.exc_info(), lambda:func(*args, **kwargs)) 110 | raise 111 | finally: 112 | if final: final() 113 | return _exceptional 114 | 115 | def get_sessions(host='localhost', port=7766): 116 | """Return sessions in OpenOPC Gateway Service as GUID:host hash""" 117 | 118 | import Pyro.core 119 | Pyro.core.initClient(banner = 0) 120 | server_obj = Pyro.core.getProxyForURI("PYROLOC://%s:%s/opc" % (host, port)) 121 | return server_obj.get_clients() 122 | 123 | def open_client(host='localhost', port=7766): 124 | """Connect to the specified OpenOPC Gateway Service""" 125 | 126 | import Pyro.core 127 | Pyro.core.initClient(banner=0) 128 | server_obj = Pyro.core.getProxyForURI("PYROLOC://%s:%s/opc" % (host, port)) 129 | return server_obj.create_client() 130 | 131 | class TimeoutError(Exception): 132 | def __init__(self, txt): 133 | Exception.__init__(self, txt) 134 | 135 | class OPCError(Exception): 136 | def __init__(self, txt): 137 | Exception.__init__(self, txt) 138 | 139 | class GroupEvents: 140 | def __init__(self): 141 | self.client = current_client 142 | 143 | def OnDataChange(self, TransactionID, NumItems, ClientHandles, ItemValues, Qualities, TimeStamps): 144 | self.client.callback_queue.put((TransactionID, ClientHandles, ItemValues, Qualities, TimeStamps)) 145 | 146 | class client(): 147 | def __init__(self, opc_class=None, client_name=None): 148 | """Instantiate OPC automation class""" 149 | 150 | self.callback_queue = Queue.Queue() 151 | 152 | pythoncom.CoInitialize() 153 | 154 | if opc_class == None: 155 | if os.environ.has_key('OPC_CLASS'): 156 | opc_class = os.environ['OPC_CLASS'] 157 | else: 158 | opc_class = OPC_CLASS 159 | 160 | opc_class_list = opc_class.split(';') 161 | 162 | for i,c in enumerate(opc_class_list): 163 | try: 164 | self._opc = win32com.client.gencache.EnsureDispatch(c, 0) 165 | self.opc_class = c 166 | break 167 | except pythoncom.com_error, err: 168 | if i == len(opc_class_list)-1: 169 | error_msg = 'Dispatch: %s' % self._get_error_str(err) 170 | raise OPCError, error_msg 171 | 172 | self._event = win32event.CreateEvent(None,0,0,None) 173 | 174 | self.opc_server = None 175 | self.opc_host = None 176 | self.client_name = client_name 177 | self._groups = {} 178 | self._group_tags = {} 179 | self._group_valid_tags = {} 180 | self._group_server_handles = {} 181 | self._group_handles_tag = {} 182 | self._group_hooks = {} 183 | self._open_serv = None 184 | self._open_self = None 185 | self._open_host = None 186 | self._open_port = None 187 | self._open_guid = None 188 | self._prev_serv_time = None 189 | self._tx_id = 0 190 | self.trace = None 191 | self.cpu = None 192 | 193 | def set_trace(self, trace): 194 | if self._open_serv == None: 195 | self.trace = trace 196 | 197 | def connect(self, opc_server=None, opc_host='localhost'): 198 | """Connect to the specified OPC server""" 199 | 200 | pythoncom.CoInitialize() 201 | 202 | if opc_server == None: 203 | # Initial connect using environment vars 204 | if self.opc_server == None: 205 | if os.environ.has_key('OPC_SERVER'): 206 | opc_server = os.environ['OPC_SERVER'] 207 | else: 208 | opc_server = OPC_SERVER 209 | # Reconnect using previous server name 210 | else: 211 | opc_server = self.opc_server 212 | opc_host = self.opc_host 213 | 214 | opc_server_list = opc_server.split(';') 215 | connected = False 216 | 217 | for s in opc_server_list: 218 | try: 219 | if self.trace: self.trace('Connect(%s,%s)' % (s, opc_host)) 220 | self._opc.Connect(s, opc_host) 221 | except pythoncom.com_error, err: 222 | if len(opc_server_list) == 1: 223 | error_msg = 'Connect: %s' % self._get_error_str(err) 224 | raise OPCError, error_msg 225 | else: 226 | # Set client name since some OPC servers use it for security 227 | try: 228 | if self.client_name == None: 229 | if os.environ.has_key('OPC_CLIENT'): 230 | self._opc.ClientName = os.environ['OPC_CLIENT'] 231 | else: 232 | self._opc.ClientName = OPC_CLIENT 233 | else: 234 | self._opc.ClientName = self.client_name 235 | except: 236 | pass 237 | connected = True 238 | break 239 | 240 | if not connected: 241 | raise OPCError, 'Connect: Cannot connect to any of the servers in the OPC_SERVER list' 242 | 243 | # With some OPC servers, the next OPC call immediately after Connect() 244 | # will occationally fail. Sleeping for 1/100 second seems to fix this. 245 | time.sleep(0.01) 246 | 247 | self.opc_server = opc_server 248 | if opc_host == 'localhost': 249 | opc_host = socket.gethostname() 250 | self.opc_host = opc_host 251 | 252 | # On reconnect we need to remove the old group names from OpenOPC's internal 253 | # cache since they are now invalid 254 | self._groups = {} 255 | self._group_tags = {} 256 | self._group_valid_tags = {} 257 | self._group_server_handles = {} 258 | self._group_handles_tag = {} 259 | self._group_hooks = {} 260 | 261 | def close(self, del_object=True): 262 | """Disconnect from the currently connected OPC server""" 263 | 264 | try: 265 | pythoncom.CoInitialize() 266 | self.remove(self.groups()) 267 | 268 | except pythoncom.com_error, err: 269 | error_msg = 'Disconnect: %s' % self._get_error_str(err) 270 | raise OPCError, error_msg 271 | 272 | except OPCError: 273 | pass 274 | 275 | finally: 276 | if self.trace: self.trace('Disconnect()') 277 | self._opc.Disconnect() 278 | 279 | # Remove this object from the open gateway service 280 | if self._open_serv and del_object: 281 | self._open_serv.release_client(self._open_self) 282 | 283 | def iread(self, tags=None, group=None, size=None, pause=0, source='hybrid', update=-1, timeout=5000, sync=False, include_error=False, rebuild=False): 284 | """Iterable version of read()""" 285 | 286 | def add_items(tags): 287 | names = list(tags) 288 | 289 | names.insert(0,0) 290 | errors = [] 291 | 292 | if self.trace: self.trace('Validate(%s)' % tags2trace(names)) 293 | 294 | try: 295 | errors = opc_items.Validate(len(names)-1, names) 296 | except: 297 | pass 298 | 299 | valid_tags = [] 300 | valid_values = [] 301 | client_handles = [] 302 | 303 | if not self._group_handles_tag.has_key(sub_group): 304 | self._group_handles_tag[sub_group] = {} 305 | n = 0 306 | elif len(self._group_handles_tag[sub_group]) > 0: 307 | n = max(self._group_handles_tag[sub_group]) + 1 308 | else: 309 | n = 0 310 | 311 | for i, tag in enumerate(tags): 312 | if errors[i] == 0: 313 | valid_tags.append(tag) 314 | client_handles.append(n) 315 | self._group_handles_tag[sub_group][n] = tag 316 | n += 1 317 | elif include_error: 318 | error_msgs[tag] = self._opc.GetErrorString(errors[i]) 319 | 320 | if self.trace and errors[i] != 0: self.trace('%s failed validation' % tag) 321 | 322 | client_handles.insert(0,0) 323 | valid_tags.insert(0,0) 324 | server_handles = [] 325 | errors = [] 326 | 327 | if self.trace: self.trace('AddItems(%s)' % tags2trace(valid_tags)) 328 | 329 | try: 330 | server_handles, errors = opc_items.AddItems(len(client_handles)-1, valid_tags, client_handles) 331 | except: 332 | pass 333 | 334 | valid_tags_tmp = [] 335 | server_handles_tmp = [] 336 | valid_tags.pop(0) 337 | 338 | if not self._group_server_handles.has_key(sub_group): 339 | self._group_server_handles[sub_group] = {} 340 | 341 | for i, tag in enumerate(valid_tags): 342 | if errors[i] == 0: 343 | valid_tags_tmp.append(tag) 344 | server_handles_tmp.append(server_handles[i]) 345 | self._group_server_handles[sub_group][tag] = server_handles[i] 346 | elif include_error: 347 | error_msgs[tag] = self._opc.GetErrorString(errors[i]) 348 | 349 | valid_tags = valid_tags_tmp 350 | server_handles = server_handles_tmp 351 | 352 | return valid_tags, server_handles 353 | 354 | def remove_items(tags): 355 | if self.trace: self.trace('RemoveItems(%s)' % tags2trace(['']+tags)) 356 | server_handles = [self._group_server_handles[sub_group][tag] for tag in tags] 357 | server_handles.insert(0,0) 358 | errors = [] 359 | 360 | try: 361 | errors = opc_items.Remove(len(server_handles)-1, server_handles) 362 | except pythoncom.com_error, err: 363 | error_msg = 'RemoveItems: %s' % self._get_error_str(err) 364 | raise OPCError, error_msg 365 | 366 | try: 367 | self._update_tx_time() 368 | pythoncom.CoInitialize() 369 | 370 | if include_error: 371 | sync = True 372 | 373 | if sync: 374 | update = -1 375 | 376 | tags, single, valid = type_check(tags) 377 | if not valid: 378 | raise TypeError, "iread(): 'tags' parameter must be a string or a list of strings" 379 | 380 | # Group exists 381 | if self._groups.has_key(group) and not rebuild: 382 | num_groups = self._groups[group] 383 | data_source = SOURCE_CACHE 384 | 385 | # Group non-existant 386 | else: 387 | if size: 388 | # Break-up tags into groups of 'size' tags 389 | tag_groups = [tags[i:i+size] for i in range(0, len(tags), size)] 390 | else: 391 | tag_groups = [tags] 392 | 393 | num_groups = len(tag_groups) 394 | data_source = SOURCE_DEVICE 395 | 396 | results = [] 397 | 398 | for gid in range(num_groups): 399 | if gid > 0 and pause > 0: time.sleep(pause/1000.0) 400 | 401 | error_msgs = {} 402 | opc_groups = self._opc.OPCGroups 403 | opc_groups.DefaultGroupUpdateRate = update 404 | 405 | # Anonymous group 406 | if group == None: 407 | try: 408 | if self.trace: self.trace('AddGroup()') 409 | opc_group = opc_groups.Add() 410 | except pythoncom.com_error, err: 411 | error_msg = 'AddGroup: %s' % self._get_error_str(err) 412 | raise OPCError, error_msg 413 | sub_group = group 414 | new_group = True 415 | else: 416 | sub_group = '%s.%d' % (group, gid) 417 | 418 | # Existing named group 419 | try: 420 | if self.trace: self.trace('GetOPCGroup(%s)' % sub_group) 421 | opc_group = opc_groups.GetOPCGroup(sub_group) 422 | new_group = False 423 | 424 | # New named group 425 | except: 426 | try: 427 | if self.trace: self.trace('AddGroup(%s)' % sub_group) 428 | opc_group = opc_groups.Add(sub_group) 429 | except pythoncom.com_error, err: 430 | error_msg = 'AddGroup: %s' % self._get_error_str(err) 431 | raise OPCError, error_msg 432 | self._groups[str(group)] = len(tag_groups) 433 | new_group = True 434 | 435 | opc_items = opc_group.OPCItems 436 | 437 | if new_group: 438 | opc_group.IsSubscribed = 1 439 | opc_group.IsActive = 1 440 | if not sync: 441 | if self.trace: self.trace('WithEvents(%s)' % opc_group.Name) 442 | global current_client 443 | current_client = self 444 | self._group_hooks[opc_group.Name] = win32com.client.WithEvents(opc_group, GroupEvents) 445 | 446 | tags = tag_groups[gid] 447 | 448 | valid_tags, server_handles = add_items(tags) 449 | 450 | self._group_tags[sub_group] = tags 451 | self._group_valid_tags[sub_group] = valid_tags 452 | 453 | # Rebuild existing group 454 | elif rebuild: 455 | tags = tag_groups[gid] 456 | 457 | valid_tags = self._group_valid_tags[sub_group] 458 | add_tags = [t for t in tags if t not in valid_tags] 459 | del_tags = [t for t in valid_tags if t not in tags] 460 | 461 | if len(add_tags) > 0: 462 | valid_tags, server_handles = add_items(add_tags) 463 | valid_tags = self._group_valid_tags[sub_group] + valid_tags 464 | 465 | if len(del_tags) > 0: 466 | remove_items(del_tags) 467 | valid_tags = [t for t in valid_tags if t not in del_tags] 468 | 469 | self._group_tags[sub_group] = tags 470 | self._group_valid_tags[sub_group] = valid_tags 471 | 472 | if source == 'hybrid': data_source = SOURCE_DEVICE 473 | 474 | # Existing group 475 | else: 476 | tags = self._group_tags[sub_group] 477 | valid_tags = self._group_valid_tags[sub_group] 478 | if sync: 479 | server_handles = [item.ServerHandle for item in opc_items] 480 | 481 | tag_value = {} 482 | tag_quality = {} 483 | tag_time = {} 484 | tag_error = {} 485 | 486 | # Sync Read 487 | if sync: 488 | values = [] 489 | errors = [] 490 | qualities = [] 491 | timestamps= [] 492 | 493 | if len(valid_tags) > 0: 494 | server_handles.insert(0,0) 495 | 496 | if source != 'hybrid': 497 | data_source = SOURCE_CACHE if source == 'cache' else SOURCE_DEVICE 498 | 499 | if self.trace: self.trace('SyncRead(%s)' % data_source) 500 | 501 | try: 502 | values, errors, qualities, timestamps = opc_group.SyncRead(data_source, len(server_handles)-1, server_handles) 503 | except pythoncom.com_error, err: 504 | error_msg = 'SyncRead: %s' % self._get_error_str(err) 505 | raise OPCError, error_msg 506 | 507 | for i,tag in enumerate(valid_tags): 508 | tag_value[tag] = values[i] 509 | tag_quality[tag] = qualities[i] 510 | tag_time[tag] = timestamps[i] 511 | tag_error[tag] = errors[i] 512 | 513 | # Async Read 514 | else: 515 | if len(valid_tags) > 0: 516 | if self._tx_id >= 0xFFFF: 517 | self._tx_id = 0 518 | self._tx_id += 1 519 | 520 | if source != 'hybrid': 521 | data_source = SOURCE_CACHE if source == 'cache' else SOURCE_DEVICE 522 | 523 | if self.trace: self.trace('AsyncRefresh(%s)' % data_source) 524 | 525 | try: 526 | opc_group.AsyncRefresh(data_source, self._tx_id) 527 | except pythoncom.com_error, err: 528 | error_msg = 'AsyncRefresh: %s' % self._get_error_str(err) 529 | raise OPCError, error_msg 530 | 531 | tx_id = 0 532 | start = time.time() * 1000 533 | 534 | while tx_id != self._tx_id: 535 | now = time.time() * 1000 536 | if now - start > timeout: 537 | raise TimeoutError, 'Callback: Timeout waiting for data' 538 | 539 | if self.callback_queue.empty(): 540 | pythoncom.PumpWaitingMessages() 541 | else: 542 | tx_id, handles, values, qualities, timestamps = self.callback_queue.get() 543 | 544 | for i,h in enumerate(handles): 545 | tag = self._group_handles_tag[sub_group][h] 546 | tag_value[tag] = values[i] 547 | tag_quality[tag] = qualities[i] 548 | tag_time[tag] = timestamps[i] 549 | 550 | for tag in tags: 551 | if tag_value.has_key(tag): 552 | if (not sync and len(valid_tags) > 0) or (sync and tag_error[tag] == 0): 553 | value = tag_value[tag] 554 | if type(value) == pywintypes.TimeType: 555 | value = str(value) 556 | quality = quality_str(tag_quality[tag]) 557 | timestamp = str(tag_time[tag]) 558 | else: 559 | value = None 560 | quality = 'Error' 561 | timestamp = None 562 | if include_error: 563 | error_msgs[tag] = self._opc.GetErrorString(tag_error[tag]).strip('\r\n') 564 | else: 565 | value = None 566 | quality = 'Error' 567 | timestamp = None 568 | if include_error and not error_msgs.has_key(tag): 569 | error_msgs[tag] = '' 570 | 571 | if single: 572 | if include_error: 573 | yield (value, quality, timestamp, error_msgs[tag]) 574 | else: 575 | yield (value, quality, timestamp) 576 | else: 577 | if include_error: 578 | yield (tag, value, quality, timestamp, error_msgs[tag]) 579 | else: 580 | yield (tag, value, quality, timestamp) 581 | 582 | if group == None: 583 | try: 584 | if not sync and self._group_hooks.has_key(opc_group.Name): 585 | if self.trace: self.trace('CloseEvents(%s)' % opc_group.Name) 586 | self._group_hooks[opc_group.Name].close() 587 | 588 | if self.trace: self.trace('RemoveGroup(%s)' % opc_group.Name) 589 | opc_groups.Remove(opc_group.Name) 590 | 591 | except pythoncom.com_error, err: 592 | error_msg = 'RemoveGroup: %s' % self._get_error_str(err) 593 | raise OPCError, error_msg 594 | 595 | except pythoncom.com_error, err: 596 | error_msg = 'read: %s' % self._get_error_str(err) 597 | raise OPCError, error_msg 598 | 599 | def read(self, tags=None, group=None, size=None, pause=0, source='hybrid', update=-1, timeout=5000, sync=False, include_error=False, rebuild=False): 600 | """Return list of (value, quality, time) tuples for the specified tag(s)""" 601 | 602 | tags_list, single, valid = type_check(tags) 603 | if not valid: 604 | raise TypeError, "read(): 'tags' parameter must be a string or a list of strings" 605 | 606 | num_health_tags = len([t for t in tags_list if t[:1] == '@']) 607 | num_opc_tags = len([t for t in tags_list if t[:1] != '@']) 608 | 609 | if num_health_tags > 0: 610 | if num_opc_tags > 0: 611 | raise TypeError, "read(): system health and OPC tags cannot be included in the same group" 612 | results = self._read_health(tags) 613 | else: 614 | results = self.iread(tags, group, size, pause, source, update, timeout, sync, include_error, rebuild) 615 | 616 | if single: 617 | return list(results)[0] 618 | else: 619 | return list(results) 620 | 621 | def _read_health(self, tags): 622 | """Return values of special system health monitoring tags""" 623 | 624 | self._update_tx_time() 625 | tags, single, valid = type_check(tags) 626 | 627 | time_str = time.strftime('%x %H:%M:%S') 628 | results = [] 629 | 630 | for t in tags: 631 | if t == '@MemFree': value = SystemHealth.mem_free() 632 | elif t == '@MemUsed': value = SystemHealth.mem_used() 633 | elif t == '@MemTotal': value = SystemHealth.mem_total() 634 | elif t == '@MemPercent': value = SystemHealth.mem_percent() 635 | elif t == '@DiskFree': value = SystemHealth.disk_free() 636 | elif t == '@SineWave': value = SystemHealth.sine_wave() 637 | elif t == '@SawWave': value = SystemHealth.saw_wave() 638 | 639 | elif t == '@CpuUsage': 640 | if self.cpu == None: 641 | self.cpu = SystemHealth.CPU() 642 | time.sleep(0.1) 643 | value = self.cpu.get_usage() 644 | 645 | else: 646 | value = None 647 | 648 | m = re.match('@TaskMem\((.*?)\)', t) 649 | if m: 650 | image_name = m.group(1) 651 | value = SystemHealth.task_mem(image_name) 652 | 653 | m = re.match('@TaskCpu\((.*?)\)', t) 654 | if m: 655 | image_name = m.group(1) 656 | value = SystemHealth.task_cpu(image_name) 657 | 658 | m = re.match('@TaskExists\((.*?)\)', t) 659 | if m: 660 | image_name = m.group(1) 661 | value = SystemHealth.task_exists(image_name) 662 | 663 | if value == None: 664 | quality = 'Error' 665 | else: 666 | quality = 'Good' 667 | 668 | if single: 669 | results.append((value, quality, time_str)) 670 | else: 671 | results.append((t, value, quality, time_str)) 672 | 673 | return results 674 | 675 | def iwrite(self, tag_value_pairs, size=None, pause=0, include_error=False): 676 | """Iterable version of write()""" 677 | 678 | try: 679 | self._update_tx_time() 680 | pythoncom.CoInitialize() 681 | 682 | def _valid_pair(p): 683 | if type(p) in (types.ListType, types.TupleType) and len(p) >= 2 and type(p[0]) in types.StringTypes: 684 | return True 685 | else: 686 | return False 687 | 688 | if type(tag_value_pairs) not in (types.ListType, types.TupleType): 689 | raise TypeError, "write(): 'tag_value_pairs' parameter must be a (tag, value) tuple or a list of (tag,value) tuples" 690 | 691 | if tag_value_pairs == None: 692 | tag_value_pairs = [''] 693 | single = False 694 | elif type(tag_value_pairs[0]) in types.StringTypes: 695 | tag_value_pairs = [tag_value_pairs] 696 | single = True 697 | else: 698 | single = False 699 | 700 | invalid_pairs = [p for p in tag_value_pairs if not _valid_pair(p)] 701 | if len(invalid_pairs) > 0: 702 | raise TypeError, "write(): 'tag_value_pairs' parameter must be a (tag, value) tuple or a list of (tag,value) tuples" 703 | 704 | names = [tag[0] for tag in tag_value_pairs] 705 | tags = [tag[0] for tag in tag_value_pairs] 706 | values = [tag[1] for tag in tag_value_pairs] 707 | 708 | # Break-up tags & values into groups of 'size' tags 709 | if size: 710 | name_groups = [names[i:i+size] for i in range(0, len(names), size)] 711 | tag_groups = [tags[i:i+size] for i in range(0, len(tags), size)] 712 | value_groups = [values[i:i+size] for i in range(0, len(values), size)] 713 | else: 714 | name_groups = [names] 715 | tag_groups = [tags] 716 | value_groups = [values] 717 | 718 | num_groups = len(tag_groups) 719 | 720 | status = [] 721 | 722 | for gid in range(num_groups): 723 | if gid > 0 and pause > 0: time.sleep(pause/1000.0) 724 | 725 | opc_groups = self._opc.OPCGroups 726 | opc_group = opc_groups.Add() 727 | opc_items = opc_group.OPCItems 728 | 729 | names = name_groups[gid] 730 | tags = tag_groups[gid] 731 | values = value_groups[gid] 732 | 733 | names.insert(0,0) 734 | errors = [] 735 | 736 | try: 737 | errors = opc_items.Validate(len(names)-1, names) 738 | except: 739 | pass 740 | 741 | n = 1 742 | valid_tags = [] 743 | valid_values = [] 744 | client_handles = [] 745 | error_msgs = {} 746 | 747 | for i, tag in enumerate(tags): 748 | if errors[i] == 0: 749 | valid_tags.append(tag) 750 | valid_values.append(values[i]) 751 | client_handles.append(n) 752 | error_msgs[tag] = '' 753 | n += 1 754 | elif include_error: 755 | error_msgs[tag] = self._opc.GetErrorString(errors[i]) 756 | 757 | client_handles.insert(0,0) 758 | valid_tags.insert(0,0) 759 | server_handles = [] 760 | errors = [] 761 | 762 | try: 763 | server_handles, errors = opc_items.AddItems(len(client_handles)-1, valid_tags, client_handles) 764 | except: 765 | pass 766 | 767 | valid_tags_tmp = [] 768 | valid_values_tmp = [] 769 | server_handles_tmp = [] 770 | valid_tags.pop(0) 771 | 772 | for i, tag in enumerate(valid_tags): 773 | if errors[i] == 0: 774 | valid_tags_tmp.append(tag) 775 | valid_values_tmp.append(valid_values[i]) 776 | server_handles_tmp.append(server_handles[i]) 777 | error_msgs[tag] = '' 778 | elif include_error: 779 | error_msgs[tag] = self._opc.GetErrorString(errors[i]) 780 | 781 | valid_tags = valid_tags_tmp 782 | valid_values = valid_values_tmp 783 | server_handles = server_handles_tmp 784 | 785 | server_handles.insert(0,0) 786 | valid_values.insert(0,0) 787 | errors = [] 788 | 789 | if len(valid_values) > 1: 790 | try: 791 | errors = opc_group.SyncWrite(len(server_handles)-1, server_handles, valid_values) 792 | except: 793 | pass 794 | 795 | n = 0 796 | for tag in tags: 797 | if tag in valid_tags: 798 | if errors[n] == 0: 799 | status = 'Success' 800 | else: 801 | status = 'Error' 802 | if include_error: error_msgs[tag] = self._opc.GetErrorString(errors[n]) 803 | n += 1 804 | else: 805 | status = 'Error' 806 | 807 | # OPC servers often include newline and carriage return characters 808 | # in their error message strings, so remove any found. 809 | if include_error: error_msgs[tag] = error_msgs[tag].strip('\r\n') 810 | 811 | if single: 812 | if include_error: 813 | yield (status, error_msgs[tag]) 814 | else: 815 | yield status 816 | else: 817 | if include_error: 818 | yield (tag, status, error_msgs[tag]) 819 | else: 820 | yield (tag, status) 821 | 822 | opc_groups.Remove(opc_group.Name) 823 | 824 | except pythoncom.com_error, err: 825 | error_msg = 'write: %s' % self._get_error_str(err) 826 | raise OPCError, error_msg 827 | 828 | def write(self, tag_value_pairs, size=None, pause=0, include_error=False): 829 | """Write list of (tag, value) pair(s) to the server""" 830 | 831 | if type(tag_value_pairs) in (types.ListType, types.TupleType) and type(tag_value_pairs[0]) in (types.ListType, types.TupleType): 832 | single = False 833 | else: 834 | single = True 835 | 836 | status = self.iwrite(tag_value_pairs, size, pause, include_error) 837 | 838 | if single: 839 | return list(status)[0] 840 | else: 841 | return list(status) 842 | 843 | def groups(self): 844 | """Return a list of active tag groups""" 845 | return self._groups.keys() 846 | 847 | def remove(self, groups): 848 | """Remove the specified tag group(s)""" 849 | 850 | try: 851 | pythoncom.CoInitialize() 852 | opc_groups = self._opc.OPCGroups 853 | 854 | if type(groups) in types.StringTypes: 855 | groups = [groups] 856 | single = True 857 | else: 858 | single = False 859 | 860 | status = [] 861 | 862 | for group in groups: 863 | if self._groups.has_key(group): 864 | for i in range(self._groups[group]): 865 | sub_group = '%s.%d' % (group, i) 866 | 867 | if self._group_hooks.has_key(sub_group): 868 | if self.trace: self.trace('CloseEvents(%s)' % sub_group) 869 | self._group_hooks[sub_group].close() 870 | 871 | try: 872 | if self.trace: self.trace('RemoveGroup(%s)' % sub_group) 873 | errors = opc_groups.Remove(sub_group) 874 | except pythoncom.com_error, err: 875 | error_msg = 'RemoveGroup: %s' % self._get_error_str(err) 876 | raise OPCError, error_msg 877 | 878 | del(self._group_tags[sub_group]) 879 | del(self._group_valid_tags[sub_group]) 880 | del(self._group_handles_tag[sub_group]) 881 | del(self._group_server_handles[sub_group]) 882 | del(self._groups[group]) 883 | 884 | except pythoncom.com_error, err: 885 | error_msg = 'remove: %s' % self._get_error_str(err) 886 | raise OPCError, error_msg 887 | 888 | def iproperties(self, tags, id=None): 889 | """Iterable version of properties()""" 890 | 891 | try: 892 | self._update_tx_time() 893 | pythoncom.CoInitialize() 894 | 895 | tags, single_tag, valid = type_check(tags) 896 | if not valid: 897 | raise TypeError, "properties(): 'tags' parameter must be a string or a list of strings" 898 | 899 | try: 900 | id.remove(0) 901 | include_name = True 902 | except: 903 | include_name = False 904 | 905 | if id != None: 906 | descriptions= [] 907 | 908 | if isinstance(id, list) or isinstance(id, tuple): 909 | property_id = list(id) 910 | single_property = False 911 | else: 912 | property_id = [id] 913 | single_property = True 914 | 915 | for i in property_id: 916 | descriptions.append('Property id %d' % i) 917 | else: 918 | single_property = False 919 | 920 | properties = [] 921 | 922 | for tag in tags: 923 | 924 | if id == None: 925 | descriptions = [] 926 | property_id = [] 927 | count, property_id, descriptions, datatypes = self._opc.QueryAvailableProperties(tag) 928 | 929 | # Remove bogus negative property id (not sure why this sometimes happens) 930 | tag_properties = map(None, property_id, descriptions) 931 | property_id = [p for p, d in tag_properties if p > 0] 932 | descriptions = [d for p, d in tag_properties if p > 0] 933 | 934 | property_id.insert(0, 0) 935 | values = [] 936 | errors = [] 937 | values, errors = self._opc.GetItemProperties(tag, len(property_id)-1, property_id) 938 | 939 | property_id.pop(0) 940 | values = [str(v) if type(v) == pywintypes.TimeType else v for v in values] 941 | 942 | # Replace variant id with type strings 943 | try: 944 | i = property_id.index(1) 945 | values[i] = vt[values[i]] 946 | except: 947 | pass 948 | 949 | # Replace quality bits with quality strings 950 | try: 951 | i = property_id.index(3) 952 | values[i] = quality_str(values[i]) 953 | except: 954 | pass 955 | 956 | # Replace access rights bits with strings 957 | try: 958 | i = property_id.index(5) 959 | values[i] = ACCESS_RIGHTS[values[i]] 960 | except: 961 | pass 962 | 963 | if id != None: 964 | if single_property: 965 | if single_tag: 966 | tag_properties = values 967 | else: 968 | tag_properties = [values] 969 | else: 970 | tag_properties = map(None, property_id, values) 971 | else: 972 | tag_properties = map(None, property_id, descriptions, values) 973 | tag_properties.insert(0, (0, 'Item ID (virtual property)', tag)) 974 | 975 | if include_name: tag_properties.insert(0, (0, tag)) 976 | if not single_tag: tag_properties = [tuple([tag] + list(p)) for p in tag_properties] 977 | 978 | for p in tag_properties: yield p 979 | 980 | except pythoncom.com_error, err: 981 | error_msg = 'properties: %s' % self._get_error_str(err) 982 | raise OPCError, error_msg 983 | 984 | def properties(self, tags, id=None): 985 | """Return list of property tuples (id, name, value) for the specified tag(s) """ 986 | 987 | if type(tags) not in (types.ListType, types.TupleType) and type(id) not in (types.NoneType, types.ListType, types.TupleType): 988 | single = True 989 | else: 990 | single = False 991 | 992 | props = self.iproperties(tags, id) 993 | 994 | if single: 995 | return list(props)[0] 996 | else: 997 | return list(props) 998 | 999 | def ilist(self, paths='*', recursive=False, flat=False, include_type=False): 1000 | """Iterable version of list()""" 1001 | 1002 | try: 1003 | self._update_tx_time() 1004 | pythoncom.CoInitialize() 1005 | 1006 | try: 1007 | browser = self._opc.CreateBrowser() 1008 | # For OPC servers that don't support browsing 1009 | except: 1010 | return 1011 | 1012 | paths, single, valid = type_check(paths) 1013 | if not valid: 1014 | raise TypeError, "list(): 'paths' parameter must be a string or a list of strings" 1015 | 1016 | if len(paths) == 0: paths = ['*'] 1017 | nodes = {} 1018 | 1019 | for path in paths: 1020 | 1021 | if flat: 1022 | browser.MoveToRoot() 1023 | browser.Filter = '' 1024 | browser.ShowLeafs(True) 1025 | 1026 | pattern = re.compile('^%s$' % wild2regex(path) , re.IGNORECASE) 1027 | matches = filter(pattern.search, browser) 1028 | if include_type: matches = [(x, node_type) for x in matches] 1029 | 1030 | for node in matches: yield node 1031 | continue 1032 | 1033 | queue = [] 1034 | queue.append(path) 1035 | 1036 | while len(queue) > 0: 1037 | tag = queue.pop(0) 1038 | 1039 | browser.MoveToRoot() 1040 | browser.Filter = '' 1041 | pattern = None 1042 | 1043 | path_str = '/' 1044 | path_list = tag.replace('.','/').split('/') 1045 | path_list = [p for p in path_list if len(p) > 0] 1046 | found_filter = False 1047 | path_postfix = '/' 1048 | 1049 | for i, p in enumerate(path_list): 1050 | if found_filter: 1051 | path_postfix += p + '/' 1052 | elif p.find('*') >= 0: 1053 | pattern = re.compile('^%s$' % wild2regex(p) , re.IGNORECASE) 1054 | found_filter = True 1055 | elif len(p) != 0: 1056 | pattern = re.compile('^.*$') 1057 | browser.ShowBranches() 1058 | 1059 | # Branch node, so move down 1060 | if len(browser) > 0: 1061 | try: 1062 | browser.MoveDown(p) 1063 | path_str += p + '/' 1064 | except: 1065 | if i < len(path_list)-1: return 1066 | pattern = re.compile('^%s$' % wild2regex(p) , re.IGNORECASE) 1067 | 1068 | # Leaf node, so append all remaining path parts together 1069 | # to form a single search expression 1070 | else: 1071 | p = string.join(path_list[i:], '.') 1072 | pattern = re.compile('^%s$' % wild2regex(p) , re.IGNORECASE) 1073 | break 1074 | 1075 | browser.ShowBranches() 1076 | 1077 | if len(browser) == 0: 1078 | browser.ShowLeafs(False) 1079 | lowest_level = True 1080 | node_type = 'Leaf' 1081 | else: 1082 | lowest_level = False 1083 | node_type = 'Branch' 1084 | 1085 | matches = filter(pattern.search, browser) 1086 | 1087 | if not lowest_level and recursive: 1088 | queue += [path_str + x + path_postfix for x in matches] 1089 | else: 1090 | if lowest_level: matches = [exceptional(browser.GetItemID,x)(x) for x in matches] 1091 | if include_type: matches = [(x, node_type) for x in matches] 1092 | for node in matches: 1093 | if not nodes.has_key(node): yield node 1094 | nodes[node] = True 1095 | 1096 | except pythoncom.com_error, err: 1097 | error_msg = 'list: %s' % self._get_error_str(err) 1098 | raise OPCError, error_msg 1099 | 1100 | def list(self, paths='*', recursive=False, flat=False, include_type=False): 1101 | """Return list of item nodes at specified path(s) (tree browser)""" 1102 | 1103 | nodes = self.ilist(paths, recursive, flat, include_type) 1104 | return list(nodes) 1105 | 1106 | def servers(self, opc_host='localhost'): 1107 | """Return list of available OPC servers""" 1108 | 1109 | try: 1110 | pythoncom.CoInitialize() 1111 | servers = self._opc.GetOPCServers(opc_host) 1112 | servers = [s for s in servers if s != None] 1113 | return servers 1114 | 1115 | except pythoncom.com_error, err: 1116 | error_msg = 'servers: %s' % self._get_error_str(err) 1117 | raise OPCError, error_msg 1118 | 1119 | def info(self): 1120 | """Return list of (name, value) pairs about the OPC server""" 1121 | 1122 | try: 1123 | self._update_tx_time() 1124 | pythoncom.CoInitialize() 1125 | 1126 | info_list = [] 1127 | 1128 | if self._open_serv: 1129 | mode = 'OpenOPC' 1130 | else: 1131 | mode = 'DCOM' 1132 | 1133 | info_list += [('Protocol', mode)] 1134 | 1135 | if mode == 'OpenOPC': 1136 | info_list += [('Gateway Host', '%s:%s' % (self._open_host, self._open_port))] 1137 | info_list += [('Gateway Version', '%s' % __version__)] 1138 | info_list += [('Class', self.opc_class)] 1139 | info_list += [('Client Name', self._opc.ClientName)] 1140 | info_list += [('OPC Host', self.opc_host)] 1141 | info_list += [('OPC Server', self._opc.ServerName)] 1142 | info_list += [('State', OPC_STATUS[self._opc.ServerState])] 1143 | info_list += [('Version', '%d.%d (Build %d)' % (self._opc.MajorVersion, self._opc.MinorVersion, self._opc.BuildNumber))] 1144 | 1145 | try: 1146 | browser = self._opc.CreateBrowser() 1147 | browser_type = BROWSER_TYPE[browser.Organization] 1148 | except: 1149 | browser_type = 'Not Supported' 1150 | 1151 | info_list += [('Browser', browser_type)] 1152 | info_list += [('Start Time', str(self._opc.StartTime))] 1153 | info_list += [('Current Time', str(self._opc.CurrentTime))] 1154 | info_list += [('Vendor', self._opc.VendorInfo)] 1155 | 1156 | return info_list 1157 | 1158 | except pythoncom.com_error, err: 1159 | error_msg = 'info: %s' % self._get_error_str(err) 1160 | raise OPCError, error_msg 1161 | 1162 | def ping(self): 1163 | """Check if we are still talking to the OPC server""" 1164 | try: 1165 | # Convert OPC server time to milliseconds 1166 | opc_serv_time = int(float(self._opc.CurrentTime) * 1000000.0) 1167 | if opc_serv_time == self._prev_serv_time: 1168 | return False 1169 | else: 1170 | self._prev_serv_time = opc_serv_time 1171 | return True 1172 | except pythoncom.com_error: 1173 | return False 1174 | 1175 | def _get_error_str(self, err): 1176 | """Return the error string for a OPC or COM error code""" 1177 | 1178 | hr, msg, exc, arg = err 1179 | 1180 | if exc == None: 1181 | error_str = str(msg) 1182 | else: 1183 | scode = exc[5] 1184 | 1185 | try: 1186 | opc_err_str = unicode(self._opc.GetErrorString(scode)).strip('\r\n') 1187 | except: 1188 | opc_err_str = None 1189 | 1190 | try: 1191 | com_err_str = unicode(pythoncom.GetScodeString(scode)).strip('\r\n') 1192 | except: 1193 | com_err_str = None 1194 | 1195 | # OPC error codes and COM error codes are overlapping concepts, 1196 | # so we combine them together into a single error message. 1197 | 1198 | if opc_err_str == None and com_err_str == None: 1199 | error_str = str(scode) 1200 | elif opc_err_str == com_err_str: 1201 | error_str = opc_err_str 1202 | elif opc_err_str == None: 1203 | error_str = com_err_str 1204 | elif com_err_str == None: 1205 | error_str = opc_err_str 1206 | else: 1207 | error_str = '%s (%s)' % (opc_err_str, com_err_str) 1208 | 1209 | return error_str 1210 | 1211 | def _update_tx_time(self): 1212 | """Update the session's last transaction time in the Gateway Service""" 1213 | if self._open_serv: 1214 | self._open_serv._tx_times[self._open_guid] = time.time() 1215 | 1216 | def __getitem__(self, key): 1217 | """Read single item (tag as dictionary key)""" 1218 | value, quality, time = self.read(key) 1219 | return value 1220 | 1221 | def __setitem__(self, key, value): 1222 | """Write single item (tag as dictionary key)""" 1223 | self.write((key, value)) 1224 | return 1225 | -------------------------------------------------------------------------------- /OpenOPC/opc.py: -------------------------------------------------------------------------------- 1 | ########################################################################### 2 | # 3 | # OpenOPC Command Line Client 4 | # 5 | # A cross-platform OPC-DA client built using the OpenOPC for Python 6 | # library module. 7 | # 8 | # Copyright (c) 2007-2012 Barry Barnreiter (barry_b@users.sourceforge.net) 9 | # 10 | ########################################################################### 11 | 12 | from sys import * 13 | from getopt import * 14 | from os import * 15 | import signal 16 | import sys 17 | import os 18 | import types 19 | import datetime 20 | import re, time, csv 21 | import OpenOPC 22 | 23 | try: 24 | import Pyro 25 | except ImportError: 26 | pyro_found = False 27 | else: 28 | pyro_found = True 29 | 30 | # Common function aliases 31 | 32 | write = sys.stdout.write 33 | 34 | # Initialize default settings 35 | 36 | if os.name == 'nt': 37 | opc_mode = 'dcom' 38 | else: 39 | opc_mode = 'open' 40 | 41 | opc_class = OpenOPC.OPC_CLASS 42 | client_name = OpenOPC.OPC_CLIENT 43 | opc_host = 'localhost' 44 | opc_server = OpenOPC.OPC_SERVER 45 | open_host = 'localhost' 46 | open_port = 7766 47 | 48 | action = 'read' 49 | style = 'table' 50 | append = '' 51 | num_columns = 0 52 | pipe = False 53 | verbose = False 54 | recursive = False 55 | read_function = 'async' 56 | data_source = 'hybrid' 57 | group_size = None 58 | update_rate = None 59 | timeout = 5000 60 | tx_pause = 0 61 | repeat = 1 62 | repeat_pause = None 63 | property_ids = None 64 | include_err_msg = False 65 | 66 | if environ.has_key('OPC_MODE'): opc_mode = environ['OPC_MODE'] 67 | if environ.has_key('OPC_CLASS'): opc_class = environ['OPC_CLASS'] 68 | if environ.has_key('OPC_CLIENT'): client_name = environ['OPC_CLIENT'] 69 | if environ.has_key('OPC_HOST'): opc_host = environ['OPC_HOST'] 70 | if environ.has_key('OPC_SERVER'): opc_server = environ['OPC_SERVER'] 71 | if environ.has_key('OPC_GATE_HOST'): open_host = environ['OPC_GATE_HOST'] 72 | if environ.has_key('OPC_GATE_PORT'): open_port = environ['OPC_GATE_PORT'] 73 | if environ.has_key('OPC_TIMEOUT'): timeout = int(environ['OPC_TIMEOUT']) 74 | 75 | # FUNCTION: Print comand line usage summary 76 | 77 | def usage(): 78 | print 'OpenOPC Command Line Client', OpenOPC.__version__ 79 | print 'Copyright (c) 2007-2012 Barry Barnreiter (barry_b@users.sourceforge.net)' 80 | print '' 81 | print 'Usage: opc [OPTIONS] [ACTION] [ITEM|PATH...]' 82 | print '' 83 | print 'Actions:' 84 | print ' -r, --read Read ITEM values (default action)' 85 | print ' -w, --write Write values to ITEMs (use ITEM=VALUE)' 86 | print ' -p, --properties View properties of ITEMs' 87 | print ' -l, --list List items at specified PATHs (tree browser)' 88 | print ' -f, --flat List all ITEM names (flat browser)' 89 | print ' -i, --info Display OPC server information' 90 | print ' -q, --servers Query list of available OPC servers' 91 | print ' -S, --sessions List sessions in OpenOPC Gateway Service' 92 | print '' 93 | print 'Options:' 94 | print ' -m MODE, --mode=MODE Protocol MODE (dcom, open) (default: OPC_MODE)' 95 | print ' -C CLASS,--class=CLASS OPC Automation CLASS (default: OPC_CLASS)' 96 | print ' -n NAME, --name=NAME Set OPC Client NAME (default: OPC_CLIENT)' 97 | print ' -h HOST, --host=HOST DCOM OPC HOST (default: OPC_HOST)' 98 | print ' -s SERV, --server=SERVER DCOM OPC SERVER (default: OPC_SERVER)' 99 | print ' -H HOST, --gate-host=HOST OpenOPC Gateway HOST (default: OPC_GATE_HOST)' 100 | print ' -P PORT, --gate-port=PORT OpenOPC Gateway PORT (default: OPC_GATE_PORT)' 101 | print '' 102 | print ' -F FUNC, --function=FUNC Read FUNCTION to use (sync, async)' 103 | print ' -c SRC, --source=SOURCE Set data SOURCE for reads (cache, device, hybrid)' 104 | print ' -g SIZE, --size=SIZE Group tags into SIZE items per transaction' 105 | print ' -z MSEC, --pause=MSEC Sleep MSEC milliseconds between transactions' 106 | print ' -u MSEC, --update=MSEC Set update rate for group to MSEC milliseconds' 107 | print ' -t MSEC, --timeout=MSEC Set read timeout to MSEC mulliseconds' 108 | print '' 109 | print ' -o FMT, --output=FORMAT Output FORMAT (table, values, pairs, csv, html)' 110 | print ' -L SEC, --repeat=SEC Loop ACTION every SEC seconds until stopped' 111 | print ' -y ID, --id=ID,... Retrieve only specific Property IDs' 112 | print ' -a STR, --append=STR,... Append STRINGS to each input item name' 113 | print ' -x N --rotate=N Rotate output orientation in groups of N values' 114 | print ' -v, --verbose Verbose mode showing all OPC function calls' 115 | print ' -e, --errors Include descriptive error message strings' 116 | print ' -R, --recursive List items recursively when browsing tree' 117 | print ' -, --pipe Pipe item/value list from standard input' 118 | 119 | # Helper class for handling signals (i.e. Ctrl-C) 120 | 121 | class SigHandler: 122 | def __init__(self): 123 | self.signaled = 0 124 | self.sn = None 125 | def __call__(self, sn, sf): 126 | self.sn = sn 127 | self.signaled += 1 128 | 129 | # FUNCTION: Iterable version of rotate() 130 | 131 | def irotate(data, num_columns, value_idx = 1): 132 | if num_columns == 0: 133 | for row in data: yield row 134 | return 135 | 136 | new_row = [] 137 | 138 | for i, row in enumerate(data): 139 | if type(row) not in (types.ListType, types.TupleType): 140 | value_idx = 0 141 | row = [row] 142 | 143 | new_row.append(row[value_idx]) 144 | if (i + 1) % num_columns == 0: 145 | yield new_row 146 | new_row = [] 147 | 148 | if len(new_row) > 0: 149 | yield new_row 150 | 151 | # FUNCTION: Rotate the values of every N rows to form N columns 152 | 153 | def rotate(data, num_columns, value_idx = 1): 154 | return list(irotate(data, num_columns, value_idx)) 155 | 156 | # FUNCTION: Print output in the specified style from a list of data 157 | 158 | def output(data, style = 'table', value_idx = 1): 159 | global write 160 | name_idx = 0 161 | 162 | # Cast value to a stirng (trap Unicode errors) 163 | def to_str(value): 164 | try: 165 | if type(value) == types.FloatType: 166 | return '%.4f' % value 167 | else: 168 | return str(value) 169 | except: 170 | return '' 171 | 172 | # Generator passed (single row passed at a time) 173 | if type(data) == types.GeneratorType: 174 | generator = True 175 | pad_length = [] 176 | 177 | # List passed (multiple rows passed all at once) 178 | elif type(data) in (types.ListType, types.TupleType): 179 | generator = False 180 | 181 | if len(data) == 0: return 182 | 183 | if type(data[0]) not in (types.ListType, types.TupleType): 184 | data = [[e] for e in data] 185 | 186 | if style == 'table' or style == '': 187 | pad_length = [] 188 | num_columns = len(data[0]) 189 | for i in range(num_columns-1): 190 | pad_length.append(len(max([to_str(row[i]) for row in data], key=len)) + 5) 191 | pad_length.append(0) 192 | else: 193 | raise TypeError, "output(): 'data' parameter must be a list or a generator" 194 | 195 | if style == 'html': 196 | write('\n') 197 | 198 | rows = [] 199 | 200 | for i, row in enumerate(data): 201 | rows.append(row) 202 | 203 | if style == 'values': 204 | write('%s' % str(row[value_idx])) 205 | elif style == 'pairs': 206 | write('%s,%s' % (row[name_idx], row[value_idx])) 207 | else: 208 | 209 | if generator and (style == 'table' or style == ''): 210 | 211 | # Convert single value into a single element list, thus making it 212 | # represent a 1-column wide table. 213 | if type(row) not in (types.ListType, types.TupleType): 214 | row = [row] 215 | 216 | num_columns = len(row) 217 | 218 | # Allow columns widths to always grow wider, but never shrink. 219 | # Unfortunetly we won't know the required width until the generator is finished! 220 | for k in range(num_columns-1): 221 | new_length = len(to_str(row[k])) 222 | if i == 0: 223 | pad_length.append(new_length + 5) 224 | else: 225 | if new_length - pad_length[k] > 0: pad_length[k] = new_length 226 | if i == 0: 227 | pad_length.append(0) 228 | 229 | for j, item in enumerate(row): 230 | if style == 'csv': 231 | if j > 0: write(',') 232 | write('%s' % to_str(item)) 233 | elif style == 'html': 234 | if j == 0: write(' \n') 235 | if len(to_str(item)) < 40: 236 | write(' \n' % to_str(item)) 237 | else: 238 | write(' \n' % to_str(item)) 239 | if j == len(row)-1: write(' ') 240 | else: 241 | if num_columns > 1: 242 | write('%s' % to_str(item).ljust(pad_length[j])) 243 | else: 244 | write('%s' % to_str(item)) 245 | 246 | write('\n') 247 | 248 | if style == 'html': 249 | write('
%s%s
') 250 | 251 | return rows 252 | 253 | # FUNCTION: Convert Unix time to formatted time string 254 | 255 | def time2str(t): 256 | d = datetime.datetime.fromtimestamp(t) 257 | return d.strftime('%x %H:%M:%S') 258 | 259 | 260 | ######## MAIN ######## 261 | 262 | # Parse command line arguments 263 | 264 | if argv.count('-') > 0: 265 | argv[argv.index('-')] = '--pipe' 266 | pipe = True 267 | 268 | try: 269 | opts, args = gnu_getopt(argv[1:], 'rwlpfiqRSevx:m:C:H:P:c:h:s:L:F:z:o:a:u:t:g:y:n:', ['read','write','list','properties','flat','info','mode=','gate-host=','gate-port=','class=','host=','server=','output=','pause=','pipe','servers','sessions','repeat=','function=','append=','update=','timeout=','size=','source=','id=','verbose','recursive','rotate=','errors','name=']) 270 | except GetoptError: 271 | usage() 272 | exit() 273 | 274 | for o, a in opts: 275 | if o in ['-m', '--mode'] : opc_mode = a 276 | if o in ['-C', '--class'] : opc_class = a 277 | if o in ['-n', '--name'] : client_name = a 278 | if o in ['-H', '--open-host'] : open_host = a; opc_mode = 'open' 279 | if o in ['-P', '--open-port'] : open_port = a; opc_mode = 'open' 280 | if o in ['-h', '--host'] : opc_host = a 281 | if o in ['-s', '--server'] : opc_server = a 282 | 283 | if o in ['-r', '--read'] : action = 'read' 284 | if o in ['-w', '--write'] : action = 'write' 285 | if o in ['-l', '--list'] : action = 'list' 286 | if o in ['-f', '--flat'] : action = 'flat' 287 | if o in ['-p', '--properties'] : action = 'properties' 288 | if o in ['-i', '--info'] : action = 'info' 289 | if o in ['-q', '--servers'] : action = 'servers' 290 | if o in ['-S', '--sessions'] : action = 'sessions' 291 | 292 | if o in ['-o', '--output'] : style = a 293 | if o in ['-L', '--repeat'] : repeat_pause = float(a); 294 | if o in ['-F', '--function'] : read_function = a; 295 | if o in ['-z', '--pause'] : tx_pause = int(a) 296 | if o in ['-u', '--update'] : update_rate = int(a) 297 | if o in ['-t', '--timeout'] : timeout = int(a) 298 | if o in ['-g', '--size'] : group_size = int(a) 299 | if o in ['-c', '--source'] : data_source = a 300 | if o in ['-y', '--id'] : property_ids = a 301 | if o in ['-a', '--append'] : append = a 302 | if o in ['-x', '--rotate'] : num_columns = int(a) 303 | if o in ['-v', '--verbose'] : verbose = True 304 | if o in ['-e', '--errors'] : include_err_msg = True 305 | if o in ['-R', '--recursive'] : recursive = True 306 | if o in ['--pipe'] : pipe = True 307 | 308 | # Check validity of command line options 309 | 310 | if num_columns > 0 and style in ('values', 'pairs'): 311 | print "'%s' style format may not be used with rotate" % style 312 | exit() 313 | 314 | if opc_mode not in ('open', 'dcom'): 315 | print "'%s' is not a valid protocol mode (options: dcom, open)" % opc_mode 316 | exit() 317 | 318 | if opc_mode == 'dcom' and not OpenOPC.win32com_found: 319 | print "win32com modules required when using DCOM protocol mode (http://pywin32.sourceforge.net/)" 320 | exit() 321 | 322 | if opc_mode == 'open' and not pyro_found: 323 | print "Pyro module required when using Open protocol mode (http://pyro.sourceforge.net)" 324 | exit() 325 | 326 | if style not in ('table', 'values', 'pairs', 'csv', 'html'): 327 | print "'%s' is not a valid style format (options: table, values, pairs, csv, html)" % style 328 | exit() 329 | 330 | if read_function not in ('sync', 'async'): 331 | print "'%s' is not a valid read function (options: sync, async)" % read_function 332 | exit() 333 | else: 334 | sync = (read_function == 'sync') 335 | 336 | if data_source not in ('cache', 'device', 'hybrid'): 337 | print "'%s' is not a valid data source mode (options: cache, device, hybrid)" % data_source 338 | exit() 339 | 340 | if len(argv[1:]) == 0 or argv[1] == '/?' or argv[1] == '--help': 341 | usage() 342 | exit() 343 | 344 | if opc_server == '' and action not in ('servers', 'sessions'): 345 | print 'OPC server name missing: use -s option or set OPC_SERVER environment variable' 346 | exit() 347 | 348 | if data_source in ('cache', 'hybrid') and read_function == 'async' and update_rate == None and repeat_pause != None: 349 | update_rate = int(repeat_pause * 1000.0) 350 | elif update_rate == None: 351 | update_rate = -1 352 | 353 | # Build tag list 354 | 355 | tags = [] 356 | 357 | # Tag list passed via standrd input 358 | if pipe: 359 | try: 360 | reader = csv.reader(sys.stdin) 361 | tags_nested = list(reader) 362 | except KeyboardInterrupt: 363 | exit() 364 | 365 | tags = [line[0] for line in tags_nested if len(line) > 0] 366 | if len(tags) == 0: 367 | print 'Input stream must contain ITEMs (one per line)' 368 | exit() 369 | 370 | if action == 'write': 371 | try: 372 | tag_value_pairs = [(item[0], item[1]) for item in tags_nested] 373 | except IndexError: 374 | print 'Write input must be in ITEM,VALUE (CSV) format' 375 | exit() 376 | 377 | # Tag list passed via command line arguments 378 | else: 379 | for a in args: 380 | tags.append(a.replace('+', ' ')) 381 | tags_nested = [[tag] for tag in tags] 382 | 383 | if action == 'write': 384 | if len(tags) % 2 == 0: 385 | tag_value_pairs = [(tags[i], tags[i+1]) for i in range(0, len(tags), 2)] 386 | else: 387 | print 'Write arguments must be supplied in ITEM=VALUE or ITEM VALUE format' 388 | exit() 389 | 390 | if len(append) > 0: 391 | tags = [t + a for t in tags for a in append.split(',')] 392 | 393 | if property_ids != None: 394 | try: 395 | property_ids = [int(p) for p in property_ids.split(',')] 396 | except ValueError: 397 | print 'Property ids must be numeric' 398 | exit() 399 | 400 | if action in ('read','write') and not pipe and len(tags) == 0: 401 | usage() 402 | exit() 403 | 404 | # Were only health monitoring "@" tags supplied? 405 | 406 | health_tags = [t for t in tags if t[:1] == '@'] 407 | opc_tags = [t for t in tags if t[:1] != '@'] 408 | if len(health_tags) > 0 and len(opc_tags) == 0: 409 | health_only = True 410 | else: 411 | health_only = False 412 | 413 | # Establish signal handler for keyboard interrupts 414 | 415 | sh = SigHandler() 416 | signal.signal(signal.SIGINT,sh) 417 | if os.name == 'nt': 418 | signal.signal(signal.SIGBREAK,sh) 419 | signal.signal(signal.SIGTERM,sh) 420 | 421 | # ACTION: List active sessions in OpenOPC service 422 | 423 | if action == 'sessions': 424 | print ' %-38s %-18s %-18s' % ('Remote Client', 'Start Time', 'Last Transaction') 425 | try: 426 | for guid, host, init_time, tx_time in OpenOPC.get_sessions(open_host, open_port): 427 | print ' %-38s %-18s %-18s' % (host, time2str(init_time), time2str(tx_time)) 428 | except: 429 | error_msg = sys.exc_info()[1] 430 | print "Cannot connect to OpenOPC service at %s:%s - %s" % (open_host, open_port, error_msg) 431 | exit() 432 | 433 | # Connect to OpenOPC service (Open mode) 434 | 435 | if opc_mode == 'open': 436 | try: 437 | opc = OpenOPC.open_client(open_host, open_port) 438 | except: 439 | error_msg = sys.exc_info()[1] 440 | print "Cannot connect to OpenOPC Gateway Service at %s:%s - %s" % (open_host, open_port, error_msg) 441 | exit() 442 | 443 | # Dispatch to COM class (DCOM mode) 444 | 445 | else: 446 | try: 447 | opc = OpenOPC.client(opc_class, client_name) 448 | except OpenOPC.OPCError, error_msg: 449 | print "Failed to initialize an OPC Automation Class from the search list '%s' - %s" % (opc_class, error_msg) 450 | exit() 451 | 452 | # Connect to OPC server 453 | 454 | if action not in ['servers'] and not health_only: 455 | try: 456 | opc.connect(opc_server, opc_host) 457 | except OpenOPC.OPCError, error_msg: 458 | if opc_mode == 'open': error_msg = error_msg[0] 459 | print "Connect to OPC server '%s' on '%s' failed - %s" % (opc_server, opc_host, error_msg) 460 | exit() 461 | 462 | # Perform requested action... 463 | 464 | start_time = time.time() 465 | 466 | # ACTION: Read Items 467 | 468 | if action == 'read': 469 | if group_size and len(tags) > group_size and opc_mode == 'dcom': 470 | opc_read = opc.iread 471 | rotate = irotate 472 | else: 473 | opc_read = opc.read 474 | 475 | if verbose: 476 | def trace(msg): print msg 477 | opc.set_trace(trace) 478 | 479 | success_count = 0 480 | total_count = 0 481 | com_connected = True 482 | pyro_connected = True 483 | 484 | while not sh.signaled: 485 | 486 | try: 487 | if not pyro_connected: 488 | opc = OpenOPC.open_client(open_host, open_port) 489 | opc.connect(opc_server, opc_host) 490 | opc_read = opc.read 491 | pyro_connected = True 492 | com_connected = True 493 | 494 | if not com_connected: 495 | opc.connect(opc_server, opc_host) 496 | com_connected = True 497 | 498 | status = output(rotate(opc_read(tags, 499 | group='test', 500 | size=group_size, 501 | pause=tx_pause, 502 | source=data_source, 503 | update=update_rate, 504 | timeout=timeout, 505 | sync=sync, 506 | include_error=include_err_msg), 507 | num_columns), style) 508 | 509 | except OpenOPC.TimeoutError, error_msg: 510 | if opc_mode == 'open': error_msg = error_msg[0] 511 | print error_msg 512 | success = False 513 | 514 | except OpenOPC.OPCError, error_msg: 515 | if opc_mode == 'open': error_msg = error_msg[0] 516 | print error_msg 517 | success = False 518 | 519 | if opc.ping(): 520 | com_connected = True 521 | else: 522 | com_connected = False 523 | 524 | except (Pyro.errors.ConnectionClosedError, Pyro.errors.ProtocolError), error_msg: 525 | print 'Gateway Service: %s' % error_msg 526 | success = False 527 | pyro_connected = False 528 | 529 | except TypeError, error_msg: 530 | if opc_mode == 'open': error_msg = error_msg[0] 531 | print error_msg 532 | break 533 | 534 | except IOError: 535 | opc.close() 536 | exit() 537 | 538 | else: 539 | success = True 540 | 541 | if success and num_columns == 0: 542 | success_count += len([s for s in status if s[2] != 'Error']) 543 | total_count += len(status) 544 | 545 | if repeat_pause != None: 546 | try: 547 | time.sleep(repeat_pause) 548 | except IOError: 549 | break 550 | else: 551 | break 552 | 553 | if style == 'table' and num_columns == 0: 554 | print '\nRead %d of %d items (%.2f seconds)' % (success_count, total_count, time.time() - start_time) 555 | 556 | try: 557 | opc.remove('test') 558 | except OpenOPC.OPCError, error_msg: 559 | if opc_mode == 'open': error_msg = error_msg[0] 560 | print error_msg 561 | 562 | # ACTION: Write Items 563 | 564 | elif action == 'write': 565 | if group_size and len(tags) > group_size and opc_mode == 'dcom': 566 | opc_write = opc.iwrite 567 | rotate = irotate 568 | else: 569 | opc_write = opc.write 570 | 571 | try: 572 | status = output(rotate(opc_write(tag_value_pairs, 573 | size=group_size, 574 | pause=tx_pause, 575 | include_error=include_err_msg), 576 | num_columns), style) 577 | 578 | except OpenOPC.OPCError, error_msg: 579 | if opc_mode == 'open': error_msg = error_msg[0] 580 | print error_msg 581 | 582 | if style == 'table' and num_columns == 0: 583 | success = len([s for s in status if s[1] != 'Error']) 584 | print '\nWrote %d of %d items (%.2f seconds)' % (success, len(tag_value_pairs), time.time() - start_time) 585 | 586 | # ACTION: List Items (Tree Browser) 587 | 588 | elif action == 'list': 589 | if opc_mode == 'open': 590 | opc_list = opc.list 591 | else: 592 | opc_list = opc.ilist 593 | rotate = irotate 594 | 595 | try: 596 | output(rotate(opc_list(tags, recursive=recursive), num_columns), style) 597 | except OpenOPC.OPCError, error_msg: 598 | if opc_mode == 'open': error_msg = error_msg[0] 599 | print error_msg 600 | 601 | # ACTION: List Items (Flat Browser) 602 | 603 | elif action == 'flat': 604 | try: 605 | output(opc.list(tags, flat=True), style) 606 | except OpenOPC.OPCError, error_msg: 607 | if opc_mode == 'open': error_msg = error_msg[0] 608 | print error_msg 609 | 610 | # ACTION: Item Properties 611 | 612 | elif action == 'properties': 613 | if opc_mode == 'open': 614 | opc_properties = opc.properties 615 | else: 616 | opc_properties = opc.iproperties 617 | rotate = irotate 618 | 619 | if property_ids != None: 620 | value_idx = 2 621 | else: 622 | value_idx = 3 623 | 624 | try: 625 | output(rotate(opc_properties(tags, property_ids), num_columns, value_idx), style, value_idx) 626 | except OpenOPC.OPCError, error_msg: 627 | if opc_mode == 'open': error_msg = error_msg[0] 628 | print error_msg 629 | 630 | # ACTION: Server Info 631 | 632 | elif action == 'info': 633 | try: 634 | output(rotate(opc.info(), num_columns), style) 635 | except OpenOPC.OPCError, error_msg: 636 | if opc_mode == 'open': error_msg = error_msg[0] 637 | print error_msg 638 | 639 | # ACTION: List Servers 640 | 641 | elif action == 'servers': 642 | try: 643 | output(rotate(opc.servers(opc_host), num_columns), style) 644 | except OpenOPC.OPCError, error_msg: 645 | if opc_mode == 'open': error_msg = error_msg[0] 646 | print "Error getting server list from '%s' - %s" % (opc_host, error_msg) 647 | 648 | # Disconnect from OPC Server 649 | 650 | try: 651 | opc.close() 652 | except OpenOPC.OPCError, error_msg: 653 | if opc_mode == 'open': error_msg = error_msg[0] 654 | print error_msg 655 | -------------------------------------------------------------------------------- /README-UNIX.txt: -------------------------------------------------------------------------------- 1 | Note that when using OpenOPC on a non-Windows system you must be running the 2 | OpenOPC Gateway Service (OpenOPCService.py) on a Windows box somewhere on 3 | your network. The OpenOPC Gateway Service then acts as a proxy for all 4 | your OPC calls. 5 | 6 | For example, if your Windows node which is running the Gateway Service 7 | had the IP address of 192.168.1.20 then you would use the OpenOPC command 8 | line client like this.... 9 | 10 | opc.py -H 192.168.1.20 -s Matrikon.OPC.Simulation -r Random.Real4 11 | 12 | Or when using the OpenOPC.py module inside your own Python code, you 13 | would create your opc object like this.... 14 | 15 | import OpenOPC 16 | opc = opc.open_client('192.168.1.20') 17 | opc.connect('Matrikon.OPC.Simulation') 18 | 19 | The above examples all assume that the OPC server and the OpenOPC 20 | Gateway Service are both installed on the same box. This is the 21 | recommended configuration in order to avoid any DCOM security issues. 22 | 23 | If you downloaded this source code only version of OpenOPC for Unix, it 24 | is recommended you also download one of the win32 labeld downloads 25 | and use the pre-complied OpenOPCService.exe executible for Windows. 26 | This file is self contained and does not require that Python 27 | be installed on your Windows box. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | OpenOPC 2 | ======= 3 | 4 | This is a clone of http://openopc.sourceforge.net, with modifications to make it use distutils 5 | 6 | 7 | 8 | About OpenOPC 9 | ------------------- 10 | OpenOPC for Python is a free, open source OPC (OLE for Process Control) toolkit designed for use with the popular Python programming language. The unique features that set it apart from the many commercially available OPC toolkits include... 11 | 12 | Easy to use 13 | ------------------ 14 | Because the OpenOPC library implements a minimal number of Python functions which may be chained together in a variety of ways, the library is simple to learn and easy to remember. In its simplest form, you can read and write OPC items as easily as any variable in your Python program... 15 | print opc['Square Waves.Int4'] 16 | opc['Square Waves.Real8'] = 100.0 17 | Cross platform support 18 | 19 | OpenOPC works with both Windows and non-Windows platforms. It has been tested with Windows, Linux, and Mac OS X. 20 | Functional programming style 21 | 22 | OpenOPC allows OPC calls to be chained together in an elegant, functional programming style. For example, you can read the values of all items matching a wildcard pattern using a single line of Python code! 23 | opc.read(opc.list('Square Waves.*')) 24 | Designed for dynamic languages 25 | 26 | Most OPC toolkits today are designed for use with static system languages (such as C++ or C#), providing a close mapping to the underlying Win32 COM methods. OpenOPC discards this cumbersome model and instead attempts to take advantage of the dynamic language features provided by Python. 27 | 28 | OpenOPC is also one of the very few OPC-DA toolkits available for any dynamic language, and future support is planned for Ruby. 29 | EXAMPLE: Minimal working program 30 | 31 | import OpenOPC 32 | opc = OpenOPC.client() 33 | opc.connect('Matrikon.OPC.Simulation') 34 | print opc['Square Waves.Real8'] 35 | opc.close() 36 | 37 | This project utilizes the de facto OPC-DA (Win32 COM-based) industrial automation standard. If you are looking for an OPC XML-DA library for Python, then please visit the PyOPC project. 38 | 39 | 40 | OpenOPC Gateway Service 41 | --------------------------------- 42 | The Gateway Service is an optional Windows service which handles all the Win32 COM/DCOM calls used by the OpenOPC library module. This offers the following potential advantages: 43 | 44 | Avoidance of DCOM security headaches and firewall issues when running the OPC client and OPC server on different nodes. 45 | The ability to write OPC client applications that can run on non-Windows platforms such as Linux, Unix, and Mac OS X. 46 | Installing the service 47 | 48 | C:\OpenOPC\bin> OpenOPCService.exe -install 49 | Installing service OpenOpcService 50 | Service installed 51 | Starting the service 52 | 53 | C:\OpenOPC\bin> net start zzzOpenOpcService 54 | Stopping the service 55 | 56 | C:\OpenOPC\bin> net stop zzzOpenOpcService 57 | 58 | 59 | **NOTE**: If you want to use the OpenOPC Gateway service from another machine as a client and are using OpenOPC 1.2 then you must 60 | set the environment variable in windows and restart the gateway service: 61 | 62 | OPC_GATE_HOST=x.x.x.x 63 | 64 | Reference: 65 | http://sourceforge.net/p/openopc/discussion/709251/thread/b0216f58/ 66 | 67 | 68 | OpenOPC Library Tutorial 69 | ------------------------------- 70 | 71 | The best way to learn the OpenOPC library is by trying it interactively from the Python Shell. The following examples use the Matrikon OPC Simulation Server which you can download for free from the company's website. We recommended you use a simulation server such as this one while learning OpenOPC as opposed to testing using a "live" OPC server. 72 | 73 | #### 1. Import the OpenOPC module 74 | 75 | Start by making the OpenOPC library available to your application. This imports the OpenOPC.py module file located in the lib/site-packages/ directory. 76 | 77 | >>> import OpenOPC 78 | 79 | #### 2. Create OpenOPC instance (DCOM mode) 80 | 81 | DCOM mode is used to talk directly to OPC servers without the need for the OpenOPC Gateway Service. This mode is only available to Windows clients. 82 | 83 | >>> opc = OpenOPC.client() 84 | 85 | #### 2. Create OpenOPC instance (Open mode) 86 | 87 | In Open mode a connection is made to the OpenOPC Gateway Service running on the specified node. This mode is available to both Windows and non-Windows clients. 88 | 89 | 90 | >>> opc = OpenOPC.open_client('localhost') 91 | 92 | #### 3. Getting a list of available OPC servers 93 | 94 | 95 | >>> opc.servers() 96 | ['Matrikon.OPC.Simulation.1', 'Kepware.KEPServerEX.V4'] 97 | 98 | 99 | #### 4. Connect to OPC Server 100 | 101 | Connect to the specified OPC server. The function returns True if the connection was successful, False on failure. 102 | 103 | >>> opc.connect('Matrikon.OPC.Simulation') 104 | True 105 | 106 | If the OPC server is running on a different node, you can include the optional host parameter... 107 | 108 | >>> opc.connect('Matrikon.OPC.Simulation', 'localhost') 109 | True 110 | 111 | #### 5. Reading a single item 112 | 113 | Read the specified OPC item. The function returns a (value, quality, timestamp) tuple. If the call fails, the quality will be set to 'Error'. 114 | 115 | >>> opc.read('Random.Int4') 116 | (19169, 'Good', '06/24/07 15:56:11') 117 | >>> value, quality, time = opc.read('Random.Int4') 118 | 119 | When using the special short form of the function, only the value portion is returned. If any problems are encountered, value will be set to None. 120 | 121 | >>> value = opc['Random.Int4'] 122 | 123 | #### 6. Reading multiple items 124 | 125 | Multiple values may be read with a single call by passing a list of item names. Whenever a list is provided as input, the read function returns back a list of (name, value, quality, time) tuples. 126 | 127 | >>> opc.read( ['Random.Int2', 'Random.Real4', 'Random.String'] ) 128 | [('Random.Int2', 28145, 'Good', '06/24/07 17:44:43'), ('Random.Real4', 19025.2324, 'Good', '06/24/07 17:44:43'), ('Random.String', 'your', 'Good', '06/24/07 17:44:43')] 129 | 130 | 131 | There is a special version of read function called iread (Iterative Read). iread returns a Python generator which can be used to iterate through the returned results, item by item. 132 | 133 | for name, value, quality, time in opc.iread( ['Random.Int2', 'Random.Int4'] ): 134 | print name, value 135 | 136 | 137 | #### 7. Reading items using OPC Groups 138 | 139 | For best performance it is often necessary to place the items into a named group, then repeatedly request the group's values be updated. Including both the item list along with a group name will cause a new group to be defined and an initial read to be preformed. 140 | 141 | >>> tags = ['Random.String', 'Random.Int4', 'Random.Real4'] 142 | >>> opc.read(tags, group='test') 143 | [('Random.String', 'options', 'Good', '06/24/07 23:38:24'), ('Random.Int4', 31101, 'Good', '06/24/07 23:38:24'), ('Random.Real4', 19933.958984375, 'Good', '06/24/07 23:38:24')] 144 | 145 | Once the group has been defined, you can re-read the items in the group by supplying only the group name. You can repeat this call as often as necessary. 146 | 147 | >>> opc.read(group='test') 148 | [('Random.String', 'clients', 'Good', '06/24/07 23:38:30'), ('Random.Int4', 26308, 'Good', '06/24/07 23:38:30'), ('Random.Real4', 13846.63671875, 'Good', '06/24/07 23:38:30')] 149 | 150 | When you are done using the group, be sure to remove it. This will free up any allocated resources. If the removal was successful True will be returned, otherwise False. 151 | 152 | >>> opc.remove('test') 153 | True 154 | 155 | 156 | #### 8. Writing a single item 157 | 158 | Writing a single item can be accomplished by submitting a (name, value) tuple to the write function. If the write was successful True is returned, or False on failure. 159 | 160 | >>> opc.write( ('Triangle Waves.Real8', 100.0) ) 161 | 'Success' 162 | You can also use the short form... 163 | >>> opc['Triangle Waves.Real8'] = 100.0 164 | 165 | 166 | #### 9. Writing multiple items 167 | 168 | To write multiple items at once, submit a list of (name, value) tuples. The function returns a list of (name, status) tuples letting you know for each item name if the write attempt was successful or not. 169 | 170 | >>> opc.write( [('Triangle Waves.Real4', 10.0), ('Random.String', 20.0)] ) 171 | [('Triangle Waves.Real4', 'Success'), ('Random.String', 'Error')] 172 | 173 | The iwrite function returns a generator designed for iterating through the return statuses item by item... 174 | 175 | for item, status in opc.iwrite( [('Triangle Waves.Real4', 10.0), ('Random.String', 20.0)] ): 176 | print item, status 177 | 178 | #### 10. Getting error message strings 179 | 180 | Including the optional include_error=True parameter will cause many of the OpenOPC functions to append a descriptive error message to the end of each item tuple. In the case of the write function, it will return (name, status, error) tuples. 181 | 182 | >>> opc.write( [('Triangle Waves.Real4', 10.0), ('Random.Int4', 20.0)], include_error=True) 183 | [('Triangle Waves.Real4', 'Success', 'The operation completed successfully'), ('Random.Int4', 'Error', "The item's access rights do not allow the operation")] 184 | 185 | #### 11. Retrieving item properties 186 | 187 | Requesting properties for a single item returns a list of (id, description, value) tuples. Each tuple in the list represents a single property. 188 | >>> opc.properties('Random.Int4') 189 | [(1, 'Item Canonical DataType', 'VT_I4'), (2, 'Item Value', 491), (3, 'Item Quality', 'Good'), (4, 'Item Timestamp', '06/25/07 02:24:44'), (5, 'Item Access Rights', 'Read'), (6, 'Server Scan Rate', 100.0), (7, 'Item EU Type', 0), (8, 'Item EUInfo', None), (101, 'Item Description', 'Random value.')] 190 | 191 | If a list of items is submitted, the item name will be appended to the beginning of each tuple to produce a list of (name, id, description, value) tuples. 192 | 193 | >>> opc.properties( ['Random.Int2', 'Random.Int4', 'Random.String'] ) 194 | [('Random.Int2', 1, 'Item Canonical DataType', 'VT_I2'), ('Random.Int2', 2, 'Item Value', 4827), ('Random.Int2', 3, 'Item Quality', 'Good'), ('Random.Int2', 4, 'Item Timestamp', '06/25/07 02:35:28'), ('Random.Int2', 5, 'Item Access Rights', 'Read'), ('Random.Int2', 6, 'Server Scan Rate', 100.0), ('Random.Int2', 7, 'Item EU Type', 0), ('Random.Int2', 8, 'Item EUInfo', None), ('Random.Int2', 101, 'Item Description', 'Random value.'), ('Random.Int4', 1, 'Item Canonical DataType', 'VT_I4'), ('Random.Int4', 2, 'Item Value', 14604), ('Random.Int4', 3, 'Item Quality', 'Good'), ('Random.Int4', 4, 'Item Timestamp', '06/25/07 02:35:28'), ('Random.Int4', 5, 'Item Access Rights', 'Read'), ('Random.Int4', 6, 'Server Scan Rate', 100.0), ('Random.Int4', 7, 'Item EU Type', 0), ('Random.Int4', 8, 'Item EUInfo', None), ('Random.Int4', 101, 'Item Description', 'Random value.'), ('Random.String', 1, 'Item Canonical DataType', 'VT_BSTR'), ('Random.String', 2, 'Item Value', 'profit...'), ('Random.String', 3, 'Item Quality', 'Good'), ('Random.String', 4, 'Item Timestamp', '06/25/07 02:35:28'), ('Random.String', 5, 'Item Access Rights', 'Read'), ('Random.String', 6, 'Server Scan Rate', 100.0), ('Random.String', 7, 'Item EU Type', 0), ('Random.String', 8, 'Item EUInfo', None), ('Random.String', 101, 'Item Description', 'Random value.')] 195 | 196 | The optional id parameter can be used to limit the returned value to that of a single property... 197 | 198 | >>> opc.properties('Random.Int4', id=1) 199 | 'VT_I4' 200 | 201 | Like other OpenOPC function calls, providing a list of items causes the item names to be included in the output... 202 | 203 | >>> opc.properties( ['Random.Int2', 'Random.Int4', 'Random.String'], id=1) 204 | [('Random.Int2', 'VT_I2'), ('Random.Int4', 'VT_I4'), ('Random.String', 'VT_BSTR')] 205 | 206 | The id parameter can also be used to specify a list of ids... 207 | 208 | >>> opc.properties('Random.Int4', id=(1,2,5)) 209 | [(1, 'VT_I4'), (2, 1869), (5, 'Read')] 210 | 211 | 212 | #### 12. Getting a list of available items 213 | 214 | List nodes at the root of the tree... 215 | 216 | >>> opc.list() 217 | ['Simulation Items', 'Configured Aliases'] 218 | 219 | List nodes under the Simulation Items branch... 220 | 221 | >>> opc.list('Simulation Items') 222 | ['Bucket Brigade', 'Random', 'Read Error', 'Saw-toothed Waves', 'Square Waves', 'Triangle Waves', 'Write Error', 'Write Only'] 223 | 224 | Use the "." character as a seperator between branch names... 225 | 226 | >>> opc.list('Simulation Items.Random') 227 | ['Random.ArrayOfReal8', 'Random.ArrayOfString', 'Random.Boolean', 'Random.Int1', 'Random.Int2', 'Random.Int4', 'Random.Money', 'Random.Qualities', 'Random.Real4', 'Random.Real8', 'Random.String', 'Random.Time', 'Random.UInt1', 'Random.UInt2', 'Random.UInt4'] 228 | 229 | You can use Unix and DOS style wildcards... 230 | 231 | >>> opc.list('Simulation Items.Random.*Real*') 232 | ['Random.ArrayOfReal8', 'Random.Real4', 'Random.Real8'] 233 | 234 | If recursive=True is included, you can include wildcards in multiple parts of the path. The function will go thru the entire tree returning all children (leaf nodes) which match. 235 | 236 | >>> opc.list('Sim*.R*.Real*', recursive=True) 237 | ['Random.Real4', 'Random.Real8', 'Read Error.Real4', 'Read Error.Real8'] 238 | 239 | Including the optional flat=True parameter flattens out the entire tree into leaf nodes, freeing you from needing to be concerned with the hierarchical structure. (Note that this function is not implemented consistantly in many OPC servers) 240 | 241 | >>> opc.list('*.Real4', flat=True) 242 | ['Bucket Brigade.Real4', 'Random.Real4', 'Read Error.Real4', 'Saw-toothed Waves.Real4', 'Square Waves.Real4', 'Triangle Waves.Real4', 'Write Error.Real4', 'Write Only.Real4'] 243 | 244 | You can also submit a list of item search patterns. The returned results will be a union of the matching nodes. 245 | 246 | >>> opc.list(('Simulation Items.Random.*Int*', 'Simulation Items.Random.Real*')) 247 | ['Random.Int1', 'Random.Int2', 'Random.Int4', 'Random.UInt1', 'Random.UInt2', 'Random.UInt4', 'Random.Real4', 'Random.Real8'] 248 | 249 | 250 | #### 13. Retrieving OPC server information 251 | 252 | >>> opc.info() 253 | [('Host', 'localhost'), ('Server', 'Matrikon.OPC.Simulation'), ('State', 'Running'), ('Version', '1.1 (Build 307)'), ('Browser', 'Hierarchical'), ('Start Time', '06/24/07 13:50:54'), ('Current Time', '06/24/07 18:30:11'), ('Vendor', 'Matrikon Consulting Inc (780) 448-1010 http://www.matrikon.com')] 254 | 255 | 256 | #### 14. Combine functions together 257 | 258 | The output from many of the OpenOPC functions can be used as input to other OpenOPC functions. This allows you to employ a functional programming style which is concise and doesn't require the use of temporary variables. 259 | 260 | Read the values of all Random integer items... 261 | 262 | >>> opc.read(opc.list('Simulation Items.Random.*Int*')) 263 | [('Random.Int1', 99, 'Good', '06/24/07 22:44:28'), ('Random.Int2', 26299, 'Good', '06/24/07 22:44:28'), ('Random.Int4', 17035, 'Good', '06/24/07 22:44:28'), ('Random.UInt1', 77, 'Good', '06/24/07 22:44:28'), ('Random.UInt2', 28703, 'Good', '06/24/07 22:44:28'), ('Random.UInt4', 23811.0, 'Good', '06/24/07 22:44:28')] 264 | 265 | Read property #1 (data type) of all Real4 items... 266 | 267 | >>> opc.properties(opc.list('*.Real4', flat=True), id=1) 268 | [('Bucket Brigade.Real4', 'VT_R4'), ('Random.Real4', 'VT_R4'), ('Read Error.Real4', 'VT_R4'), ('Saw-toothed Waves.Real4', 'VT_R4'), ('Square Waves.Real4', 'VT_R4'), ('Triangle Waves.Real4', 'VT_R4'), ('Write Error.Real4', 'VT_R4'), ('Write Only.Real4', 'VT_R4')] 269 | 270 | Read the value of all Triangle Wave integers and then write the values back out to the OPC server. (A better example would be to do this between two different OPC servers!) 271 | 272 | >>> opc.write(opc.read(opc.list('Simulation Items.Triangle Waves.*Int*'))) 273 | [('Triangle Waves.Int1', 'Success'), ('Triangle Waves.Int2', 'Success'), ('Triangle Waves.Int4', 'Success'), ('Triangle Waves.UInt1', 'Success'), ('Triangle Waves.UInt2', 'Success'), ('Triangle Waves.UInt4', 'Success')] 274 | 275 | The short form of the read and write functions are useful for building easy to read calculations... 276 | 277 | >>> opc['Square Waves.Real4'] = ( opc['Random.Int4'] * opc['Random.Real4'] ) / 100.0 278 | 279 | Remove all named groups which were created with the read function... 280 | 281 | >>> opc.remove(opc.groups()) 282 | 283 | 284 | #### 15. Disconnecting from the OPC server 285 | 286 | >>> opc.close() 287 | 288 | 289 | 290 | 291 | OpenOPC Command-line Client 292 | ----------------------------------------------------- 293 | 294 | To the best of our knowledge, the OpenOPC project includes the only publically available command-line based OPC client. Unlike graphical OPC clients, this client can be easily used in scripts or batch files. And because of its piping capability (i.e. chaining commands together), it is far more powerful than other OPC clients. 295 | 296 | #### Read items 297 | 298 | C:\> opc -r Random.String Random.Int4 Random.Real8 299 | Random.String Your Good 06/25/07 23:46:33 300 | Random.Int4 19169 Good 06/25/07 23:46:33 301 | Random.Real8 8009.5730 Good 06/25/07 23:46:33 302 | 303 | Read 3 of 3 items (0.02 seconds) 304 | 305 | 306 | #### Multiple output styles 307 | 308 | C:\> opc -r Random.String Random.Int4 Random.Real8 -o csv 309 | Random.String,--,Good,06/25/07 23:58:16 310 | Random.Int4,15724,Good,06/25/07 23:58:16 311 | Random.Real8,5846.7234,Good,06/25/07 23:58:16 312 | 313 | 314 | #### List available items 315 | 316 | C:\> opc -f Random.*Int* 317 | Random.Int1 318 | Random.Int2 319 | Random.Int4 320 | Random.UInt1 321 | Random.UInt2 322 | Random.UInt4 323 | 324 | 325 | #### Combine commands using pipes 326 | 327 | C:\> opc -f Random.*Int* | opc -r - 328 | Random.Int1 0 Good 06/25/07 23:52:16 329 | Random.Int2 18467 Good 06/25/07 23:52:16 330 | Random.Int4 6334 Good 06/25/07 23:52:16 331 | Random.UInt1 206 Good 06/25/07 23:52:16 332 | Random.UInt2 19169 Good 06/25/07 23:52:16 333 | Random.UInt4 15724.0000 Good 06/25/07 23:52:16 334 | 335 | Read 6 of 6 items (0.02 seconds) 336 | 337 | 338 | #### Read a collection of items and values into a CSV file, edit the item values using a spreadsheet or other software, then write the new values back to the OPC server... 339 | 340 | C:\> opc -f "Triangle Waves.*Int*" | opc -r - -o csv >data.csv 341 | 342 | C:\> opc -w - < data.csv 343 | Triangle Waves.Int1 Success 344 | Triangle Waves.Int2 Success 345 | Triangle Waves.Int4 Success 346 | Triangle Waves.UInt1 Success 347 | Triangle Waves.UInt2 Success 348 | Triangle Waves.UInt4 Success 349 | 350 | Wrote 6 of 6 items (0.02 seconds) 351 | 352 | 353 | #### Data logger 354 | 355 | Read values of items every 60 seconds, continually logging the results to a file until stopped by Ctrl-C... 356 | 357 | C:\> opc Random.Int4 Random.Real8 -L 60 >data.log 358 | 359 | 360 | #### Command usage summary 361 | 362 | C:\> opc 363 | OpenOPC Command Line Client 1.1.6 364 | Copyright (c) 2007-2008 Barry Barnreiter (barry_b@users.sourceforge.net) 365 | 366 | Usage: opc [OPTIONS] [ACTION] [ITEM|PATH...] 367 | 368 | Actions: 369 | -r, --read Read ITEM values (default action) 370 | -w, --write Write values to ITEMs (use ITEM=VALUE) 371 | -p, --properties View properties of ITEMs 372 | -l, --list List items at specified PATHs (tree browser) 373 | -f, --flat List all ITEM names (flat browser) 374 | -i, --info Display OPC server information 375 | -q, --servers Query list of available OPC servers 376 | -S, --sessions List sessions in OpenOPC Gateway Service 377 | 378 | Options: 379 | -m MODE, --mode=MODE Protocol MODE (dcom, open) (default: OPC_MODE) 380 | -C CLASS,--class=CLASS OPC Automation CLASS (default: OPC_CLASS) 381 | -n NAME, --name=NAME Set OPC Client NAME (default: OPC_CLIENT) 382 | -h HOST, --host=HOST DCOM OPC HOST (default: OPC_HOST) 383 | -s SERV, --server=SERVER DCOM OPC SERVER (default: OPC_SERVER) 384 | -H HOST, --gate-host=HOST OpenOPC Gateway HOST (default: OPC_GATE_HOST) 385 | -P PORT, --gate-port=PORT OpenOPC Gateway PORT (default: OPC_GATE_PORT) 386 | 387 | -F FUNC, --function=FUNC Read FUNCTION to use (sync, async) 388 | -c SRC, --source=SOURCE Set data SOURCE for reads (cache, device, hybrid) 389 | -g SIZE, --size=SIZE Group tags into SIZE items per transaction 390 | -z MSEC, --pause=MSEC Sleep MSEC milliseconds between transactions 391 | -u MSEC, --update=MSEC Set update rate for group to MSEC milliseconds 392 | -t MSEC, --timeout=MSEC Set read timeout to MSEC mulliseconds 393 | 394 | -o FMT, --output=FORMAT Output FORMAT (table, values, pairs, csv, html) 395 | -L SEC, --repeat=SEC Loop ACTION every SEC seconds until stopped 396 | -y ID, --id=ID,... Retrieve only specific Property IDs 397 | -a STR, --append=STR,... Append STRINGS to each input item name 398 | -x N --rotate=N Rotate output orientation in groups of N values 399 | -v, --verbose Verbose mode showing all OPC function calls 400 | -e, --errors Include descriptive error message strings 401 | -R, --recursive List items recursively when browsing tree 402 | -, --pipe Pipe item/value list from standard input 403 | 404 | -------------------------------------------------------------------------------- /README.txt: -------------------------------------------------------------------------------- 1 | OpenOPC for Python 1.2.0 2 | Copyright (c) 2008-2012 by Barry Barnreiter (barry_b@users.sourceforge.net) 3 | 4 | http://openopc.sourceforge.net/ 5 | 6 | 7 | Post installation 8 | ----------------- 9 | 10 | Please go through the following post installation steps and functional 11 | checks to verify your installation of OpenOPC for Python is working 12 | correctly. 13 | 14 | 1. Get a listing of the available OPC servers on your computer by 15 | going to the command prompt and entering: 16 | 17 | opc -q 18 | 19 | 2. Set your prefered OPC server as the default by setting the system 20 | wide enviornment variable OPC_SERVER. (On Windows you can do this 21 | by going to Control Panel > System > Advanced > Environment Variables) 22 | 23 | OPC_SERVER=Matrikon.OPC.Simulation 24 | 25 | 3. Test your Win32 COM connection to the OPC server by entering the 26 | following at the command prompt: 27 | 28 | opc -i 29 | 30 | 4. Test to see if the OpenOPC Gateway Service is functioning by 31 | entering: 32 | 33 | opc -m open -i 34 | 35 | 5. Test some of the other commands available using the OPC Command 36 | Line Client. To get started, try entering the opc command without 37 | any arguments in order to see the help page: 38 | 39 | opc 40 | 41 | To read an item from your OPC server, just include the item name as 42 | one of your arguments. For example, if you're using Matrikon's 43 | Simulation server you could do: 44 | 45 | opc Random.Int4 46 | 47 | To read items from a specific OPC server you have installed, 48 | include the -s switch followed by the OPC server name. For 49 | example: 50 | 51 | opc -s Matrikon.OPC.Simulation Random.Int4 52 | 53 | If you experience any unexpected errors during these tests, please 54 | check the FAQ on http://openopc.sourceforge.net for additional help. 55 | 56 | If after reading through the FAQ you still require additional help, 57 | then the author of this package would be happy to assist you via 58 | e-mail. Please see the project website for current contact 59 | information. 60 | 61 | 62 | 63 | Software Developers 64 | ------------------- 65 | 66 | If you elected to install the OpenOPC Development library during the 67 | installation process, then you'll need to also download and install 68 | the following packages in order to develop your own Python programs 69 | which use the OpenOPC library: 70 | 71 | 1. Python 2.7.x 72 | http://www.python.org/download/ 73 | 74 | 2. Python for Windows Extensions (pywin32) 75 | http://sourceforge.net/projects/pywin32/ 76 | 77 | 3. Pyro 3.15 78 | http://irmen.home.xs4all.nl/pyro3/ 79 | 80 | Of course, Python is necessary on all platforms. However the other 81 | packages may be optional depending on your configuration: 82 | 83 | 1. Win32 platform, using the OpenOPC Gateway Service 84 | 85 | Pywin32: optional 86 | Pyro: required 87 | 88 | 2. Win32 platform, talking to OPC Servers directly using COM/DCOM 89 | 90 | Pywin32: required 91 | Pyro: optional 92 | 93 | 3. Non-Windows platform (use of Gateway Service is manditory) 94 | 95 | Pywin32: not applicable 96 | Pyro: required 97 | 98 | In order to get the most from the OpenOPC package, Windows developers 99 | are encouraged to install both Pywin32 and Pyro. Using Pyro to talk to 100 | the Gateway Service provides a quick and easy method for bypassing the 101 | DCOM security nightmares which are all too common when using OPC. 102 | 103 | 104 | Documentation 105 | ------------- 106 | 107 | A PDF manual for OpenOPC is included in this installation inside the 108 | "doc" folder. Users are encouraged to also look at the OpenOPC web 109 | site for additional usage examples that may not be contained in the 110 | manual. 111 | 112 | 113 | Technical Support 114 | ----------------- 115 | 116 | If you have any questions, bug reports, or suggestions for improvements 117 | please feel free to contact the author at: 118 | 119 | barry_b@users.sourceforge.net 120 | 121 | While I cannot always guarantee a quick response, I eventually respond 122 | to all e-mails and will do my best to slove any issues which are discovered. 123 | 124 | Thanks for using OpenOPC for Python! 125 | -------------------------------------------------------------------------------- /doc/OpenOPC.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sightmachine/OpenOPC/b4cc5edaac81a8d8bdaefc435edbe80db3fa39b4/doc/OpenOPC.pdf -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | #~ import os, sys, glob, fnmatch 3 | 4 | setup(name="OpenOPC", 5 | version=1.2, 6 | download_url='https://github.com/ingenuitas/OpenOPC/zipball/master', 7 | description="This is a clone of http://openopc.sourceforge.net modified to be used with distutils", 8 | keywords='python, opc, openopc', 9 | url='http://openopc.sourceforge.net', 10 | license='GPLv2', 11 | packages = find_packages(exclude=['ez_setup']), 12 | zip_safe = False, 13 | ) 14 | --------------------------------------------------------------------------------