├── .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('
%s | \n' % to_str(item)) 237 | else: 238 | write('%s | \n' % to_str(item)) 239 | if j == len(row)-1: write('