├── .gitignore ├── README.md ├── demo_clients ├── locked_time_client.py ├── srq-client.py ├── time_client.py ├── unittest-client.py └── unittest-rs-client.py ├── demo_servers ├── srq-device.py ├── time-device.py └── unittest-device.py └── vxi11_server ├── __init__.py ├── instrument_device.py ├── instrument_server.py ├── rpc.py └── vxi11.py /.gitignore: -------------------------------------------------------------------------------- 1 | # python 2 | *.pyc 3 | __pycache__ 4 | 5 | # emacs 6 | *.*~ 7 | *.*# 8 | build 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## VXI-11 Server in Python 2 | 3 | A VXI-11 Server implementation in Python that allows your BeagleBone Black or Raspberry PI appear as a VXI-11 device. 4 | 5 | VXI-11 is an instrument control protocol for accessing laboratory devices such as signal generators, power meters, and oscilloscopes over ethernet. 6 | 7 | python-vxi11-server makes your Beagle Bone Black or other linux/python enabled device work as a VXI-11 ethernet instrument. Coupled with python-vxi11 on the client side, controlling your device over ethernet is seamless with your other VXI-11 devices. 8 | 9 | Since VXI-11 specifies how instrument control and data messages transfer over ethernet, libraries such as this one and python-vxi11 do the hard work of setting up and tearing down the link, packing and unpacking the data, and getting the data to the proper endpoints. 10 | 11 | ### Requirements 12 | * Ported to Python 3 and tested on the RPi 13 | 14 | ### Dependencies 15 | #### Server 16 | * rpcbind package 17 | 18 | On a systemd os, use the command ```systemctl status rpcbind``` to verify run status. 19 | 20 | #### Client 21 | * [python-vxi11](https://github.com/python-ivi/python-vxi11) or some other VXI-11 client library that enables interaction with VXI-11 devices. 22 | 23 | The only client library tested against is python-vxi11. Other clients may expose various server bugs or protocol misunderstandings by the developer. Lets address them as they come. 24 | 25 | ### Getting started 26 | Unlike a client library, this 'package' is not installed. Simply clone and copy into the source tree of your project. 27 | 28 | #### Server side 29 | Run the simple demo clock_device.py program to start an instrument server that responds to a read command with the current time. Address any portmapper (rpcbind?) issues that may occur. See this [issue](https://github.com/coburnw/python-vxi11-server/issues/2#issuecomment-840338371) for some help running in windows. 30 | 31 | #### Client side 32 | Copy clock_client.py to the client folder/computer and edit the connect string to reflect domain names or ip addresses of your network. Run clock_client.py to extract the time from the server's clock_device. 33 | clock_client.py relies on [python-vxi11](https://github.com/python-ivi/python-vxi11) for interacting with the instrument server. Install python-vxi11 or adapt to your client library. 34 | 35 | ### Instrument Device development 36 | The InstrumentDevice base class is the handler for each instrument device that resides in an instrument server and should be the base class for your instrument implementation. The base class alone should make a fully functioning instrument that does absolutely nothing, the right way. Only override the methods necessary to make your instrument respond to the VXI-11 requests that are important for your instrument. 37 | 38 | See 'TCP/IP Instrument Protocol Specification' at [vxibus](http://www.vxibus.org/specifications.html) for help with the VXI-11 device_xxxx commands. 39 | 40 | For instance here is a very simple VXI-11 time server device that defines an InstrumentDevice handler and overrides just the device_read() function of the base class: 41 | 42 | import time 43 | import vxi11_server as Vxi11 44 | 45 | class TimeDevice(Vxi11.InstrumentDevice): 46 | def device_init(self): 47 | pass 48 | 49 | def device_read(self, request_size, term_char, flags, io_timeout): 50 | '''respond to the device_read rpc: refer to section (B.6.4) 51 | of the VXI-11 TCP/IP Instrument Protocol Specification''' 52 | 53 | error = Vxi11.Error.NO_ERROR 54 | reason = Vxi11.ReadRespReason.END 55 | 56 | # opaque_data is a bytes array, so encode our string appropriately! 57 | data = time.strftime('%H:%M:%S +0000', time.gmtime()) 58 | opaque_data = data.encode('ascii') 59 | 60 | return error, reason, opaque_data 61 | 62 | if __name__ == '__main__': 63 | # create a server, attach a device, and start a thread to listen for requests 64 | instr_server = Vxi11.InstrumentServer() 65 | instr_server.add_device_handler(TimeDevice, 'inst1') 66 | instr_server.listen() 67 | 68 | # sleep (or do foreground work) while the Instrument threads do their job 69 | while True: 70 | time.sleep(1) 71 | 72 | 73 | To access the time server using python-vxi11 as the client library: 74 | 75 | import vxi11 76 | 77 | instr = vxi11.Instrument('TCPIP::127.0.0.1::inst1::INSTR') 78 | 79 | # read the current time value from the instruments TimeDevice 80 | instr.read() 81 | 82 | 83 | ### Notes 84 | * be aware that ``add_device_handler()`` requires a class definition not a class instance as indicated by the lack of parenthesis. The server instantiates a new instance of your device handler class with each connect request. 85 | * Write a [python-ivi](https://github.com/python-ivi/python-ivi) driver for your new device 86 | * no serious attempt to harden, benchmark, or even test the code has been made. use at own risk. 87 | 88 | ### Thanks To 89 | * sonium0's [pyvxi11server](https://github.com/sonium0/pyvxi11server) for inspiration 90 | * SRQ support by [ulda](https://github.com/ulda) 91 | 92 | ### Examples Projects 93 | * thasti's [GPIB Bridge](https://git.loetlabor-jena.de/thasti/tcpip2instr) project 94 | 95 | ### Todo 96 | * move responsibility for packet segmentation from the application to the library 97 | * add abort example to explore/verify functionality 98 | -------------------------------------------------------------------------------- /demo_clients/locked_time_client.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | # use the python-vxi11 client library for interacting with the server. 4 | #import vxi11 5 | 6 | # use our vxi11 module so we can tinker with the lock client. 7 | import sys 8 | import os 9 | sys.path.append(os.path.abspath('..')) 10 | import vxi11_server as vxi11 11 | 12 | # with a default instrument, inst0 is implied. 13 | default_instr = vxi11.Instrument("TCPIP::127.0.0.1::INSTR") 14 | time_instr = vxi11.Instrument("TCPIP::127.0.0.1::inst1::INSTR") 15 | 16 | print('The INSTR instrument:') 17 | 18 | default_instr.write('*IDN?') 19 | print(default_instr.read()) 20 | 21 | print('contains the following devices:') 22 | default_instr.write('*DEVICE_LIST?') 23 | print(default_instr.read()) 24 | 25 | print() 26 | print('The TIME device has a current value of:') 27 | 28 | time_instr.lock_timeout= 10 29 | 30 | time_instr.lock(wait=True) 31 | time.sleep(1) 32 | time_instr.unlock() 33 | # verify missmatched unlocks raise 'no lock held' exception 34 | #time_instr.unlock() 35 | 36 | while True: 37 | try: 38 | result = time_instr.lock(wait=True) 39 | except Exception as ex: 40 | print(ex) 41 | else: 42 | is_locked = True 43 | while is_locked: 44 | try: 45 | print(time_instr.read()) 46 | except Exception as ex: 47 | print(ex) 48 | is_locked = False 49 | time.sleep(1) 50 | time.sleep(1) 51 | 52 | -------------------------------------------------------------------------------- /demo_clients/srq-client.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import time 4 | 5 | sys.path.append(os.path.abspath('..')) 6 | import vxi11_server as vxi11 7 | 8 | # with a default instrument, inst0 is implied. 9 | default_instr = vxi11.Instrument("TCPIP::127.0.0.1::INSTR") 10 | test1_instr = vxi11.Instrument("TCPIP::127.0.0.1::inst1::INSTR") 11 | test2_instr = vxi11.Instrument("TCPIP::127.0.0.1::inst2::INSTR") 12 | 13 | def asknprint( instr, q): 14 | print("send: %s"%q) 15 | print("answer: %s"% instr.ask(q)) 16 | 17 | 18 | asknprint( default_instr, "*IDN?") 19 | asknprint( test1_instr, "*IDN?") 20 | asknprint( test2_instr, "*IDN?") 21 | 22 | def srq_intr_handler1(): 23 | print("got SRQ for instrument 1") 24 | 25 | def srq_intr_handler2(): 26 | print("got SRQ for instrument 2") 27 | 28 | print("registering srq handler") 29 | test1_instr.on_srq(srq_intr_handler1) 30 | test2_instr.on_srq(srq_intr_handler2) 31 | 32 | print("asking instrument for SRQs") 33 | 34 | asknprint(test1_instr, "SRQTIMER") 35 | time.sleep(4) 36 | asknprint(test2_instr, "SRQTIMER") 37 | 38 | print('talking while waiting for srq') 39 | 40 | for i in range(5): 41 | 42 | print (i) 43 | 44 | time.sleep(4) 45 | 46 | asknprint( test1_instr, "*IDN?") 47 | asknprint( test2_instr, "*IDN?") 48 | 49 | print ("unregistering srq handler") 50 | test1_instr.on_srq(None) 51 | test2_instr.on_srq(None) 52 | -------------------------------------------------------------------------------- /demo_clients/time_client.py: -------------------------------------------------------------------------------- 1 | import vxi11 2 | 3 | # with a default instrument, inst0 is implied. 4 | default_instr = vxi11.Instrument("TCPIP::127.0.0.1::INSTR") 5 | time_instr = vxi11.Instrument("TCPIP::127.0.0.1::inst1::INSTR") 6 | #time_instr = vxi11.Instrument("TCPIP::192.168.2.101::TIME::INSTR") 7 | 8 | print('The INSTR instrument:') 9 | 10 | default_instr.write('*IDN?') 11 | print(default_instr.read()) 12 | 13 | print('contains the following devices:') 14 | default_instr.write('*DEVICE_LIST?') 15 | print(default_instr.read()) 16 | 17 | print() 18 | print('The TIME device has a current value of:') 19 | print(time_instr.read()) 20 | -------------------------------------------------------------------------------- /demo_clients/unittest-client.py: -------------------------------------------------------------------------------- 1 | # this unit test is tuned to the in-tree version of the vxi11.py library. 2 | # The in tree version has the addition of a client lock_on_open option among others. 3 | 4 | # adapted from work by @raphaelvalentin 5 | 6 | import os 7 | import sys 8 | import unittest 9 | 10 | sys.path.append(os.path.abspath('..')) 11 | import vxi11_server as vxi11 12 | 13 | class Test1(unittest.TestCase): 14 | 15 | def test1(self): 16 | """ Verify default (inst0) instrument 17 | """ 18 | try: 19 | inst0 = vxi11.Instrument("TCPIP::127.0.0.1::INSTR") 20 | inst0.open() 21 | 22 | inst0.write("*IDN?\n") 23 | dbytes = inst0.read() 24 | assert dbytes == 'my instrument zero', "has returned %r" % dbytes 25 | finally: 26 | inst0.close() 27 | 28 | def test2(self): 29 | """ Verify inst1 instrument 30 | """ 31 | try: 32 | inst1 = vxi11.Instrument("TCPIP::127.0.0.1::inst1::INSTR") 33 | inst1.open() 34 | 35 | inst1.write("*IDN?\n") 36 | dbytes = inst1.read() 37 | assert dbytes == 'my instrument one', "has returned %r" % dbytes 38 | finally: 39 | inst1.close() 40 | 41 | def test3(self): 42 | """ Verify multiple devices on the same server can be opened with locks 43 | """ 44 | try: 45 | inst0 = vxi11.Instrument("TCPIP::127.0.0.1::inst0::INSTR", lock_on_open=True) 46 | inst0.open() 47 | 48 | inst1 = vxi11.Instrument("TCPIP::127.0.0.1::inst1::INSTR", lock_on_open=True) 49 | inst1.open() 50 | 51 | inst0.write("*IDN?\n") 52 | dbytes = inst0.read() 53 | assert dbytes == 'my instrument zero', "has returned %r" % dbytes 54 | 55 | inst1.write("*IDN?\n") 56 | dbytes = inst1.read() 57 | assert dbytes == 'my instrument one', "has returned %r" % dbytes 58 | finally: 59 | inst0.close() 60 | inst1.close() 61 | 62 | def test4(self): 63 | """ Verify only one instance of a specific device can be opened with a lock 64 | """ 65 | try: 66 | inst0a = vxi11.Instrument("TCPIP::127.0.0.1::inst0::INSTR", lock_on_open=True) 67 | inst0a.open() 68 | 69 | inst0b = vxi11.Instrument("TCPIP::127.0.0.1::inst0::INSTR", lock_on_open=True) 70 | with self.assertRaisesRegex(Exception, "Device locked by another link") as cm: 71 | inst0b.open() 72 | finally: 73 | inst0a.close() 74 | inst0b.close() 75 | 76 | def test5(self): 77 | """ test create_link with invalid device name 78 | """ 79 | try: 80 | with self.assertRaisesRegex(Exception, "Device not accessible") as cm: 81 | inst0 = vxi11.Instrument("TCPIP::127.0.0.1::inst3::INSTR") 82 | inst0.open() 83 | 84 | inst0.write("*IDN?\n") 85 | dbytes = inst0.read() 86 | assert dbytes == 'my instrument zero', "has returned %r" % dbytes 87 | finally: 88 | inst0.close() 89 | pass 90 | 91 | def test6(self): 92 | """ Verify multiple instances of a specific device can be accessed without a lock 93 | """ 94 | try: 95 | inst0a = vxi11.Instrument("TCPIP::127.0.0.1::inst0::INSTR") 96 | inst0a.open() 97 | 98 | inst0b = vxi11.Instrument("TCPIP::127.0.0.1::inst0::INSTR") 99 | inst0b.open() 100 | 101 | inst0a.write("*IDN?\n") 102 | dbytes = inst0a.read() 103 | assert dbytes == 'my instrument zero', "has returned %r" % dbytes 104 | 105 | inst0b.write("*IDN?\n") 106 | dbytes = inst0b.read() 107 | assert dbytes == 'my instrument zero', "has returned %r" % dbytes 108 | finally: 109 | inst0a.close() 110 | inst0b.close() 111 | 112 | if __name__ == '__main__': 113 | unittest.main() 114 | -------------------------------------------------------------------------------- /demo_clients/unittest-rs-client.py: -------------------------------------------------------------------------------- 1 | 2 | """RS-VISA Setup for Ubuntu can be downloaded and install at 3 | https://www.rohde-schwarz.com/us/applications/r-s-visa-application-note_56280-148812.html 4 | 5 | by @raphaelvalentin 6 | """ 7 | 8 | import unittest 9 | import pyvisa 10 | from pyvisa.constants import * 11 | 12 | class Test1(unittest.TestCase): 13 | 14 | def test1(self): 15 | """ test create_link 16 | """ 17 | rm = pyvisa.ResourceManager("/usr/lib/librsvisa.so") 18 | lib = rm.visalib 19 | visarmsession, statuscode = lib.open_default_resource_manager() 20 | try: 21 | visasession, statuscode = lib.open(visarmsession, "TCPIP0::127.0.0.1::inst0::INSTR") 22 | nbytes, statuscode = lib.write(visasession, "*IDN?\n") 23 | dbytes, statuscode = lib.read(visasession, 256) 24 | assert dbytes == b'my instrument zero', "has returned %r" % dbytes 25 | finally: 26 | lib.close(visasession) 27 | 28 | def test2(self): 29 | """ test create_link 30 | """ 31 | rm = pyvisa.ResourceManager("/usr/lib/librsvisa.so") 32 | lib = rm.visalib 33 | visarmsession, statuscode = lib.open_default_resource_manager() 34 | try: 35 | visasession, statuscode = lib.open(visarmsession, "TCPIP0::127.0.0.1::inst1::INSTR") 36 | nbytes, statuscode = lib.write(visasession, "*IDN?\n") 37 | dbytes, statuscode = lib.read(visasession, 256) 38 | assert dbytes == b'my instrument one', "has returned %r" % dbytes 39 | finally: 40 | lib.close(visasession) 41 | 42 | def test3(self): 43 | """ test create_link twice 44 | """ 45 | rm = pyvisa.ResourceManager("/usr/lib/librsvisa.so") 46 | lib = rm.visalib 47 | visarmsession, statuscode = lib.open_default_resource_manager() 48 | try: 49 | visasession1, statuscode = lib.open(visarmsession, "TCPIP0::127.0.0.1::inst0::INSTR", AccessModes.exclusive_lock, 10000) 50 | visasession2, statuscode = lib.open(visarmsession, "TCPIP0::127.0.0.1::inst1::INSTR", AccessModes.exclusive_lock, 10000) 51 | nbytes, statuscode = lib.write(visasession1, "*IDN?\n") 52 | dbytes, statuscode = lib.read(visasession1, 256) 53 | assert dbytes == b'my instrument zero', "has returned %r" % dbytes 54 | nbytes, statuscode = lib.write(visasession2, "*IDN?\n") 55 | dbytes, statuscode = lib.read(visasession2, 256) 56 | assert dbytes == b'my instrument one', "has returned %r" % dbytes 57 | finally: 58 | lib.close(visasession1) 59 | lib.close(visasession2) 60 | 61 | def test4(self): 62 | """ test create_link twice 63 | """ 64 | rm = pyvisa.ResourceManager("/usr/lib/librsvisa.so") 65 | lib = rm.visalib 66 | visarmsession, statuscode = lib.open_default_resource_manager() 67 | try: 68 | visasession1, statuscode = lib.open(visarmsession, "TCPIP0::127.0.0.1::inst0::INSTR", AccessModes.exclusive_lock, 10000) 69 | visasession2, statuscode = lib.open(visarmsession, "TCPIP0::127.0.0.1::inst0::INSTR", AccessModes.exclusive_lock, 10000) 70 | finally: 71 | lib.close(visasession1) 72 | lib.close(visasession2) 73 | 74 | def test5(self): 75 | """ test create_link with not registered device name 76 | """ 77 | rm = pyvisa.ResourceManager("/usr/lib/librsvisa.so") 78 | lib = rm.visalib 79 | visarmsession, statuscode = lib.open_default_resource_manager() 80 | try: 81 | with self.assertRaisesRegex(Exception, "error creating link: 3") as cm: 82 | lib.open(visarmsession, "TCPIP0::127.0.0.1::inst3::INSTR", AccessModes.exclusive_lock, 10000) 83 | finally: 84 | lib.close(visarmsession) 85 | 86 | if __name__ == '__main__': 87 | unittest.main() 88 | -------------------------------------------------------------------------------- /demo_servers/srq-device.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import signal 4 | import logging 5 | from threading import Timer 6 | import time 7 | 8 | sys.path.append(os.path.abspath('..')) 9 | import vxi11_server as vxi11 10 | 11 | _logging = logging.getLogger(__name__) 12 | 13 | 14 | def signal_handler(signal, frame): 15 | _logging.info('Handling Ctrl+C!') 16 | instr_server.close() 17 | sys.exit(0) 18 | 19 | class SRQTestDevice(vxi11.InstrumentDevice): 20 | 21 | def device_init(self): 22 | self.response = "" 23 | return 24 | 25 | def device_write(self, opaque_data, flags, io_timeout): 26 | "The device_write RPC is used to write data to the specified device" 27 | error = vxi11.Error.NO_ERROR 28 | 29 | commands= opaque_data.decode("ascii").split(";") 30 | for cmd in commands: 31 | error= self._processCommand(cmd.strip()) 32 | if error != vxi11.Error.NO_ERROR: 33 | break 34 | return error 35 | 36 | def device_read(self, request_size, term_char, flags, io_timeout): 37 | "The device_read RPC is used to read data from the device to the controller" 38 | error = vxi11.Error.NO_ERROR 39 | aStr=self.response 40 | self.response="" 41 | reason = vxi11.ReadRespReason.END 42 | # returns opaque_data! 43 | return error, reason, aStr.encode("ascii","ignore") 44 | 45 | def _addResponse(self,aStr): 46 | self.response+=aStr 47 | 48 | def _processCommand(self, cmd ): 49 | error = vxi11.Error.NO_ERROR 50 | 51 | # commands ordered by usage rate 52 | if cmd.startswith("*IDN?"): 53 | self._addResponse("ulda,srq-test,1,V1.0") 54 | elif cmd.startswith("SRQTIMER"): 55 | t= Timer(10, self.signal_srq ) 56 | t.start() 57 | self._addResponse("OK") 58 | else: 59 | _logging.debug("unsupported vxi11-cmd %s",cmd) 60 | error = vxi11.Error.OPERATION_NOT_SUPPORTED 61 | return error 62 | 63 | def signal_srq(self): 64 | _logging.info("SRQ startet for instrument %s",self.name()) 65 | super().signal_srq() 66 | 67 | 68 | if __name__ == '__main__': 69 | logging.basicConfig(level=logging.DEBUG) 70 | _logging = logging.getLogger(__name__) 71 | 72 | signal.signal(signal.SIGINT, signal_handler) 73 | print('Press Ctrl+C to exit') 74 | 75 | _logging.info('starting SRQ test device') 76 | 77 | instr_server = vxi11.InstrumentServer() 78 | instr_server.add_device_handler(SRQTestDevice, "inst1") 79 | instr_server.add_device_handler(SRQTestDevice, "inst2") 80 | 81 | instr_server.listen() 82 | 83 | # sleep (or do foreground work) while the Instrument threads do their job 84 | while True: 85 | time.sleep(1) 86 | 87 | -------------------------------------------------------------------------------- /demo_servers/time-device.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import signal 4 | import time 5 | import logging 6 | 7 | sys.path.append(os.path.abspath('..')) 8 | import vxi11_server as Vxi11 9 | 10 | # 11 | # A simple instrument server. 12 | # 13 | # creates an InstrumentServer with the name INSTR 14 | # adds a device handler with the name inst1 15 | # this instrument simply responds with the current time when queried by 16 | # a vxi11 client. 17 | # 18 | # 'TIME' may not be a legal vxi11 instrument name, but seems to work well. 19 | # allowing some introspection on a device you havent used (and didnt document) 20 | # in some time. 21 | # 22 | 23 | def signal_handler(signal, frame): 24 | logger.info('Handling Ctrl+C!') 25 | instr_server.close() 26 | sys.exit(0) 27 | 28 | class TimeDevice(Vxi11.InstrumentDevice): 29 | 30 | def device_init(self): 31 | #print('TimeDevice: device_init()') 32 | return 33 | 34 | def device_read(self, request_size, term_char, flags, io_timeout): 35 | '''respond to the device_read rpc: refer to section (B.6.4) 36 | of the VXI-11 TCP/IP Instrument Protocol Specification''' 37 | error = Vxi11.Error.NO_ERROR 38 | reason = Vxi11.ReadRespReason.END 39 | 40 | # opaque_data is a bytes array, so encode correctly! 41 | data = time.strftime("%H:%M:%S +0000", time.gmtime()) 42 | opaque_data = data.encode("ascii") 43 | 44 | return error, reason, opaque_data 45 | 46 | if __name__ == '__main__': 47 | logging.basicConfig(level=logging.DEBUG) 48 | logger = logging.getLogger(__name__) 49 | 50 | signal.signal(signal.SIGINT, signal_handler) 51 | print('Press Ctrl+C to exit') 52 | logger.info('starting time_device') 53 | 54 | # create a server, attach a device, and start a thread to listen for requests 55 | instr_server = Vxi11.InstrumentServer() 56 | #name = 'TIME' 57 | name = 'inst1' 58 | instr_server.add_device_handler(TimeDevice, name) 59 | instr_server.listen() 60 | 61 | # sleep (or do foreground work) while the Instrument threads do their job 62 | while True: 63 | time.sleep(1) 64 | 65 | 66 | -------------------------------------------------------------------------------- /demo_servers/unittest-device.py: -------------------------------------------------------------------------------- 1 | # adapted from work by @raphaelvalentin 2 | 3 | import sys 4 | import os 5 | import logging 6 | import signal 7 | import time 8 | 9 | #sys.path.append(os.path.abspath('./python-vxi11-server')) 10 | sys.path.append(os.path.abspath('..')) 11 | import vxi11_server as Vxi11 12 | from vxi11_server import vxi11 13 | 14 | 15 | def signal_handler(signal, frame): 16 | logger.info('Handling Ctrl+C!') 17 | instr_server.close() 18 | sys.exit(0) 19 | 20 | 21 | class InstrumentRemote(Vxi11.InstrumentDevice): 22 | def device_write(self, opaque_data, flags, io_timeout): 23 | error = vxi11.ERR_NO_ERROR 24 | 25 | cmd=opaque_data.decode("ascii") 26 | self.result = None 27 | 28 | if cmd.strip() == '*IDN?': 29 | self.result = "{}".format(self.idn) 30 | else: 31 | self.result = 'invalid' 32 | 33 | logger.info("%s: device_write(): %s", self.name(), cmd) 34 | return error 35 | 36 | def device_read(self, request_size, term_char, flags, io_timeout): 37 | error = Vxi11.Error.NO_ERROR 38 | reason = Vxi11.ReadRespReason.END 39 | 40 | logger.info("%s: device_write(): %s", self.name(), self.result.strip()) 41 | 42 | opaque_data = self.result.encode("ascii") 43 | return error, reason, opaque_data 44 | 45 | class InstrumentRemote_0(InstrumentRemote): 46 | def device_init(self): 47 | "Set the devices idn string etc here. Called immediately after instance creation." 48 | self.idn = 'my instrument zero' 49 | self.result = 'empty' 50 | return 51 | 52 | class InstrumentRemote_1(InstrumentRemote): 53 | def device_init(self): 54 | "Set the devices idn string etc here. Called immediately after instance creation." 55 | self.idn = 'my instrument one' 56 | self.result = 'empty' 57 | return 58 | 59 | if __name__ == '__main__': 60 | logging.basicConfig(level=logging.DEBUG) 61 | logger = logging.getLogger(__name__) 62 | 63 | signal.signal(signal.SIGINT, signal_handler) 64 | print('Press Ctrl+C to exit') 65 | logger.info('starting time_device') 66 | 67 | # create a server, attach a device, and start a thread to listen for requests 68 | instr_server = Vxi11.InstrumentServer(InstrumentRemote_0) 69 | instr_server.add_device_handler(InstrumentRemote_1, 'inst1') 70 | instr_server.listen() 71 | 72 | # sleep (or do foreground work) while the Instrument threads do their job 73 | while True: 74 | time.sleep(1) 75 | -------------------------------------------------------------------------------- /vxi11_server/__init__.py: -------------------------------------------------------------------------------- 1 | from .vxi11 import Instrument, InterfaceDevice, list_devices, list_resources 2 | from .instrument_server import InstrumentServer, Error 3 | from .instrument_device import InstrumentDevice, ReadRespReason 4 | -------------------------------------------------------------------------------- /vxi11_server/instrument_device.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) [2019] [Coburn Wightman] 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | import logging 24 | import ipaddress 25 | 26 | from . import vxi11 27 | 28 | logger = logging.getLogger(__name__) 29 | 30 | class ReadRespReason(): 31 | END = vxi11.RX_END 32 | CHR = vxi11.RX_CHR 33 | REQCNT = vxi11.RX_REQCNT 34 | 35 | class InstrumentDevice(object): 36 | '''Base class for Instrument Devices. 37 | 38 | All devices should inherit from this class overriding the methods 39 | that make sense for the intended device. Since each method of this base class is 40 | expected to respond rationally, a very simple device might override one method only. 41 | 42 | See the "VXI-11 TCP/IP Instrument Protocol Specification" for details on 43 | each device_xxx procedure. The procedures are from the host perspective, ie 44 | a device write is a write to the device and device read is a read from the device. 45 | ''' 46 | 47 | def __init__(self, device_name, device_lock): 48 | self.device_name = device_name 49 | self.lock = device_lock 50 | 51 | self.intr_client = None 52 | self.srq_enabled = False 53 | self.srq_handle = None 54 | self.srq_active = False 55 | 56 | return 57 | 58 | def create_intr_chan(self,host_addr, host_port, prog_num, prog_vers, prog_family): 59 | if self.intr_client is not None: 60 | return vxi11.ERR_CHANNEL_ALREADY_ESTABLISHED 61 | 62 | if prog_num != vxi11.DEVICE_INTR_PROG or prog_vers!= vxi11.DEVICE_INTR_VERS or prog_family != vxi11.DEVICE_TCP: 63 | return vxi11.ERR_OPERATION_NOT_SUPPORTED 64 | 65 | try: 66 | self.intr_client=vxi11.TCPIntrClient(str(ipaddress.IPv4Address(host_addr)), host_port) 67 | #self.intr_client.connect() # done in __init__ 68 | return vxi11.ERR_NO_ERROR 69 | except Exception as e: 70 | logger.info("exception in create_intr_chan: %s",str(e)) 71 | return vxi11.ERR_CHANNEL_NOT_ESTABLISHED 72 | 73 | def destroy_intr_chan(self): 74 | error = vxi11.ERR_CHANNEL_NOT_ESTABLISHED 75 | try: 76 | if self.intr_client is not None: 77 | self.intr_client.close() 78 | error=vxi11.ERR_NO_ERROR 79 | finally: 80 | self.intr_client=None 81 | return error 82 | 83 | def device_readstb(self, flags, io_timeout): # 13, generic params 84 | "The device_readstb RPC is used to read a device's status byte." 85 | error = vxi11.ERR_NO_ERROR 86 | stb = 0 87 | 88 | #STB Status Byte Register 89 | # 0b_1000_0000 instrument specific 90 | # 0b_0100_0000 RQS - request service (send SRQ on rising edge) 91 | # 0b_0010_0000 ESB = (ESR >0) 92 | # 0b_0001_0000 Message avaliable 93 | # 0b_0000_1000 instrument specific 94 | # 0b_0000_0100 Error avaliable 95 | # 0b_0000_0010 instrument specific 96 | # 0b_0000_0001 instrument specific 97 | 98 | # set bit 6 (of 0..7) if srq is activated 99 | if self.srq_active: 100 | stb |= 0b_0100_0000 101 | # reset SRQ after read according to spec 102 | self.srq_active=False 103 | 104 | return error, stb 105 | 106 | def signal_srq(self): 107 | if self.srq_enabled and self.intr_client is not None: 108 | self.srq_active=True 109 | self.intr_client.signal_intr_srq(self.srq_handle) 110 | else: 111 | raise vxi11.Vxi11Exception(vxi11.ERR_CHANNEL_NOT_ESTABLISHED, 112 | "channel not enabled to signal SRQ") 113 | 114 | def name(self): 115 | return self.device_name 116 | 117 | # functions to overwrite when subclassing start here 118 | 119 | def device_init(self): 120 | "Set the devices idn string etc here. Called immediately after instance creation." 121 | return 122 | 123 | def device_abort(self): 124 | "The device_abort RPC stops an in-progress call." 125 | error = vxi11.ERR_NO_ERROR 126 | return error 127 | 128 | def device_write(self, opaque_data, flags, io_timeout): # 11 129 | "The device_write RPC is used to write data to the specified device" 130 | error = vxi11.ERR_NO_ERROR 131 | 132 | if False: 133 | error = vxi11.ERR_IO_TIMEOUT 134 | elif False: 135 | error = vxi11.ERR_IO_ERROR 136 | elif False: 137 | error = vxi11.ERR_ABORT 138 | else: 139 | error = vxi11.ERR_OPERATION_NOT_SUPPORTED 140 | 141 | return error 142 | 143 | def device_read(self, request_size, term_char, flags, io_timeout): #= 12 144 | "The device_read RPC is used to read data from the device to the controller" 145 | error = vxi11.ERR_NO_ERROR 146 | opaque_data = b"" 147 | 148 | if False: 149 | error = vxi11.ERR_IO_TIMEOUT 150 | elif False: 151 | error = vxi11.ERR_IO_ERROR 152 | elif False: 153 | error = vxi11.ERR_ABORT 154 | else: 155 | error = vxi11.ERR_OPERATION_NOT_SUPPORTED 156 | 157 | reason = ReadRespReason.END 158 | result = error, reason, opaque_data 159 | return result 160 | 161 | def device_trigger(self, flags, io_timeout): # 14, generic params 162 | "The device_trigger RPC is used to send a trigger to a device." 163 | error = vxi11.ERR_NO_ERROR 164 | 165 | if False: 166 | error = vxi11.ERR_IO_TIMEOUT 167 | elif False: 168 | error = vxi11.ERR_IO_ERROR 169 | elif False: 170 | error = vxi11.ERR_ABORT 171 | else: 172 | error = vxi11.ERR_OPERATION_NOT_SUPPORTED 173 | 174 | return error 175 | 176 | def device_clear(self, flags, io_timeout): # 15, generic params 177 | "The device_clear RPC is used to send a device clear to a device" 178 | error = vxi11.ERR_NO_ERROR 179 | 180 | if False: 181 | error = vxi11.ERR_IO_TIMEOUT 182 | elif False: 183 | error = vxi11.ERR_IO_ERROR 184 | elif False: 185 | error = vxi11.ERR_ABORT 186 | else: 187 | error = vxi11.ERR_OPERATION_NOT_SUPPORTED 188 | 189 | return error 190 | 191 | def device_remote(self, flags, io_timeout): # 16, generic params 192 | "The device_remote RPC is used to place a device in a remote state wherein all programmable local controls are disabled" 193 | error = vxi11.ERR_NO_ERROR 194 | 195 | if False: 196 | error = vxi11.ERR_IO_TIMEOUT 197 | elif False: 198 | error = vxi11.ERR_IO_ERROR 199 | elif False: 200 | error = vxi11.ERR_ABORT 201 | else: 202 | error = vxi11.ERR_OPERATION_NOT_SUPPORTED 203 | 204 | return error 205 | 206 | def device_local(self, flags, io_timeout): # 17, generic params 207 | "The device_local RPC is used to place a device in a local state wherein all programmable local controls are enabled" 208 | error = vxi11.ERR_NO_ERROR 209 | 210 | if False: 211 | error = vxi11.ERR_IO_TIMEOUT 212 | elif False: 213 | error = vxi11.ERR_IO_ERROR 214 | elif False: 215 | error = vxi11.ERR_ABORT 216 | else: 217 | error = vxi11.ERR_OPERATION_NOT_SUPPORTED 218 | 219 | return error 220 | 221 | def device_enable_srq(self, enable, handle): # = 20 222 | "The device_enable_srq RPC is used to enable or disable the sending of device_intr_srq RPCs by thenetwork instrument server" 223 | error = vxi11.ERR_NO_ERROR 224 | 225 | if enable == True: 226 | self.srq_handle = handle 227 | self.srq_enabled = True 228 | else: 229 | self.srq_enabled = False 230 | 231 | return error 232 | 233 | def device_docmd(self, flags, io_timeout, cmd, network_order, data_size, opaque_data_in): # = 22 234 | "The device_docmd RPC allows a variety of operations to be executed" 235 | error = vxi11.ERR_NO_ERROR 236 | 237 | if False: 238 | error = vxi11.ERR_IO_TIMEOUT 239 | elif False: 240 | error = vxi11.ERR_IO_ERROR 241 | elif False: 242 | error = vxi11.ERR_ABORT 243 | else: 244 | error = vxi11.ERR_OPERATION_NOT_SUPPORTED 245 | 246 | opaque_data_out = b"" 247 | return error, opaque_data_out 248 | 249 | class DefaultInstrumentDevice(InstrumentDevice): 250 | '''The default device is the device registered with the name of "inst0". 251 | 252 | The vxi-11 spec expects the default device to respond to the *IDN? command. 253 | If a custom default_device_handler is not specified when the InstrumentServer is 254 | initialized, this is the one that will be used. 255 | 256 | Many instruments have only one device, the "inst0" device. copy this class 257 | to YourDeviceHandler, use as boilerplate, and register it when the InstrumentServer 258 | is initialized. 259 | ''' 260 | 261 | def device_init(self): 262 | self.idn = 'python-vxi11-server', 'bbb', '1234', '567' 263 | self.result = 'empty' 264 | return 265 | 266 | def device_write(self, opaque_data, flags, io_timeout): 267 | error = vxi11.ERR_NO_ERROR 268 | 269 | #opaque_data is a bytes array, so decode it correctly 270 | cmd=opaque_data.decode("ascii").strip() 271 | 272 | if cmd == '*IDN?': 273 | mfg, model, sn, fw = self.idn 274 | self.result = "{},{},{},{}".format(mfg, model, sn, fw) 275 | elif cmd == '*DEVICE_LIST?': 276 | devs = self.device_list 277 | self.result = '' 278 | isFirst = True 279 | for dev in devs: 280 | if isFirst: 281 | self.result = '{}'.format(dev) 282 | isFirst = False 283 | else: 284 | self.result = '{}, {}'.format(self.result, dev) 285 | else: 286 | self.result = 'invalid' 287 | 288 | logger.info("%s: device_write(): %s %s", self.name(), cmd , self.result) 289 | return error 290 | 291 | def device_read(self, request_size, term_char, flags, io_timeout): 292 | error = vxi11.ERR_NO_ERROR 293 | reason = ReadRespReason.END 294 | 295 | #device-read returns opaque_data so encode it correctly 296 | opaque_data = self.result.encode("ascii") 297 | 298 | return error, reason, opaque_data 299 | -------------------------------------------------------------------------------- /vxi11_server/instrument_server.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | # Copyright (c) [2019] [Coburn Wightman] 4 | 5 | # Permission is hereby granted, free of charge, to any person obtaining a copy 6 | # of this software and associated documentation files (the "Software"), to deal 7 | # in the Software without restriction, including without limitation the rights 8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | # copies of the Software, and to permit persons to whom the Software is 10 | # furnished to do so, subject to the following conditions: 11 | 12 | # The above copyright notice and this permission notice shall be included in all 13 | # copies or substantial portions of the Software. 14 | 15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | # SOFTWARE. 22 | 23 | 24 | from . import rpc 25 | from . import vxi11 26 | 27 | #import os 28 | import logging 29 | import threading 30 | import socketserver 31 | from contextlib import contextmanager 32 | 33 | from . import instrument_device as Instrument 34 | 35 | MAX_RECEIVE_SIZE = 1024 36 | 37 | logger = logging.getLogger(__name__) 38 | 39 | class Error(object): 40 | '''vxi11 specification error constants''' 41 | NO_ERROR = vxi11.ERR_NO_ERROR 42 | SYNTAX_ERROR = vxi11.ERR_SYNTAX_ERROR 43 | DEVICE_NOT_ACCESSIBLE = vxi11.ERR_DEVICE_NOT_ACCESSIBLE 44 | INVALID_LINK_IDENTIFIER = vxi11.ERR_INVALID_LINK_IDENTIFIER 45 | PARAMETER_ERROR = vxi11.ERR_PARAMETER_ERROR 46 | CHANNEL_NOT_ESTABLISHED = vxi11.ERR_CHANNEL_NOT_ESTABLISHED 47 | OPERATION_NOT_SUPPORTED = vxi11.ERR_OPERATION_NOT_SUPPORTED 48 | OUT_OF_RESOURCES = vxi11.ERR_OUT_OF_RESOURCES 49 | DEVICE_LOCKED_BY_ANOTHER_LINK = vxi11.ERR_DEVICE_LOCKED_BY_ANOTHER_LINK 50 | NO_LOCK_HELD_BY_THIS_LINK = vxi11.ERR_NO_LOCK_HELD_BY_THIS_LINK 51 | IO_TIMEOUT = vxi11.ERR_IO_TIMEOUT 52 | IO_ERROR = vxi11.ERR_IO_ERROR 53 | INVALID_ADDRESS = vxi11.ERR_INVALID_ADDRESS 54 | ABORT = vxi11.ERR_ABORT 55 | CHANNEL_ALREADY_ESTABLISHED = vxi11.ERR_CHANNEL_ALREADY_ESTABLISHED 56 | 57 | class Flags(object): 58 | WAITLOCK = vxi11.OP_FLAG_WAIT_BLOCK 59 | END = vxi11.OP_FLAG_END 60 | TERMCHARSET = vxi11.OP_FLAG_TERMCHAR_SET 61 | 62 | class LockedIncrementer(object): 63 | _value = 0 #private global. 64 | 65 | #contain a list of active sessions also? 66 | #client_id = random.getrandbits(31) 67 | def __init__(self, start_value): 68 | self.lock = threading.Lock() 69 | self._value = start_value 70 | 71 | def next(self): 72 | self.lock.acquire() 73 | try: 74 | self._value = self._value + 1 75 | return self._value 76 | finally: 77 | self.lock.release() 78 | 79 | class DeviceLock(object): 80 | def __init__(self, device_name): 81 | self.device_name = device_name 82 | self.lock = threading.RLock() 83 | self.lock_id = 0 84 | return 85 | 86 | def _acquire(self, flags, lock_timeout): 87 | blocking = flags & Flags.WAITLOCK 88 | timeout = -1 89 | if blocking: 90 | timeout = lock_timeout/1000 91 | 92 | return self.lock.acquire(blocking, timeout) 93 | 94 | def _release(self): 95 | self.lock.release() 96 | return True 97 | 98 | def acquire(self, link_id, flags, lock_timeout): 99 | logger.debug('locking device: %s', self.device_name) 100 | 101 | error = vxi11.ERR_DEVICE_LOCKED_BY_ANOTHER_LINK 102 | 103 | if self.lock_id == link_id: 104 | pass 105 | elif self._acquire(flags, lock_timeout): 106 | self.lock_id = link_id 107 | error = vxi11.ERR_NO_ERROR 108 | 109 | return error 110 | 111 | def release(self, link_id): 112 | logger.debug('unlocking device: %s', self.device_name) 113 | 114 | error = vxi11.ERR_NO_LOCK_HELD_BY_THIS_LINK 115 | if self.lock_id == link_id: 116 | self._release() 117 | self.lock_id = 0 118 | error = vxi11.ERR_NO_ERROR 119 | 120 | return error 121 | 122 | @contextmanager 123 | def __call__(self, link_id, flags, lock_timeout): 124 | error = vxi11.ERR_DEVICE_LOCKED_BY_ANOTHER_LINK 125 | if self._acquire(flags, lock_timeout): 126 | error = vxi11.ERR_NO_ERROR 127 | try: 128 | yield error 129 | finally: 130 | if error == vxi11.ERR_NO_ERROR: 131 | self._release() 132 | return error 133 | 134 | class DeviceItem(object): 135 | def __init__(self, device_class): 136 | self.device_class = device_class 137 | self.lock = None 138 | return 139 | 140 | class DeviceRegistry(object): 141 | _next_device_index = 0 142 | _registry = {} 143 | 144 | def __init__(self): 145 | pass 146 | 147 | def register(self, name, device_class): 148 | if name is None: 149 | while 'inst' + str(self._next_device_index) in self._registry: 150 | self._next_device_index += 1 151 | name = 'inst' + str(self._next_device_index) 152 | 153 | if name in self._registry: 154 | raise KeyError 155 | 156 | item = DeviceItem(device_class) 157 | item.lock = DeviceLock(name) 158 | 159 | self._registry[name] = item 160 | 161 | return 162 | 163 | def remove(self, name): 164 | del self._registry[name] 165 | 166 | def directory(self): 167 | return self._registry.keys() 168 | 169 | def factory(self, name): 170 | item = self._registry[name] 171 | 172 | device = item.device_class(name, item.lock) 173 | device.device_list = self.directory() 174 | 175 | return device 176 | 177 | class Vxi11Server(socketserver.ThreadingMixIn, rpc.TCPServer): 178 | _next_device_index = 0 179 | _device_registry = DeviceRegistry() 180 | _link_registry = {} 181 | 182 | def __init__(self, host, prog, vers, port, handler_class): 183 | rpc.TCPServer.__init__(self, host, prog, vers, port, handler_class) 184 | self.lid_gen = LockedIncrementer(200) 185 | return 186 | 187 | # move device_class_registry, link_create/delete to CoreHandler? 188 | def link_create(self, device_name): 189 | # create and initialize an instance of the device handler registered to device_name 190 | device_instance = self._device_registry.factory(device_name) 191 | 192 | # and register it to a new link_id 193 | link_id = self.lid_gen.next() 194 | self._link_registry[link_id] = device_instance 195 | 196 | return link_id, self._link_registry[link_id] 197 | 198 | def link_delete(self, link_id): 199 | del self._link_registry[link_id] 200 | return 201 | 202 | #def link_name(self, link_id): 203 | # return self._link_registry[link_id] 204 | 205 | def link_abort(self, link_id): 206 | try: 207 | device_instance = self._link_registry[link_id] 208 | logger.debug('AbortServer: ABORT_LINK_ID %s to %s', link_id, self._link_registry[link_id]) 209 | error = device_instance.device_abort() 210 | except KeyError : 211 | logger.debug('AbortServer: ABORT_LINK_ID %s. link_id does not exist.', link_id) 212 | error = vxi11.ERR_INVALID_LINK_IDENTIFIER 213 | return error 214 | 215 | # should the device registry be moved to the core server? 216 | def device_register(self, name, device_class): 217 | self._device_registry.register(name, device_class) 218 | 219 | def device_unregister(self, name): 220 | self._device_registry.remove(name) 221 | return 222 | 223 | # def device_list(self): 224 | # return self._device_registry.directory() 225 | 226 | class Vxi11Handler(rpc.RPCRequestHandler): 227 | def addpackers(self): 228 | # amend rpc packers with our vxi11 packers 229 | self.packer = vxi11.Packer() 230 | self.unpacker = vxi11.Unpacker('') 231 | return 232 | 233 | class Vxi11AbortServer(Vxi11Server): 234 | def __init__(self): 235 | Vxi11Server.__init__(self, '', vxi11.DEVICE_ASYNC_PROG, vxi11.DEVICE_ASYNC_VERS, 0, Vxi11AbortHandler) 236 | return 237 | 238 | class Vxi11AbortHandler(Vxi11Handler): 239 | def handle_1(self): 240 | params = self.unpacker.unpack_device_link() 241 | link_id = params 242 | error=self.server.link_abort(link_id) 243 | 244 | self.turn_around() 245 | self.packer.pack_device_error(error) 246 | return 247 | 248 | class Vxi11CoreServer(Vxi11Server): 249 | def __init__(self, abort_port): 250 | Vxi11Server.__init__(self, '', vxi11.DEVICE_CORE_PROG, vxi11.DEVICE_CORE_VERS, 0, Vxi11CoreHandler) 251 | self.abort_port = abort_port 252 | return 253 | 254 | 255 | class Vxi11CoreHandler(Vxi11Handler): 256 | 257 | def handle_10(self): 258 | '''The create_link RPC creates a new link. 259 | This link is identified on subsequent RPCs by the lid returned from the network instrument server.''' 260 | 261 | params = self.unpacker.unpack_create_link_parms() 262 | client_id, lock_device, lock_timeout, device_name = params 263 | 264 | logger.debug('****************************') 265 | logger.debug('CREATE_LINK %s' ,params) 266 | 267 | self.link_id = 0 268 | error = vxi11.ERR_NO_ERROR 269 | 270 | try: 271 | logger.debug('Device name "%s"', device_name) 272 | self.link_id, self.device = self.server.link_create(device_name) 273 | except KeyError: 274 | error = vxi11.ERR_DEVICE_NOT_ACCESSIBLE 275 | logger.debug("Create link failed") 276 | else: 277 | self.device.device_init() 278 | if lock_device == True: 279 | flags = 0 280 | error = self.device.lock.acquire(self.link_id, flags, lock_timeout) 281 | 282 | abort_port = 0 283 | if error == vxi11.ERR_NO_ERROR: 284 | abort_port = self.server.abort_port 285 | 286 | result = (error, self.link_id, abort_port, MAX_RECEIVE_SIZE) 287 | self.turn_around() 288 | self.packer.pack_create_link_resp(result) 289 | return 290 | 291 | def handle_23(self): 292 | "The destroy_link call is used to close the identified link. The network instrument server recovers resources associated with the link" 293 | 294 | params = self.unpacker.unpack_device_link() 295 | link_id = params 296 | 297 | if link_id != self.link_id: 298 | error = vxi11.ERR_INVALID_LINK_IDENTIFIER 299 | else: 300 | logger.debug('DESTROY_LINK %s to %s', link_id, self.device.device_name) 301 | # first disable interrupt handling of the device if it was on 302 | self.device.device_enable_srq(False,None) 303 | self.device.destroy_intr_chan() 304 | 305 | self.device.lock.release(link_id) 306 | 307 | # now remove link and therefore delete everything. 308 | self.server.link_delete(link_id) 309 | error = vxi11.ERR_NO_ERROR 310 | 311 | self.turn_around() 312 | self.packer.pack_device_error(error) 313 | return 314 | 315 | def handle_11(self): 316 | "The device_write RPC is used to write data to the specified device" 317 | 318 | params = self.unpacker.unpack_device_write_parms() 319 | logger.debug('DEVICE_WRITE %s', params) 320 | link_id, io_timeout, lock_timeout, flags, opaque_data = params 321 | 322 | if link_id != self.link_id: 323 | error = vxi11.ERR_INVALID_LINK_IDENTIFIER 324 | elif len(opaque_data) > MAX_RECEIVE_SIZE: 325 | error = vxi11.ERR_PARAMETER_ERROR 326 | else: 327 | with self.device.lock(link_id, flags, lock_timeout) as error: 328 | if error == vxi11.ERR_NO_ERROR: 329 | error = self.device.device_write(opaque_data, flags, io_timeout) 330 | 331 | result = (error, 0) 332 | if error == vxi11.ERR_NO_ERROR: 333 | result = (error, len(opaque_data)) 334 | 335 | self.turn_around() 336 | self.packer.pack_device_write_resp(result) 337 | return 338 | 339 | def handle_12(self): 340 | "The device_read RPC is used to read data from the device to the controller" 341 | 342 | params = self.unpacker.unpack_device_read_parms() 343 | logger.debug('DEVICE_READ %s', params) 344 | link_id, request_size, io_timeout, lock_timeout, flags, term_char = params 345 | 346 | opaque_data = b'' 347 | reason = 0 348 | 349 | if link_id != self.link_id: 350 | error = vxi11.ERR_INVALID_LINK_IDENTIFIER 351 | else: 352 | with self.device.lock(link_id, flags, lock_timeout) as error: 353 | if error == vxi11.ERR_NO_ERROR: 354 | error, reason, opaque_data = self.device.device_read(request_size, term_char, flags, io_timeout) 355 | 356 | result = (error, reason, opaque_data) 357 | self.turn_around() 358 | self.packer.pack_device_read_resp(result) 359 | return 360 | 361 | def handle_13(self): 362 | "The device_readstb RPC is used to read a device's status byte." 363 | 364 | params = self.unpacker.unpack_device_generic_parms() 365 | logger.debug('DEVICE_READSTB %s', params) 366 | link_id, flags, lock_timeout, io_timeout = params 367 | 368 | stb=0 369 | if link_id != self.link_id: 370 | error = vxi11.ERR_INVALID_LINK_IDENTIFIER 371 | else: 372 | with self.device.lock(link_id, flags, lock_timeout) as error: 373 | if error == vxi11.ERR_NO_ERROR: 374 | error, stb = self.device.device_readstb(flags, io_timeout) 375 | 376 | result = (error, stb) 377 | self.turn_around() 378 | self.packer.pack_device_read_stb_resp(result) 379 | return 380 | 381 | def handle_14(self): 382 | "The device_trigger RPC is used to send a trigger to a device." 383 | 384 | params = self.unpacker.unpack_device_generic_parms() 385 | logger.debug('DEVICE_TRIGGER %s', params) 386 | link_id, flags, lock_timeout, io_timeout = params 387 | 388 | if link_id != self.link_id: 389 | error = vxi11.ERR_INVALID_LINK_IDENTIFIER 390 | else: 391 | with self.device.lock(link_id, flags, lock_timeout) as error: 392 | if error == vxi11.ERR_NO_ERROR: 393 | error = self.device.device_trigger(flags, io_timeout) 394 | 395 | self.turn_around() 396 | self.packer.pack_device_error(error) 397 | return 398 | 399 | def handle_15(self): 400 | "The device_clear RPC is used to send a device clear to a device" 401 | 402 | params = self.unpacker.unpack_device_generic_parms() 403 | logger.debug('DEVICE_CLEAR %s', params) 404 | link_id, flags, lock_timeout, io_timeout = params 405 | 406 | if link_id != self.link_id: 407 | error = vxi11.ERR_INVALID_LINK_IDENTIFIER 408 | else: 409 | with self.device.lock(link_id, flags, lock_timeout) as error: 410 | if error == vxi11.ERR_NO_ERROR: 411 | error = self.device.device_clear(flags, io_timeout) 412 | 413 | self.turn_around() 414 | self.packer.pack_device_error(error) 415 | return 416 | 417 | def handle_16(self): 418 | "The device_remote RPC is used to place a device in a remote state wherein all programmable local controls are disabled" 419 | 420 | params = self.unpacker.unpack_device_generic_parms() 421 | logger.debug('DEVICE_REMOTE %s', params) 422 | link_id, flags, lock_timeout, io_timeout = params 423 | 424 | if link_id != self.link_id: 425 | error = vxi11.ERR_INVALID_LINK_IDENTIFIER 426 | else: 427 | with self.device.lock.is_open(link_id, flags, lock_timeout) as error: 428 | if error == vxi11.ERR_NO_ERROR: 429 | error = self.device.device_remote(flags, io_timeout) 430 | 431 | self.turn_around() 432 | self.packer.pack_device_error(error) 433 | return 434 | 435 | def handle_17(self): 436 | "The device_local RPC is used to place a device in a local state wherein all programmable local controls are enabled" 437 | 438 | params = self.unpacker.unpack_device_generic_parms() 439 | logger.debug('DEVICE_LOCAL %s', params) 440 | link_id, flags, lock_timeout, io_timeout = params 441 | 442 | if link_id != self.link_id: 443 | error = vxi11.ERR_INVALID_LINK_IDENTIFIER 444 | else: 445 | with self.device.lock(link_id, flags, lock_timeout) as error: 446 | if error == vxi11.ERR_NO_ERROR: 447 | error = self.device.device_local(flags, io_timeout) 448 | 449 | self.turn_around() 450 | self.packer.pack_device_error(error) 451 | return 452 | 453 | def handle_18(self): 454 | "The device_lock RPC is used to acquire a device's lock" 455 | 456 | params = self.unpacker.unpack_device_lock_parms() 457 | logger.debug('DEVICE_LOCK %s', params) 458 | link_id, flags, lock_timeout = params 459 | 460 | if link_id != self.link_id: 461 | error = vxi11.ERR_INVALID_LINK_IDENTIFIER 462 | else: 463 | error = self.device.lock.acquire(link_id, flags, lock_timeout) 464 | 465 | self.turn_around() 466 | self.packer.pack_device_error(error) 467 | return 468 | 469 | def handle_19(self): 470 | "The device_unlock RPC is used to release locks acquired by the device_lock RPC" 471 | 472 | params = self.unpacker.unpack_device_link() 473 | logger.debug('DEVICE_UNLOCK %s', params) 474 | link_id = params 475 | 476 | if link_id != self.link_id: 477 | error = vxi11.ERR_INVALID_LINK_IDENTIFIER 478 | else: 479 | error = self.device.lock.release(link_id) 480 | 481 | self.turn_around() 482 | self.packer.pack_device_error(error) 483 | return 484 | 485 | def handle_25(self): 486 | "The create_intr_chan RPC is used to inform the network instrument server to establish an interrupt channel" 487 | 488 | params = self.unpacker.unpack_device_remote_func_parms() 489 | logger.debug('DEVICE_CREATE_INTR_CHAN %s', params) 490 | host_addr, host_port, prog_num, prog_vers, prog_family = params 491 | 492 | error = self.device.create_intr_chan(host_addr, host_port, prog_num, prog_vers, prog_family) 493 | 494 | self.turn_around() 495 | self.packer.pack_device_error(error) 496 | return 497 | 498 | def handle_26(self): 499 | "The destroy_intr_chan RPC is used to inform the network instrument server to close its interrupt channel" 500 | 501 | # no params (void) for this function according to vxi11-spec B.6.13 V1.0 ! 502 | logger.debug('DEVICE_DESTROY_INTR_CHAN') 503 | 504 | error = self.device.destroy_intr_chan() 505 | 506 | self.turn_around() 507 | self.packer.pack_device_error(error) 508 | return 509 | 510 | def handle_20(self): 511 | "The device_enable_srq RPC is used to enable or disable the sending of device_intr_srq RPCs by thenetwork instrument server" 512 | 513 | params = self.unpacker.unpack_device_enable_srq_parms() 514 | logger.debug('DEVICE_ENABLE_SRQ %s', params) 515 | link_id, enable, handle = params 516 | 517 | if link_id != self.link_id: 518 | error = vxi11.ERR_INVALID_LINK_IDENTIFIER 519 | else: 520 | error = self.device.device_enable_srq(enable,handle) 521 | 522 | self.turn_around() 523 | self.packer.pack_device_error(error) 524 | return 525 | 526 | def handle_22(self): 527 | "The device_docmd RPC allows a variety of operations to be executed" 528 | 529 | params = self.unpacker.unpack_device_docmd_parms() 530 | logger.debug('DEVICE_DOCMD %s', params) 531 | link_id, flags, io_timeout, lock_timeout, cmd, network_order, data_size, opaque_data_in = params 532 | 533 | opaque_data_out = b"" 534 | if link_id != self.link_id: 535 | error = vxi11.ERR_INVALID_LINK_IDENTIFIER 536 | else: 537 | with self.device.lock(link_id, flags, lock_timeout) as error: 538 | if error == vxi11.ERR_NO_ERROR: 539 | error, opaque_data_out = self.device.device_docmd(flags, io_timeout, cmd, network_order, data_size, opaque_data_in) 540 | 541 | result = error, opaque_data_out 542 | self.turn_around() 543 | self.packer.pack_device_docmd_resp(result) 544 | return 545 | 546 | 547 | class InstrumentServer(): 548 | '''Maintains a registry of device handlers and routes incoming client RPC's to appropriate handler. 549 | ''' 550 | def __init__(self, default_device_handler=None): 551 | '''Initialize the instrument and start a default device handler on inst0. 552 | 553 | default_device_handler: (optional) a device_handler class to be use 554 | as the default devive handler registered as "inst0". 555 | ''' 556 | self.abortServer = Vxi11AbortServer() 557 | 558 | abort_host, abort_port = self.abortServer.server_address 559 | self.coreServer = Vxi11CoreServer(abort_port) 560 | 561 | if default_device_handler is None: 562 | default_device_handler = Instrument.DefaultInstrumentDevice 563 | 564 | self.add_device_handler(default_device_handler, 'inst0') 565 | return 566 | 567 | def add_device_handler(self, device_handler, device_name=None ): 568 | '''registers a device handler to serve client requests. 569 | 570 | device_handler: device handler class to handle incoming requests on device_name. 571 | device_name: (optional) name of device to be used in clients connect string. 572 | if none supplied, next available "inst" used 573 | ''' 574 | self.coreServer.device_register(device_name, device_handler) 575 | return(True) 576 | 577 | def close(self): 578 | logger.info('Closing...') 579 | self.coreServer.unregister() 580 | self.coreServer.shutdown() 581 | self.coreServer.server_close() 582 | 583 | self.abortServer.shutdown() 584 | self.abortServer.server_close() 585 | logger.info('Closed.') 586 | return(True) 587 | 588 | def listen(self, loglevel = 'DEBUG'): # 'INFO' 589 | #self.ch.setLevel(getattr(logging, loglevel)) 590 | 591 | abortThread = threading.Thread(target=self.abortServer.serve_forever) 592 | abortThread.setDaemon(True) # don't hang on exit 593 | abortThread.start() 594 | logger.info('abortServer started...') 595 | 596 | self.coreServer.register() 597 | coreThread = threading.Thread(target=self.coreServer.serve_forever) 598 | coreThread.setDaemon(True) 599 | coreThread.start() 600 | logger.info('coreServer started...') 601 | return(True) 602 | 603 | -------------------------------------------------------------------------------- /vxi11_server/rpc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sun RPC version 2 -- RFC1057 3 | 4 | This file is drawn from Python's RPC demo, updated for python 3. There 5 | doesn't seem to be an original author or license associated the original 6 | file. 7 | 8 | # March 2018 RCW Converted TCPServer to SocketServer with BaseRequestHandler 9 | 10 | XXX There should be separate exceptions for the various reasons why 11 | XXX an RPC can fail, rather than using RuntimeError for everything 12 | 13 | XXX Need to use class based exceptions rather than string exceptions 14 | 15 | XXX The UDP version of the protocol resends requests when it does 16 | XXX not receive a timely reply -- use only for idempotent calls! 17 | 18 | XXX There is no provision for call timeout on TCP connections 19 | 20 | Original source: http://svn.python.org/projects/python/trunk/Demo/rpc/rpc.py 21 | 22 | """ 23 | 24 | import xdrlib 25 | import socket 26 | import os 27 | import struct 28 | import logging 29 | 30 | import socketserver 31 | 32 | logger = logging.getLogger(__name__) 33 | 34 | RPCVERSION = 2 35 | 36 | CALL = 0 37 | REPLY = 1 38 | 39 | AUTH_NULL = 0 40 | AUTH_UNIX = 1 41 | AUTH_SHORT = 2 42 | AUTH_DES = 3 43 | 44 | MSG_ACCEPTED = 0 45 | MSG_DENIED = 1 46 | 47 | SUCCESS = 0 # RPC executed successfully 48 | PROG_UNAVAIL = 1 # remote hasn't exported program 49 | PROG_MISMATCH = 2 # remote can't support version # 50 | PROC_UNAVAIL = 3 # program can't support procedure 51 | GARBAGE_ARGS = 4 # procedure can't decode params 52 | 53 | RPC_MISMATCH = 0 # RPC version number != 2 54 | AUTH_ERROR = 1 # remote can't authenticate caller 55 | 56 | AUTH_BADCRED = 1 # bad credentials (seal broken) 57 | AUTH_REJECTEDCRED = 2 # client must begin new session 58 | AUTH_BADVERF = 3 # bad verifier (seal broken) 59 | AUTH_REJECTEDVERF = 4 # verifier expired or replayed 60 | AUTH_TOOWEAK = 5 # rejected for security reasons 61 | 62 | # Exceptions 63 | class RPCError(Exception): pass 64 | class RPCBadFormat(RPCError): pass 65 | class RPCBadVersion(RPCError): pass 66 | class RPCGarbageArgs(RPCError): pass 67 | class RPCUnpackError(RPCError): pass 68 | 69 | def make_auth_null(): 70 | return b'' 71 | 72 | class Packer(xdrlib.Packer): 73 | 74 | def pack_auth(self, auth): 75 | flavor, stuff = auth 76 | self.pack_enum(flavor) 77 | self.pack_opaque(stuff) 78 | 79 | def pack_auth_unix(self, stamp, machinename, uid, gid, gids): 80 | self.pack_uint(stamp) 81 | self.pack_string(machinename) 82 | self.pack_uint(uid) 83 | self.pack_uint(gid) 84 | self.pack_uint(len(gids)) 85 | for i in gids: 86 | self.pack_uint(i) 87 | 88 | def pack_callheader(self, xid, prog, vers, proc, cred, verf): 89 | self.pack_uint(xid) 90 | self.pack_enum(CALL) 91 | self.pack_uint(RPCVERSION) 92 | self.pack_uint(prog) 93 | self.pack_uint(vers) 94 | self.pack_uint(proc) 95 | self.pack_auth(cred) 96 | self.pack_auth(verf) 97 | # Caller must add procedure-specific part of call 98 | 99 | def pack_replyheader(self, xid, verf): 100 | self.pack_uint(xid) 101 | self.pack_enum(REPLY) 102 | self.pack_uint(MSG_ACCEPTED) 103 | self.pack_auth(verf) 104 | self.pack_enum(SUCCESS) 105 | # Caller must add procedure-specific part of reply 106 | 107 | class Unpacker(xdrlib.Unpacker): 108 | 109 | def unpack_auth(self): 110 | flavor = self.unpack_enum() 111 | stuff = self.unpack_opaque() 112 | return (flavor, stuff) 113 | 114 | def unpack_callheader(self): 115 | xid = self.unpack_uint() 116 | temp = self.unpack_enum() 117 | if temp != CALL: 118 | raise RPCBadFormat('no CALL but %r' % (temp,)) 119 | temp = self.unpack_uint() 120 | if temp != RPCVERSION: 121 | raise RPCBadVersion('bad RPC version %r' % (temp,)) 122 | prog = self.unpack_uint() 123 | vers = self.unpack_uint() 124 | proc = self.unpack_uint() 125 | cred = self.unpack_auth() 126 | verf = self.unpack_auth() 127 | return xid, prog, vers, proc, cred, verf 128 | # Caller must add procedure-specific part of call 129 | 130 | def unpack_replyheader(self): 131 | xid = self.unpack_uint() 132 | mtype = self.unpack_enum() 133 | if mtype != REPLY: 134 | raise RPCUnpackError('no REPLY but %r' % (mtype,)) 135 | stat = self.unpack_enum() 136 | if stat == MSG_DENIED: 137 | stat = self.unpack_enum() 138 | if stat == RPC_MISMATCH: 139 | low = self.unpack_uint() 140 | high = self.unpack_uint() 141 | raise RPCUnpackError('MSG_DENIED: RPC_MISMATCH: %r' % ((low, high),)) 142 | if stat == AUTH_ERROR: 143 | stat = self.unpack_uint() 144 | raise RPCUnpackError('MSG_DENIED: AUTH_ERROR: %r' % (stat,)) 145 | raise RPCUnpackError('MSG_DENIED: %r' % (stat,)) 146 | if stat != MSG_ACCEPTED: 147 | raise RPCUnpackError('Neither MSG_DENIED nor MSG_ACCEPTED: %r' % (stat,)) 148 | verf = self.unpack_auth() 149 | stat = self.unpack_enum() 150 | if stat == PROG_UNAVAIL: 151 | raise RPCUnpackError('call failed: PROG_UNAVAIL') 152 | if stat == PROG_MISMATCH: 153 | low = self.unpack_uint() 154 | high = self.unpack_uint() 155 | raise RPCUnpackError('call failed: PROG_MISMATCH: %r' % ((low, high),)) 156 | if stat == PROC_UNAVAIL: 157 | raise RPCUnpackError('call failed: PROC_UNAVAIL') 158 | if stat == GARBAGE_ARGS: 159 | raise RPCGarbageArgs 160 | if stat != SUCCESS: 161 | raise RPCUnpackError('call failed: %r' % (stat,)) 162 | return xid, verf 163 | # Caller must get procedure-specific part of reply 164 | 165 | # Common base class for clients 166 | 167 | class Client: 168 | 169 | def __init__(self, host, prog, vers, port): 170 | self.host = host 171 | self.prog = prog 172 | self.vers = vers 173 | self.port = port 174 | self.lastxid = 0 # XXX should be more random? 175 | self.cred = None 176 | self.verf = None 177 | 178 | def make_call(self, proc, args, pack_func, unpack_func): 179 | # Don't normally override this (but see Broadcast) 180 | if pack_func is None and args is not None: 181 | raise TypeError('non-null args with null pack_func') 182 | self.start_call(proc) 183 | if pack_func: 184 | pack_func(args) 185 | self.do_call() 186 | if unpack_func: 187 | result = unpack_func() 188 | else: 189 | result = None 190 | self.unpacker.done() 191 | return result 192 | 193 | def start_call(self, proc): 194 | # Don't override this 195 | self.lastxid = xid = self.lastxid + 1 196 | cred = self.mkcred() 197 | verf = self.mkverf() 198 | p = self.packer 199 | p.reset() 200 | p.pack_callheader(xid, self.prog, self.vers, proc, cred, verf) 201 | 202 | def do_call(self): 203 | # This MUST be overridden 204 | raise RPCError('do_call not defined') 205 | 206 | def mkcred(self): 207 | # Override this to use more powerful credentials 208 | if self.cred is None: 209 | self.cred = (AUTH_NULL, make_auth_null()) 210 | return self.cred 211 | 212 | def mkverf(self): 213 | # Override this to use a more powerful verifier 214 | if self.verf is None: 215 | self.verf = (AUTH_NULL, make_auth_null()) 216 | return self.verf 217 | 218 | def call_0(self): 219 | # Procedure 0 is always like this 220 | return self.make_call(0, None, None, None) 221 | 222 | 223 | # Record-Marking standard support 224 | 225 | def sendfrag(sock, last, frag): 226 | x = len(frag) 227 | if last: x = x | 0x80000000 228 | header = struct.pack(">I", x) 229 | sock.sendall(header + frag) 230 | 231 | def sendrecord(sock, record): 232 | if len(record) > 0: 233 | sendfrag(sock, 1, record) 234 | 235 | def recvfrag(sock): 236 | header = sock.recv(4) 237 | if len(header) < 4: 238 | raise EOFError 239 | x = struct.unpack(">I", header[0:4])[0] 240 | last = ((x & 0x80000000) != 0) 241 | n = int(x & 0x7fffffff) 242 | frag = bytearray() 243 | while len(frag) < n: 244 | buf = sock.recv(n - len(frag)) 245 | if not buf: raise EOFError 246 | frag.extend(buf) 247 | return last, frag 248 | 249 | def recvrecord(sock): 250 | record = bytearray() 251 | last = 0 252 | while not last: 253 | last, frag = recvfrag(sock) 254 | record.extend(frag) 255 | return bytes(record) 256 | 257 | 258 | # Client using TCP to a specific port 259 | 260 | class RawTCPClient(Client): 261 | def __init__(self, host, prog, vers, port): 262 | Client.__init__(self, host, prog, vers, port) 263 | self.connect() 264 | 265 | def connect(self): 266 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 267 | self.sock.connect((self.host, self.port)) 268 | 269 | def close(self): 270 | self.sock.close() 271 | 272 | def do_call(self): 273 | call = self.packer.get_buf() 274 | sendrecord(self.sock, call) 275 | while True: 276 | reply = recvrecord(self.sock) 277 | u = self.unpacker 278 | u.reset(reply) 279 | xid, verf = u.unpack_replyheader() 280 | if xid == self.lastxid: 281 | # xid matches, we're done 282 | return 283 | elif xid < self.lastxid: 284 | # Stale data in buffer due to interruption 285 | # Discard and fetch another record 286 | continue 287 | else: 288 | # xid larger than expected - packet from the future? 289 | raise RPCError('wrong xid in reply %r instead of %r' % (xid, self.lastxid)) 290 | 291 | 292 | # Client using UDP to a specific port 293 | 294 | class RawUDPClient(Client): 295 | def __init__(self, host, prog, vers, port): 296 | Client.__init__(self, host, prog, vers, port) 297 | self.connect() 298 | 299 | def connect(self): 300 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 301 | self.sock.connect((self.host, self.port)) 302 | 303 | def close(self): 304 | self.sock.close() 305 | 306 | def do_call(self): 307 | call = self.packer.get_buf() 308 | self.sock.send(call) 309 | try: 310 | from select import select 311 | except ImportError: 312 | logger.warn('select not found, RPC may hang') 313 | select = None 314 | BUFSIZE = 8192 # Max UDP buffer size 315 | timeout = 1 316 | count = 5 317 | while 1: 318 | r, w, x = [self.sock], [], [] 319 | if select: 320 | r, w, x = select(r, w, x, timeout) 321 | if self.sock not in r: 322 | count = count - 1 323 | if count < 0: raise RPCError('timeout') 324 | if timeout < 25: timeout = timeout *2 325 | ## print 'RESEND', timeout, count 326 | self.sock.send(call) 327 | continue 328 | reply = self.sock.recv(BUFSIZE) 329 | u = self.unpacker 330 | u.reset(reply) 331 | xid, verf = u.unpack_replyheader() 332 | if xid != self.lastxid: 333 | ## print 'BAD xid' 334 | continue 335 | break 336 | 337 | 338 | # Client using UDP broadcast to a specific port 339 | 340 | class RawBroadcastUDPClient(RawUDPClient): 341 | 342 | def __init__(self, bcastaddr, prog, vers, port): 343 | RawUDPClient.__init__(self, bcastaddr, prog, vers, port) 344 | self.reply_handler = None 345 | self.timeout = 30 346 | 347 | def connect(self): 348 | self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 349 | self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) 350 | 351 | def set_reply_handler(self, reply_handler): 352 | self.reply_handler = reply_handler 353 | 354 | def set_timeout(self, timeout): 355 | self.timeout = timeout # Use None for infinite timeout 356 | 357 | def make_call(self, proc, args, pack_func, unpack_func): 358 | if pack_func is None and args is not None: 359 | raise TypeError('non-null args with null pack_func') 360 | self.start_call(proc) 361 | if pack_func: 362 | pack_func(args) 363 | call = self.packer.get_buf() 364 | self.sock.sendto(call, (self.host, self.port)) 365 | try: 366 | from select import select 367 | except ImportError: 368 | logger.warn('select not found, broadcast will hang') 369 | select = None 370 | BUFSIZE = 8192 # Max UDP buffer size (for reply) 371 | replies = [] 372 | if unpack_func is None: 373 | def dummy(): pass 374 | unpack_func = dummy 375 | while 1: 376 | r, w, x = [self.sock], [], [] 377 | if select: 378 | if self.timeout is None: 379 | r, w, x = select(r, w, x) 380 | else: 381 | r, w, x = select(r, w, x, self.timeout) 382 | if self.sock not in r: 383 | break 384 | reply, fromaddr = self.sock.recvfrom(BUFSIZE) 385 | u = self.unpacker 386 | u.reset(reply) 387 | xid, verf = u.unpack_replyheader() 388 | if xid != self.lastxid: 389 | ## print 'BAD xid' 390 | continue 391 | reply = unpack_func() 392 | self.unpacker.done() 393 | replies.append((reply, fromaddr)) 394 | if self.reply_handler: 395 | self.reply_handler(reply, fromaddr) 396 | return replies 397 | 398 | 399 | # Port mapper interface 400 | 401 | # Program number, version and (fixed!) port number 402 | PMAP_PROG = 100000 403 | PMAP_VERS = 2 404 | PMAP_PORT = 111 405 | 406 | # Procedure numbers 407 | PMAPPROC_NULL = 0 # (void) -> void 408 | PMAPPROC_SET = 1 # (mapping) -> bool 409 | PMAPPROC_UNSET = 2 # (mapping) -> bool 410 | PMAPPROC_GETPORT = 3 # (mapping) -> unsigned int 411 | PMAPPROC_DUMP = 4 # (void) -> pmaplist 412 | PMAPPROC_CALLIT = 5 # (call_args) -> call_result 413 | 414 | # A mapping is (prog, vers, prot, port) and prot is one of: 415 | 416 | IPPROTO_TCP = 6 417 | IPPROTO_UDP = 17 418 | 419 | # A pmaplist is a variable-length list of mappings, as follows: 420 | # either (1, mapping, pmaplist) or (0). 421 | 422 | # A call_args is (prog, vers, proc, args) where args is opaque; 423 | # a call_result is (port, res) where res is opaque. 424 | 425 | 426 | class PortMapperPacker(Packer): 427 | 428 | def pack_mapping(self, mapping): 429 | prog, vers, prot, port = mapping 430 | self.pack_uint(prog) 431 | self.pack_uint(vers) 432 | self.pack_uint(prot) 433 | self.pack_uint(port) 434 | 435 | def pack_pmaplist(self, list): 436 | self.pack_list(list, self.pack_mapping) 437 | 438 | def pack_call_args(self, ca): 439 | prog, vers, proc, args = ca 440 | self.pack_uint(prog) 441 | self.pack_uint(vers) 442 | self.pack_uint(proc) 443 | self.pack_opaque(args) 444 | 445 | 446 | class PortMapperUnpacker(Unpacker): 447 | 448 | def unpack_mapping(self): 449 | prog = self.unpack_uint() 450 | vers = self.unpack_uint() 451 | prot = self.unpack_uint() 452 | port = self.unpack_uint() 453 | return prog, vers, prot, port 454 | 455 | def unpack_pmaplist(self): 456 | return self.unpack_list(self.unpack_mapping) 457 | 458 | def unpack_call_result(self): 459 | port = self.unpack_uint() 460 | res = self.unpack_opaque() 461 | return port, res 462 | 463 | 464 | class PartialPortMapperClient: 465 | 466 | def __init__(self): 467 | self.packer = PortMapperPacker() 468 | self.unpacker = PortMapperUnpacker('') 469 | 470 | def set(self, mapping): 471 | return self.make_call(PMAPPROC_SET, mapping, \ 472 | self.packer.pack_mapping, \ 473 | self.unpacker.unpack_uint) 474 | 475 | def unset(self, mapping): 476 | return self.make_call(PMAPPROC_UNSET, mapping, \ 477 | self.packer.pack_mapping, \ 478 | self.unpacker.unpack_uint) 479 | 480 | def get_port(self, mapping): 481 | return self.make_call(PMAPPROC_GETPORT, mapping, \ 482 | self.packer.pack_mapping, \ 483 | self.unpacker.unpack_uint) 484 | 485 | def dump(self): 486 | return self.make_call(PMAPPROC_DUMP, None, \ 487 | None, \ 488 | self.unpacker.unpack_pmaplist) 489 | 490 | def callit(self, ca): 491 | return self.make_call(PMAPPROC_CALLIT, ca, \ 492 | self.packer.pack_call_args, \ 493 | self.unpacker.unpack_call_result) 494 | 495 | 496 | class TCPPortMapperClient(PartialPortMapperClient, RawTCPClient): 497 | 498 | def __init__(self, host): 499 | RawTCPClient.__init__(self, host, PMAP_PROG, PMAP_VERS, PMAP_PORT) 500 | PartialPortMapperClient.__init__(self) 501 | 502 | 503 | class UDPPortMapperClient(PartialPortMapperClient, RawUDPClient): 504 | 505 | def __init__(self, host): 506 | RawUDPClient.__init__(self, host, PMAP_PROG, PMAP_VERS, PMAP_PORT) 507 | PartialPortMapperClient.__init__(self) 508 | 509 | 510 | class BroadcastUDPPortMapperClient(PartialPortMapperClient, RawBroadcastUDPClient): 511 | 512 | def __init__(self, bcastaddr): 513 | RawBroadcastUDPClient.__init__(self, bcastaddr, PMAP_PROG, PMAP_VERS, PMAP_PORT) 514 | PartialPortMapperClient.__init__(self) 515 | 516 | 517 | # Generic clients that find their server through the Port mapper 518 | 519 | class TCPClient(RawTCPClient): 520 | 521 | def __init__(self, host, prog, vers, port=0): 522 | if port == 0: 523 | pmap = TCPPortMapperClient(host) 524 | port = pmap.get_port((prog, vers, IPPROTO_TCP, 0)) 525 | pmap.close() 526 | if port == 0: 527 | raise RPCError('program not registered') 528 | RawTCPClient.__init__(self, host, prog, vers, port) 529 | 530 | 531 | class UDPClient(RawUDPClient): 532 | 533 | def __init__(self, host, prog, vers, port=0): 534 | if port == 0: 535 | pmap = UDPPortMapperClient(host) 536 | port = pmap.get_port((prog, vers, IPPROTO_UDP, 0)) 537 | pmap.close() 538 | if port == 0: 539 | raise RPCError('program not registered') 540 | RawUDPClient.__init__(self, host, prog, vers, port) 541 | 542 | 543 | class BroadcastUDPClient(Client): 544 | 545 | def __init__(self, bcastaddr, prog, vers): 546 | self.pmap = BroadcastUDPPortMapperClient(bcastaddr) 547 | self.pmap.set_reply_handler(self.my_reply_handler) 548 | self.prog = prog 549 | self.vers = vers 550 | self.user_reply_handler = None 551 | self.addpackers() 552 | 553 | def close(self): 554 | self.pmap.close() 555 | 556 | def set_reply_handler(self, reply_handler): 557 | self.user_reply_handler = reply_handler 558 | 559 | def set_timeout(self, timeout): 560 | self.pmap.set_timeout(timeout) 561 | 562 | def my_reply_handler(self, reply, fromaddr): 563 | port, res = reply 564 | self.unpacker.reset(res) 565 | result = self.unpack_func() 566 | self.unpacker.done() 567 | self.replies.append((result, fromaddr)) 568 | if self.user_reply_handler is not None: 569 | self.user_reply_handler(result, fromaddr) 570 | 571 | def make_call(self, proc, args, pack_func, unpack_func): 572 | self.packer.reset() 573 | if pack_func: 574 | pack_func(args) 575 | if unpack_func is None: 576 | def dummy(): pass 577 | self.unpack_func = dummy 578 | else: 579 | self.unpack_func = unpack_func 580 | self.replies = [] 581 | packed_args = self.packer.get_buf() 582 | dummy_replies = self.pmap.Callit( \ 583 | (self.prog, self.vers, proc, packed_args)) 584 | return self.replies 585 | 586 | 587 | # Server classes 588 | 589 | # These are not symmetric to the Client classes 590 | # XXX No attempt is made to provide authorization hooks yet 591 | 592 | class RPCRequestHandler(socketserver.BaseRequestHandler): 593 | def __init__(self, request, client_address, server): 594 | logger.info('starting new request handler for client %s, request %r',client_address, request) 595 | self.addpackers() 596 | self.request = request 597 | self.client_address = client_address 598 | self.server = server 599 | socketserver.BaseRequestHandler.__init__(self, request, client_address, server) 600 | 601 | def setup(self): 602 | #print 'starting request handler()' 603 | return socketserver.BaseRequestHandler.setup(self) 604 | 605 | def finish(self): 606 | #print 'finishing request handler' 607 | return socketserver.BaseRequestHandler.finish(self) 608 | 609 | def handle(self): 610 | 611 | while True: 612 | try: 613 | call = recvrecord(self.request) 614 | reply = self.handle_call(call) 615 | if reply is not None: 616 | sendrecord(self.request, reply) 617 | except(EOFError, ConnectionError): 618 | #print 'rpcrequesthandler.handle() got EOF, exiting' 619 | break 620 | 621 | return 622 | 623 | def handle_call(self, call): 624 | # Don't use unpack_header but parse the header piecewise 625 | # XXX I have no idea if I am using the right error responses! 626 | svr_prog, svr_vers, svr_prot, svr_port = self.server.mapping 627 | 628 | self.unpacker.reset(call) 629 | self.packer.reset() 630 | xid = self.unpacker.unpack_uint() 631 | self.packer.pack_uint(xid) 632 | temp = self.unpacker.unpack_enum() 633 | if temp != CALL: 634 | return None # Not worthy of a reply 635 | 636 | self.packer.pack_uint(REPLY) 637 | temp = self.unpacker.unpack_uint() 638 | if temp != RPCVERSION: 639 | self.packer.pack_uint(MSG_DENIED) 640 | self.packer.pack_uint(RPC_MISMATCH) 641 | self.packer.pack_uint(RPCVERSION) 642 | self.packer.pack_uint(RPCVERSION) 643 | return self.packer.get_buf() 644 | 645 | self.packer.pack_uint(MSG_ACCEPTED) 646 | self.packer.pack_auth((AUTH_NULL, make_auth_null())) 647 | prog = self.unpacker.unpack_uint() 648 | if prog != svr_prog: 649 | self.packer.pack_uint(PROG_UNAVAIL) 650 | return self.packer.get_buf() 651 | 652 | vers = self.unpacker.unpack_uint() 653 | if vers != svr_vers: 654 | self.packer.pack_uint(PROG_MISMATCH) 655 | self.packer.pack_uint(self.vers) 656 | self.packer.pack_uint(self.vers) 657 | return self.packer.get_buf() 658 | proc = self.unpacker.unpack_uint() 659 | 660 | methname = 'handle_' + repr(proc) 661 | try: 662 | meth = getattr(self, methname) 663 | except AttributeError: 664 | logger.debug("requested procedure not avaliable: %r",proc) 665 | self.packer.pack_uint(PROC_UNAVAIL) 666 | return self.packer.get_buf() 667 | 668 | cred = self.unpacker.unpack_auth() 669 | verf = self.unpacker.unpack_auth() 670 | try: 671 | logger.debug("running proceduire: %r",proc) 672 | meth() # Unpack args, call turn_around(), pack reply 673 | except (EOFError, RPCGarbageArgs): 674 | # Too few or too many arguments 675 | self.packer.reset() 676 | self.packer.pack_uint(xid) 677 | self.packer.pack_uint(REPLY) 678 | self.packer.pack_uint(MSG_ACCEPTED) 679 | self.packer.pack_auth((AUTH_NULL, make_auth_null())) 680 | self.packer.pack_uint(GARBAGE_ARGS) 681 | 682 | return self.packer.get_buf() 683 | 684 | def turn_around(self): 685 | try: 686 | self.unpacker.done() 687 | except RuntimeError: 688 | raise RPCGarbageArgs 689 | self.packer.pack_uint(SUCCESS) 690 | 691 | def handle_0(self): # Handle NULL message 692 | self.turn_around() 693 | 694 | def addpackers(self): 695 | # Override this to use derived classes from Packer/Unpacker 696 | self.packer = Packer() 697 | self.unpacker = Unpacker('') 698 | 699 | class TCPServer(socketserver.TCPServer): 700 | def __init__(self, host, prog, vers, port, handler_class=RPCRequestHandler): 701 | # host should normally be '' for default interface 702 | # port should normally be 0 for random port 703 | self.registered = False 704 | 705 | server_address = (host, port) 706 | socketserver.TCPServer.__init__(self, server_address, handler_class) 707 | 708 | host, port = self.server_address 709 | prot = IPPROTO_TCP 710 | self.mapping = prog, vers, prot, port 711 | return 712 | 713 | def __del__(self): 714 | # make sure to unregister on delete 715 | if self.registered: 716 | self.unregister() 717 | return 718 | 719 | def register(self): 720 | try: 721 | #super(Vxi11Server, self).unregister() 722 | self.unregister() 723 | except socket.error as msg: 724 | logger.error('Error: rpcbind -i not running? %s', msg) 725 | except RuntimeError as msg: 726 | logger.error('RuntimeError: %s (ignored)', msg) 727 | try: 728 | self.register_pmap() 729 | except RuntimeError as msg: 730 | logger.error("Error: rpcbind running in secure mode? %s", msg) 731 | raise 732 | return 733 | 734 | def register_pmap(self): 735 | host, port = self.server_address 736 | p = TCPPortMapperClient(host) 737 | logger.info('registering %s on %s', self.mapping, self.server_address) 738 | if not p.set(self.mapping): 739 | raise RPCError('register failed') 740 | self.registered = True 741 | return 742 | 743 | def unregister(self): 744 | host, port = self.server_address 745 | p = TCPPortMapperClient(host) 746 | if not p.unset(self.mapping): 747 | raise RPCError('unregister failed') 748 | self.registered = False 749 | return 750 | 751 | 752 | 753 | 754 | 755 | # class UDPServer(Server): 756 | 757 | # def __init__(self, host, prog, vers, port): 758 | # Server.__init__(self, host, prog, vers, port) 759 | # self.connect() 760 | 761 | # def connect(self): 762 | # self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 763 | # self.prot = IPPROTO_UDP 764 | # self.sock.bind((self.host, self.port)) 765 | # self.host, self.port = self.sock.getsockname() 766 | 767 | # def loop(self): 768 | # while 1: 769 | # self.session() 770 | 771 | # def session(self): 772 | # call, host_port = self.sock.recvfrom(8192) 773 | # reply = self.handle(call) 774 | # if reply is not None: 775 | # self.sock.sendto(reply, host_port) 776 | 777 | 778 | # Simple test program -- dump portmapper status 779 | 780 | def test(host = ''): 781 | pmap = UDPPortMapperClient(host) 782 | list = pmap.dump() 783 | list.sort() 784 | for prog, vers, prot, port in list: 785 | st = "%d %d " % (prog, vers) 786 | if prot == IPPROTO_TCP: st += "tcp " 787 | elif prot == IPPROTO_UDP: st += "udp " 788 | else: st += "%d " % prot 789 | st += "%d" % port 790 | print(st) 791 | 792 | 793 | -------------------------------------------------------------------------------- /vxi11_server/vxi11.py: -------------------------------------------------------------------------------- 1 | """ 2 | 3 | Python VXI-11 driver 4 | 5 | Copyright (c) 2012-2017 Alex Forencich and Michael Walle 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. 24 | 25 | """ 26 | 27 | from . import rpc 28 | #from . import rpc 29 | import random 30 | import re 31 | import struct 32 | import time 33 | import threading 34 | import ipaddress 35 | import socketserver 36 | import logging 37 | 38 | logger = logging.getLogger(__name__) 39 | 40 | # VXI-11 RPC constants 41 | 42 | # Device async 43 | DEVICE_ASYNC_PROG = 0x0607b0 44 | DEVICE_ASYNC_VERS = 1 45 | DEVICE_ABORT = 1 46 | 47 | # Device core 48 | DEVICE_CORE_PROG = 0x0607af 49 | DEVICE_CORE_VERS = 1 50 | CREATE_LINK = 10 51 | DEVICE_WRITE = 11 52 | DEVICE_READ = 12 53 | DEVICE_READSTB = 13 54 | DEVICE_TRIGGER = 14 55 | DEVICE_CLEAR = 15 56 | DEVICE_REMOTE = 16 57 | DEVICE_LOCAL = 17 58 | DEVICE_LOCK = 18 59 | DEVICE_UNLOCK = 19 60 | DEVICE_ENABLE_SRQ = 20 61 | DEVICE_DOCMD = 22 62 | DESTROY_LINK = 23 63 | CREATE_INTR_CHAN = 25 64 | DESTROY_INTR_CHAN = 26 65 | 66 | # Device intr 67 | DEVICE_INTR_PROG = 0x0607b1 68 | DEVICE_INTR_VERS = 1 69 | DEVICE_INTR_SRQ = 30 70 | DEVICE_TCP = 0 71 | DEVICE_UDP = 1 72 | 73 | # Error states 74 | ERR_NO_ERROR = 0 75 | ERR_SYNTAX_ERROR = 1 76 | ERR_DEVICE_NOT_ACCESSIBLE = 3 77 | ERR_INVALID_LINK_IDENTIFIER = 4 78 | ERR_PARAMETER_ERROR = 5 79 | ERR_CHANNEL_NOT_ESTABLISHED = 6 80 | ERR_OPERATION_NOT_SUPPORTED = 8 81 | ERR_OUT_OF_RESOURCES = 9 82 | ERR_DEVICE_LOCKED_BY_ANOTHER_LINK = 11 83 | ERR_NO_LOCK_HELD_BY_THIS_LINK = 12 84 | ERR_IO_TIMEOUT = 15 85 | ERR_IO_ERROR = 17 86 | ERR_INVALID_ADDRESS = 21 87 | ERR_ABORT = 23 88 | ERR_CHANNEL_ALREADY_ESTABLISHED = 29 89 | 90 | # Flags 91 | OP_FLAG_WAIT_BLOCK = 1 92 | OP_FLAG_END = 8 93 | OP_FLAG_TERMCHAR_SET = 128 94 | 95 | RX_REQCNT = 1 96 | RX_CHR = 2 97 | RX_END = 4 98 | 99 | # IEEE 488.1 interface device commands 100 | CMD_SEND_COMMAND = 0x020000 101 | CMD_BUS_STATUS = 0x020001 102 | CMD_ATN_CTRL = 0x020002 103 | CMD_REN_CTRL = 0x020003 104 | CMD_PASS_CTRL = 0x020004 105 | CMD_BUS_ADDRESS = 0x02000A 106 | CMD_IFC_CTRL = 0x020010 107 | 108 | CMD_BUS_STATUS_REMOTE = 1 109 | CMD_BUS_STATUS_SRQ = 2 110 | CMD_BUS_STATUS_NDAC = 3 111 | CMD_BUS_STATUS_SYSTEM_CONTROLLER = 4 112 | CMD_BUS_STATUS_CONTROLLER_IN_CHARGE = 5 113 | CMD_BUS_STATUS_TALKER = 6 114 | CMD_BUS_STATUS_LISTENER = 7 115 | CMD_BUS_STATUS_BUS_ADDRESS = 8 116 | 117 | GPIB_CMD_GTL = 0x01 # go to local 118 | GPIB_CMD_SDC = 0x04 # selected device clear 119 | GPIB_CMD_PPC = 0x05 # parallel poll config 120 | GPIB_CMD_GET = 0x08 # group execute trigger 121 | GPIB_CMD_TCT = 0x09 # take control 122 | GPIB_CMD_LLO = 0x11 # local lockout 123 | GPIB_CMD_DCL = 0x14 # device clear 124 | GPIB_CMD_PPU = 0x15 # parallel poll unconfigure 125 | GPIB_CMD_SPE = 0x18 # serial poll enable 126 | GPIB_CMD_SPD = 0x19 # serial poll disable 127 | GPIB_CMD_LAD = 0x20 # listen address (base) 128 | GPIB_CMD_UNL = 0x3F # unlisten 129 | GPIB_CMD_TAD = 0x40 # talk address (base) 130 | GPIB_CMD_UNT = 0x5F # untalk 131 | GPIB_CMD_SAD = 0x60 # my secondary address (base) 132 | GPIB_CMD_PPE = 0x60 # parallel poll enable (base) 133 | GPIB_CMD_PPD = 0x70 # parallel poll disable 134 | 135 | def parse_visa_resource_string(resource_string): 136 | # valid resource strings: 137 | # TCPIP::10.0.0.1::INSTR 138 | # TCPIP0::10.0.0.1::INSTR 139 | # TCPIP::10.0.0.1::gpib,5::INSTR 140 | # TCPIP0::10.0.0.1::gpib,5::INSTR 141 | # TCPIP0::10.0.0.1::usb0::INSTR 142 | # TCPIP0::10.0.0.1::usb0[1234::5678::MYSERIAL::0]::INSTR 143 | m = re.match('^(?P(?PTCPIP)\d*)(::(?P[^\s:]+))' 144 | '(::(?P[^\s:]+(\[.+\])?))?(::(?PINSTR))$', 145 | resource_string, re.I) 146 | 147 | if m is not None: 148 | return dict( 149 | type = m.group('type').upper(), 150 | prefix = m.group('prefix'), 151 | arg1 = m.group('arg1'), 152 | arg2 = m.group('arg2'), 153 | suffix = m.group('suffix'), 154 | ) 155 | 156 | # Exceptions 157 | class Vxi11Exception(Exception): 158 | em = {0: "No error", 159 | 1: "Syntax error", 160 | 3: "Device not accessible", 161 | 4: "Invalid link identifier", 162 | 5: "Parameter error", 163 | 6: "Channel not established", 164 | 8: "Operation not supported", 165 | 9: "Out of resources", 166 | 11: "Device locked by another link", 167 | 12: "No lock held by this link", 168 | 15: "IO timeout", 169 | 17: "IO error", 170 | 21: "Invalid address", 171 | 23: "Abort", 172 | 29: "Channel already established"} 173 | 174 | def __init__(self, err = None, note = None): 175 | self.err = err 176 | self.note = note 177 | self.msg = '' 178 | 179 | if err is None: 180 | self.msg = note 181 | else: 182 | if type(err) is int: 183 | if err in self.em: 184 | self.msg = "%d: %s" % (err, self.em[err]) 185 | else: 186 | self.msg = "%d: Unknown error" % err 187 | else: 188 | self.msg = err 189 | if note is not None: 190 | self.msg = "%s [%s]" % (self.msg, note) 191 | 192 | def __str__(self): 193 | return self.msg 194 | 195 | class Packer(rpc.Packer): 196 | def pack_device_link(self, link): 197 | self.pack_int(link) 198 | 199 | def pack_create_link_parms(self, params): 200 | id, lock_device, lock_timeout, device = params 201 | self.pack_int(id) 202 | self.pack_bool(lock_device) 203 | self.pack_uint(lock_timeout) 204 | self.pack_string(device) 205 | 206 | def pack_device_write_parms(self, params): 207 | link, timeout, lock_timeout, flags, data = params 208 | self.pack_int(link) 209 | self.pack_uint(timeout) 210 | self.pack_uint(lock_timeout) 211 | self.pack_int(flags) 212 | self.pack_opaque(data) 213 | 214 | def pack_device_read_parms(self, params): 215 | link, request_size, timeout, lock_timeout, flags, term_char = params 216 | self.pack_int(link) 217 | self.pack_uint(request_size) 218 | self.pack_uint(timeout) 219 | self.pack_uint(lock_timeout) 220 | self.pack_int(flags) 221 | self.pack_int(term_char) 222 | 223 | def pack_device_generic_parms(self, params): 224 | link, flags, lock_timeout, timeout = params 225 | self.pack_int(link) 226 | self.pack_int(flags) 227 | self.pack_uint(lock_timeout) 228 | self.pack_uint(timeout) 229 | 230 | def pack_device_remote_func_parms(self, params): 231 | host_addr, host_port, prog_num, prog_vers, prog_family = params 232 | self.pack_uint(host_addr) 233 | self.pack_uint(host_port) 234 | self.pack_uint(prog_num) 235 | self.pack_uint(prog_vers) 236 | self.pack_int(prog_family) 237 | 238 | def pack_device_enable_srq_parms(self, params): 239 | link, enable, handle = params 240 | self.pack_int(link) 241 | self.pack_bool(enable) 242 | if len(handle) > 40: 243 | raise Vxi11Exception(ERR_PARAMETER_ERROR, "array length too long") 244 | self.pack_opaque(handle) 245 | 246 | def pack_device_lock_parms(self, params): 247 | link, flags, lock_timeout = params 248 | self.pack_int(link) 249 | self.pack_int(flags) 250 | self.pack_uint(lock_timeout) 251 | 252 | def pack_device_docmd_parms(self, params): 253 | link, flags, timeout, lock_timeout, cmd, network_order, datasize, data_in = params 254 | self.pack_int(link) 255 | self.pack_int(flags) 256 | self.pack_uint(timeout) 257 | self.pack_uint(lock_timeout) 258 | self.pack_int(cmd) 259 | self.pack_bool(network_order) 260 | self.pack_int(datasize) 261 | self.pack_opaque(data_in) 262 | 263 | def pack_device_error(self, error): 264 | self.pack_int(error) 265 | 266 | def pack_device_intr_srq_parms(self, params): 267 | handle = params 268 | self.pack_opaque(handle) 269 | 270 | def pack_create_link_resp(self, params): 271 | error, link, abort_port, max_recv_size = params 272 | self.pack_device_error(error) 273 | #self.pack_int(error) 274 | self.pack_int(link) 275 | self.pack_uint(abort_port) 276 | self.pack_uint(max_recv_size) 277 | 278 | def pack_device_write_resp(self, params): 279 | error, size = params 280 | self.pack_device_error(error) 281 | #self.pack_int(error) 282 | self.pack_uint(size) 283 | 284 | def pack_device_read_resp(self, params): 285 | error, reason, data = params 286 | self.pack_device_error(error) 287 | #self.pack_int(error) 288 | self.pack_int(reason) 289 | self.pack_opaque(data) 290 | 291 | def pack_device_read_stb_resp(self, params): 292 | error, stb = params 293 | self.pack_device_error(error) 294 | #self.pack_int(error) 295 | self.pack_uint(stb) 296 | 297 | def pack_device_docmd_resp(self, params): 298 | error, data_out = params 299 | self.pack_device_error(error) 300 | #self.pack_int(error) 301 | self.pack_opaque(data_out) 302 | 303 | class Unpacker(rpc.Unpacker): 304 | def unpack_device_link(self): 305 | return self.unpack_int() 306 | 307 | def unpack_create_link_parms(self): 308 | id = self.unpack_int() 309 | lock_device = self.unpack_bool() 310 | lock_timeout = self.unpack_uint() 311 | device = self.unpack_string().decode("ascii", "ignore") 312 | return id, lock_device, lock_timeout, device 313 | 314 | def unpack_device_write_parms(self): 315 | link = self.unpack_int() 316 | timeout = self.unpack_uint() 317 | lock_timeout = self.unpack_uint() 318 | flags = self.unpack_int() 319 | data = self.unpack_opaque() 320 | return link, timeout, lock_timeout, flags, data 321 | 322 | def unpack_device_read_parms(self): 323 | link = self.unpack_int() 324 | request_size = self.unpack_uint() 325 | timeout = self.unpack_uint() 326 | lock_timeout = self.unpack_uint() 327 | flags = self.unpack_int() 328 | term_char = self.unpack_int() 329 | return link, request_size, timeout, lock_timeout, flags, term_char 330 | 331 | def unpack_device_generic_parms(self): 332 | link = self.unpack_int() 333 | flags = self.unpack_int() 334 | lock_timeout = self.unpack_uint() 335 | timeout = self.unpack_uint() 336 | return link, flags, lock_timeout, timeout 337 | 338 | def unpack_device_remote_func_parms(self): 339 | host_addr = self.unpack_uint() 340 | host_port = self.unpack_uint() 341 | prog_num = self.unpack_uint() 342 | prog_vers = self.unpack_uint() 343 | prog_family = self.unpack_int() 344 | return host_addr, host_port, prog_num, prog_vers, prog_family 345 | 346 | def unpack_device_enable_srq_parms(self): 347 | link = self.unpack_int() 348 | enable = self.unpack_bool() 349 | handle = self.unpack_opaque() 350 | return link, enable, handle 351 | 352 | def unpack_device_lock_parms(self): 353 | link = self.unpack_int() 354 | flags = self.unpack_int() 355 | lock_timeout = self.unpack_uint() 356 | return link, flags, lock_timeout 357 | 358 | def unpack_device_docmd_parms(self): 359 | link = self.unpack_int() 360 | flags = self.unpack_int() 361 | timeout = self.unpack_uint() 362 | lock_timeout = self.unpack_uint() 363 | cmd = self.unpack_int() 364 | network_order = self.unpack_bool() 365 | datasize = self.unpack_int() 366 | data_in = self.unpack_opaque() 367 | return link, flags, timeout, lock_timeout, cmd, network_order, datasize, data_in 368 | 369 | def unpack_device_error(self): 370 | return self.unpack_int() 371 | 372 | def unpack_device_intr_srq_params(self): 373 | handle = self.unpack_opaque() 374 | return handle 375 | 376 | def unpack_create_link_resp(self): 377 | error = self.unpack_int() 378 | link = self.unpack_int() 379 | abort_port = self.unpack_uint() 380 | max_recv_size = self.unpack_uint() 381 | return error, link, abort_port, max_recv_size 382 | 383 | def unpack_device_write_resp(self): 384 | error = self.unpack_int() 385 | size = self.unpack_uint() 386 | return error, size 387 | 388 | def unpack_device_read_resp(self): 389 | error = self.unpack_int() 390 | reason = self.unpack_int() 391 | data = self.unpack_opaque() 392 | return error, reason, data 393 | 394 | def unpack_device_read_stb_resp(self): 395 | error = self.unpack_int() 396 | stb = self.unpack_uint() 397 | return error, stb 398 | 399 | def unpack_device_docmd_resp(self): 400 | error = self.unpack_int() 401 | data_out = self.unpack_opaque() 402 | return error, data_out 403 | 404 | def done(self): 405 | # ignore any trailing bytes 406 | pass 407 | 408 | 409 | class CoreClient(rpc.TCPClient): 410 | def __init__(self, host, port=0): 411 | self.packer = Packer() 412 | self.unpacker = Unpacker('') 413 | rpc.TCPClient.__init__(self, host, DEVICE_CORE_PROG, DEVICE_CORE_VERS, port) 414 | 415 | def create_link(self, id, lock_device, lock_timeout, name): 416 | params = (id, lock_device, lock_timeout, name) 417 | return self.make_call(CREATE_LINK, params, 418 | self.packer.pack_create_link_parms, 419 | self.unpacker.unpack_create_link_resp) 420 | 421 | def device_write(self, link, timeout, lock_timeout, flags, data): 422 | params = (link, timeout, lock_timeout, flags, data) 423 | return self.make_call(DEVICE_WRITE, params, 424 | self.packer.pack_device_write_parms, 425 | self.unpacker.unpack_device_write_resp) 426 | 427 | def device_read(self, link, request_size, timeout, lock_timeout, flags, term_char): 428 | params = (link, request_size, timeout, lock_timeout, flags, term_char) 429 | return self.make_call(DEVICE_READ, params, 430 | self.packer.pack_device_read_parms, 431 | self.unpacker.unpack_device_read_resp) 432 | 433 | def device_read_stb(self, link, flags, lock_timeout, timeout): 434 | params = (link, flags, lock_timeout, timeout) 435 | return self.make_call(DEVICE_READSTB, params, 436 | self.packer.pack_device_generic_parms, 437 | self.unpacker.unpack_device_read_stb_resp) 438 | 439 | def device_trigger(self, link, flags, lock_timeout, timeout): 440 | params = (link, flags, lock_timeout, timeout) 441 | return self.make_call(DEVICE_TRIGGER, params, 442 | self.packer.pack_device_generic_parms, 443 | self.unpacker.unpack_device_error) 444 | 445 | def device_clear(self, link, flags, lock_timeout, timeout): 446 | params = (link, flags, lock_timeout, timeout) 447 | return self.make_call(DEVICE_CLEAR, params, 448 | self.packer.pack_device_generic_parms, 449 | self.unpacker.unpack_device_error) 450 | 451 | def device_remote(self, link, flags, lock_timeout, timeout): 452 | params = (link, flags, lock_timeout, timeout) 453 | return self.make_call(DEVICE_REMOTE, params, 454 | self.packer.pack_device_generic_parms, 455 | self.unpacker.unpack_device_error) 456 | 457 | def device_local(self, link, flags, lock_timeout, timeout): 458 | params = (link, flags, lock_timeout, timeout) 459 | return self.make_call(DEVICE_LOCAL, params, 460 | self.packer.pack_device_generic_parms, 461 | self.unpacker.unpack_device_error) 462 | 463 | def device_lock(self, link, flags, lock_timeout): 464 | params = (link, flags, lock_timeout) 465 | return self.make_call(DEVICE_LOCK, params, 466 | self.packer.pack_device_lock_parms, 467 | self.unpacker.unpack_device_error) 468 | 469 | def device_unlock(self, link): 470 | return self.make_call(DEVICE_UNLOCK, link, 471 | self.packer.pack_device_link, 472 | self.unpacker.unpack_device_error) 473 | 474 | def device_enable_srq(self, link, enable, handle): 475 | params = (link, enable, handle) 476 | return self.make_call(DEVICE_ENABLE_SRQ, params, 477 | self.packer.pack_device_enable_srq_parms, 478 | self.unpacker.unpack_device_error) 479 | 480 | def device_docmd(self, link, flags, timeout, lock_timeout, cmd, network_order, datasize, data_in): 481 | params = (link, flags, timeout, lock_timeout, cmd, network_order, datasize, data_in) 482 | return self.make_call(DEVICE_DOCMD, params, 483 | self.packer.pack_device_docmd_parms, 484 | self.unpacker.unpack_device_docmd_resp) 485 | 486 | def destroy_link(self, link): 487 | return self.make_call(DESTROY_LINK, link, 488 | self.packer.pack_device_link, 489 | self.unpacker.unpack_device_error) 490 | 491 | def create_intr_chan(self, host_addr, host_port, prog_num, prog_vers, prog_family): 492 | params = (host_addr, host_port, prog_num, prog_vers, prog_family) 493 | return self.make_call(CREATE_INTR_CHAN, params, 494 | self.packer.pack_device_remote_func_parms, 495 | self.unpacker.unpack_device_error) 496 | 497 | def destroy_intr_chan(self): 498 | return self.make_call(DESTROY_INTR_CHAN, None, 499 | None, 500 | self.unpacker.unpack_device_error) 501 | 502 | 503 | class AbortClient(rpc.TCPClient): 504 | def __init__(self, host, port=0): 505 | self.packer = Packer() 506 | self.unpacker = Unpacker('') 507 | rpc.TCPClient.__init__(self, host, DEVICE_ASYNC_PROG, DEVICE_ASYNC_VERS, port) 508 | 509 | def device_abort(self, link): 510 | return self.make_call(DEVICE_ABORT, link, 511 | self.packer.pack_device_link, 512 | self.unpacker.unpack_device_error) 513 | 514 | class TCPIntrClient(rpc.TCPClient): 515 | def __init__(self, host, port ): 516 | self.packer = Packer() 517 | self.unpacker = Unpacker('') 518 | rpc.TCPClient.__init__(self, host, DEVICE_INTR_PROG, DEVICE_INTR_VERS, port) 519 | 520 | def do_call(self): 521 | call = self.packer.get_buf() 522 | rpc.sendrecord(self.sock, call) 523 | # vxi11 spec B.3.1. states that SRQ should not wait for an answer 524 | # because of deadlocks 525 | # so overwrite the original method of RawTCPClient 526 | 527 | def signal_intr_srq(self, handle): 528 | self.make_call(DEVICE_INTR_SRQ, handle, 529 | self.packer.pack_device_intr_srq_parms, None ) 530 | 531 | class IntrHandler(rpc.RPCRequestHandler): 532 | def addpackers(self): 533 | # amend rpc packers with our vxi11 packers 534 | self.packer = Packer() 535 | self.unpacker = Unpacker('') 536 | 537 | def handle_30(self): 538 | # SRQ 539 | params = self.unpacker.unpack_device_intr_srq_params() 540 | handle = params 541 | 542 | logger.debug("got srq for handle %r",handle) 543 | # find the device to send SRQ to via handle and registry 544 | try: 545 | self.server.SRQ_CLASS_REGISTRY[handle].srq_callback() 546 | except KeyError: 547 | logger.error("got srq for unknown handle %r",handle) 548 | # we do not complain because nobody is waiting for an answer. 549 | except TypeError as e: 550 | logger.error("got srq but callback is invalid: %s",str(e) ) 551 | 552 | # turn_around() also checks for garbage arguments and raises errors, 553 | # so don't call it - nobody will listen to complains anyway 554 | # self.turn_around() 555 | 556 | # vxi11 spec B.3.1. states that SRQ should not reply anything 557 | # because this will cause deadlocks. 558 | # so empty the packer to remove anything that handle_call packed already. 559 | # sendrecord() will not transmit empty records 560 | self.packer.reset() 561 | 562 | class IntrServer(socketserver.ThreadingMixIn, rpc.TCPServer): 563 | INTR_SERVER=None 564 | SRQ_CLASS_REGISTRY={} 565 | 566 | @classmethod 567 | def getServer(cls): 568 | # create global interrupt server instance if it does not exist 569 | if cls.INTR_SERVER is None: 570 | # create a new server task and register our handler 571 | serv=cls.INTR_SERVER=IntrServer('',0) # default addr, new random port 572 | intrThread = threading.Thread(target=serv.serve_forever) 573 | intrThread.setDaemon(True) # don't hang on exit 574 | intrThread.start() 575 | logger.info('IntrServer started...') 576 | else: 577 | serv=cls.INTR_SERVER 578 | return serv 579 | 580 | @classmethod 581 | def stopServer(cls): 582 | cls.INTR_SERVER.shutdown() 583 | cls.INTR_SERVER.server_close() 584 | 585 | @classmethod 586 | def register_dev(cls,handle,device): 587 | cls.SRQ_CLASS_REGISTRY[handle]=device 588 | 589 | @classmethod 590 | def unregister_dev(cls, handle): 591 | del cls.SRQ_CLASS_REGISTRY[handle] 592 | 593 | @classmethod 594 | def has_dev(cls,handle): 595 | return handle in cls.SRQ_CLASS_REGISTRY 596 | 597 | def __init__(self, host, port ): 598 | rpc.TCPServer.__init__(self, host, DEVICE_INTR_PROG, DEVICE_INTR_VERS, port, IntrHandler) 599 | 600 | 601 | def list_devices(ip=None, timeout=1): 602 | "Detect VXI-11 devices on network" 603 | 604 | if ip is None: 605 | ip = ['255.255.255.255'] 606 | 607 | if type(ip) is str: 608 | ip = [ip] 609 | 610 | hosts = [] 611 | 612 | for addr in ip: 613 | pmap = rpc.BroadcastUDPPortMapperClient(addr) 614 | pmap.set_timeout(timeout) 615 | resp = pmap.get_port((DEVICE_CORE_PROG, DEVICE_CORE_VERS, rpc.IPPROTO_TCP, 0)) 616 | 617 | l = [r[1][0] for r in resp if r[0] > 0] 618 | 619 | hosts.extend(l) 620 | 621 | return sorted(hosts, key=lambda ip: tuple(int(part) for part in ip.split('.'))) 622 | 623 | 624 | def list_resources(ip=None, timeout=1): 625 | "List resource strings for all detected VXI-11 devices" 626 | 627 | res = [] 628 | 629 | for host in list_devices(ip, timeout): 630 | try: 631 | # try connecting as an instrument 632 | instr = Instrument(host) 633 | instr.open() 634 | res.append("TCPIP::%s::INSTR" % host) 635 | except: 636 | try: 637 | # try connecting as a GPIB interface 638 | intf_dev = InterfaceDevice(host) 639 | # enumerate connected devices 640 | devs = intf_dev.find_listeners() 641 | res.extend(['TCPIP::%s::gpib0,%d::INSTR' % (host, d) for d in devs]) 642 | except: 643 | # if that fails, just list the host 644 | res.append("TCPIP::%s::INSTR" % host) 645 | 646 | return res 647 | 648 | 649 | class Device(object): 650 | "VXI-11 device interface client" 651 | def __init__(self, host, name = None, client_id = None, term_char = None, lock_on_open = False): 652 | "Create new VXI-11 device object" 653 | self.link = None 654 | 655 | if host.upper().startswith('TCPIP') and '::' in host: 656 | res = parse_visa_resource_string(host) 657 | 658 | if res is None: 659 | raise Vxi11Exception('Invalid resource string', 'init') 660 | 661 | host = res['arg1'] 662 | name = res['arg2'] 663 | 664 | if name is None: 665 | name = "inst0" 666 | 667 | if client_id is None: 668 | client_id = random.getrandbits(31) 669 | 670 | self.client = None 671 | self.abort_client = None 672 | self.srq_callback=None 673 | 674 | self.host = host 675 | self.name = name 676 | self.client_id = client_id 677 | self.term_char = term_char 678 | 679 | self.lock_timeout = 10 680 | self.lock_on_open = 0 681 | if lock_on_open: 682 | self.lock_on_open = 1 683 | 684 | self.timeout = 10 685 | self.abort_port = 0 686 | self.max_recv_size = 0 687 | self.max_read_len = 128*1024*1024 688 | self.locked = False 689 | 690 | def __del__(self): 691 | if self.link is not None: 692 | self.close() 693 | 694 | @property 695 | def timeout(self): 696 | return self._timeout 697 | 698 | @timeout.setter 699 | def timeout(self, val): 700 | self._timeout = val 701 | self._timeout_ms = int(val * 1000) 702 | if self.client is not None: 703 | self.client.sock.settimeout(self.timeout+1) 704 | if self.abort_client is not None: 705 | self.abort_client.sock.settimeout(self.timeout+1) 706 | 707 | @property 708 | def lock_timeout(self): 709 | return self._lock_timeout 710 | 711 | @lock_timeout.setter 712 | def lock_timeout(self, val): 713 | self._lock_timeout = val 714 | self._lock_timeout_ms = int(val * 1000) 715 | 716 | def open(self): 717 | "Open connection to VXI-11 device" 718 | if self.link is not None: 719 | return 720 | 721 | if self.client is None: 722 | self.client = CoreClient(self.host) 723 | 724 | self.client.sock.settimeout(self.timeout+1) 725 | error, link, abort_port, max_recv_size = self.client.create_link( 726 | self.client_id, 727 | self.lock_on_open, 728 | self._lock_timeout_ms, 729 | self.name.encode("ascii") 730 | ) 731 | 732 | if error: 733 | raise Vxi11Exception(error, 'open') 734 | 735 | self.abort_port = abort_port 736 | 737 | self.link = link 738 | self.max_recv_size = min(max_recv_size, 1024*1024) 739 | 740 | def close(self): 741 | "Close connection" 742 | if self.link is None: 743 | return 744 | 745 | self.disable_srq_handler() 746 | 747 | self.client.destroy_link(self.link) 748 | self.client.close() 749 | self.link = None 750 | self.client = None 751 | 752 | def abort(self): 753 | "Asynchronous abort" 754 | if self.link is None: 755 | self.open() 756 | 757 | if self.abort_client is None: 758 | self.abort_client = AbortClient(self.host, self.abort_port) 759 | self.abort_client.sock.settimeout(self.timeout) 760 | 761 | error = self.abort_client.device_abort(self.link) 762 | 763 | if error: 764 | raise Vxi11Exception(error, 'abort') 765 | 766 | def write_raw(self, data): 767 | "Write binary data to instrument" 768 | if self.link is None: 769 | self.open() 770 | 771 | if self.term_char is not None: 772 | flags = OP_FLAG_TERMCHAR_SET 773 | term_char = str(self.term_char).encode('ascii')[0] 774 | data += term_char 775 | 776 | flags = 0 777 | 778 | num = len(data) 779 | 780 | offset = 0 781 | 782 | while num > 0: 783 | if num <= self.max_recv_size: 784 | flags |= OP_FLAG_END 785 | 786 | block = data[offset:offset+self.max_recv_size] 787 | 788 | error, size = self.client.device_write( 789 | self.link, 790 | self._timeout_ms, 791 | self._lock_timeout_ms, 792 | flags, 793 | block 794 | ) 795 | 796 | if error: 797 | raise Vxi11Exception(error, 'write') 798 | elif size < len(block): 799 | raise Vxi11Exception("did not write complete block", 'write') 800 | 801 | offset += size 802 | num -= size 803 | 804 | def read_raw(self, num=-1): 805 | "Read binary data from instrument" 806 | if self.link is None: 807 | self.open() 808 | 809 | read_len = self.max_read_len 810 | if num > 0: 811 | read_len = min(num, self.max_read_len) 812 | 813 | flags = 0 814 | reason = 0 815 | 816 | term_char = 0 817 | 818 | if self.term_char is not None: 819 | flags = OP_FLAG_TERMCHAR_SET 820 | term_char = str(self.term_char).encode('ascii')[0] 821 | 822 | read_data = bytearray() 823 | 824 | while reason & (RX_END | RX_CHR) == 0: 825 | error, reason, data = self.client.device_read( 826 | self.link, 827 | read_len, 828 | self._timeout_ms, 829 | self._lock_timeout_ms, 830 | flags, 831 | term_char 832 | ) 833 | 834 | if error: 835 | raise Vxi11Exception(error, 'read') 836 | 837 | read_data.extend(data) 838 | 839 | if num > 0: 840 | num = num - len(data) 841 | if num <= 0: 842 | break 843 | if num < read_len: 844 | read_len = num 845 | 846 | return bytes(read_data) 847 | 848 | def ask_raw(self, data, num=-1): 849 | "Write then read binary data" 850 | self.write_raw(data) 851 | return self.read_raw(num) 852 | 853 | def write(self, message, encoding = 'ascii'): 854 | "Write string to instrument" 855 | if type(message) is tuple or type(message) is list: 856 | # recursive call for a list of commands 857 | for message_i in message: 858 | self.write(message_i, encoding) 859 | return 860 | 861 | self.write_raw(str(message).encode(encoding)) 862 | 863 | def read(self, num=-1, encoding = 'ascii'): 864 | "Read string from instrument" 865 | return self.read_raw(num).decode(encoding).rstrip('\r\n') 866 | 867 | def ask(self, message, num=-1, encoding = 'ascii'): 868 | "Write then read string" 869 | if type(message) is tuple or type(message) is list: 870 | # recursive call for a list of commands 871 | val = list() 872 | for message_i in message: 873 | val.append(self.ask(message_i, num, encoding)) 874 | return val 875 | 876 | self.write(message, encoding) 877 | return self.read(num, encoding) 878 | 879 | def trigger(self): 880 | "Send trigger command" 881 | if self.link is None: 882 | self.open() 883 | 884 | flags = 0 885 | 886 | error = self.client.device_trigger( 887 | self.link, 888 | flags, 889 | self._lock_timeout_ms, 890 | self._timeout_ms 891 | ) 892 | 893 | if error: 894 | raise Vxi11Exception(error, 'trigger') 895 | 896 | def clear(self): 897 | "Send clear command" 898 | if self.link is None: 899 | self.open() 900 | 901 | flags = 0 902 | 903 | error = self.client.device_clear( 904 | self.link, 905 | flags, 906 | self._lock_timeout_ms, 907 | self._timeout_ms 908 | ) 909 | 910 | if error: 911 | raise Vxi11Exception(error, 'clear') 912 | 913 | def lock(self, wait=False): 914 | "Send lock command" 915 | if self.link is None: 916 | self.open() 917 | 918 | flags = 0 919 | if wait == True: 920 | flags |= OP_FLAG_WAIT_BLOCK 921 | 922 | error = self.client.device_lock( 923 | self.link, 924 | flags, 925 | self._lock_timeout_ms 926 | ) 927 | 928 | if error: 929 | raise Vxi11Exception(error, 'lock') 930 | 931 | self.locked = True 932 | 933 | def unlock(self): 934 | "Send unlock command" 935 | if self.link is None: 936 | self.open() 937 | 938 | error = self.client.device_unlock(self.link) 939 | 940 | if error: 941 | raise Vxi11Exception(error, 'unlock') 942 | 943 | self.locked = False 944 | 945 | def enable_srq_handler(self): 946 | " enable srq handling for this device" 947 | 948 | logger.info("setting up srq handler") 949 | if self.link is None: 950 | raise Vxi11Exception(ERR_DEVICE_NOT_ACCESSIBLE,"link not open") 951 | 952 | serv=IntrServer.getServer() 953 | 954 | # this is tricky, we need the device to connect 955 | # to the ip address of our network-interface that is communicating with the device 956 | 957 | # so just ask the open socket to the device for our own ip address. 958 | intr_host, _ = self.client.sock.getsockname() 959 | # and use the port from our intrserver instance 960 | _, intr_port = serv.server_address 961 | logger.info("intr handler may connect to %s, %i"% (intr_host,intr_port)) 962 | 963 | # tell the device to enable interrupt services 964 | error=self.client.create_intr_chan(int(ipaddress.IPv4Address(intr_host)), 965 | intr_port, DEVICE_INTR_PROG,DEVICE_INTR_VERS,DEVICE_TCP) 966 | if error: 967 | raise Vxi11Exception(error, 'device can not create interrupt channel') 968 | 969 | # enable SRQ on the device 970 | handle=struct.pack("!L",self.client_id) 971 | IntrServer.register_dev(handle,self) 972 | error=self.client.device_enable_srq(self.link,True,handle) 973 | if error: 974 | raise Vxi11Exception(error, 'device can not enable SRQ handling') 975 | 976 | def disable_srq_handler(self): 977 | # disable srq handling and remove old handler 978 | handle=struct.pack("!L",self.client_id) 979 | if IntrServer.has_dev(handle): 980 | self.client.device_enable_srq(self.link,False,handle) 981 | IntrServer.unregister_dev(handle) 982 | self.client.destroy_intr_chan() 983 | 984 | def on_srq(self,callback): 985 | # start the intr channel for srq, the instrument should connect, 986 | # and if the handler runs it sould trigger our callback somehow 987 | if callback is None: 988 | self.disable_srq_handler() 989 | self.srq_callback=None 990 | else: 991 | self.srq_callback=callback 992 | #install callback before activating handler, as srq may already be pending! 993 | self.enable_srq_handler() 994 | 995 | class InterfaceDevice(Device): 996 | "VXI-11 IEEE 488.1 interface device interface client" 997 | def __init__(self, host, name = None, client_id = None, term_char = None): 998 | "Create new VXI-11 488.1 interface device object" 999 | 1000 | if host.upper().startswith('TCPIP') and '::' in host: 1001 | res = parse_visa_resource_string(host) 1002 | 1003 | if res is None: 1004 | raise Vxi11Exception('Invalid resource string', 'init') 1005 | 1006 | host = res['arg1'] 1007 | name = res['arg2'] 1008 | 1009 | if name is None: 1010 | name = "gpib0" 1011 | 1012 | super(InterfaceDevice, self).__init__(host, name, client_id, term_char) 1013 | 1014 | self._bus_address = 0 1015 | 1016 | def open(self): 1017 | "Open connection to VXI-11 device" 1018 | if self.link is not None: 1019 | return 1020 | 1021 | if ',' in self.name: 1022 | raise Vxi11Exception("Cannot specify address for InterfaceDevice") 1023 | 1024 | super(InterfaceDevice, self).open() 1025 | 1026 | self._bus_address = self.get_bus_address() 1027 | 1028 | def send_command(self, data): 1029 | "Send command" 1030 | if self.link is None: 1031 | self.open() 1032 | 1033 | flags = 0 1034 | 1035 | error, data_out = self.client.device_docmd( 1036 | self.link, 1037 | flags, 1038 | self._timeout_ms, 1039 | self._lock_timeout_ms, 1040 | CMD_SEND_COMMAND, 1041 | True, 1042 | 1, 1043 | data 1044 | ) 1045 | 1046 | if error: 1047 | raise Vxi11Exception(error, 'send_command') 1048 | 1049 | return data_out 1050 | 1051 | def create_setup(self, address_list): 1052 | data = bytearray([self._bus_address | GPIB_CMD_TAD, GPIB_CMD_UNL]) 1053 | 1054 | if type(address_list) is int: 1055 | address_list = [address_list] 1056 | 1057 | for addr in address_list: 1058 | if type(addr) is tuple: 1059 | if addr[0] < 0 or addr[0] > 30: 1060 | raise Vxi11Exception("Invalid address", 'create_setup') 1061 | data.append(addr[0] | GPIB_CMD_LAD) 1062 | if len(addr) > 1: 1063 | if addr[1] < 0 or addr[1] > 30: 1064 | raise Vxi11Exception("Invalid address", 'create_setup') 1065 | data.append(addr[1] | GPIB_CMD_SAD) 1066 | else: 1067 | if addr < 0 or addr > 30: 1068 | raise Vxi11Exception("Invalid address", 'create_setup') 1069 | data.append(addr | GPIB_CMD_LAD) 1070 | 1071 | return bytes(data) 1072 | 1073 | def send_setup(self, address_list): 1074 | "Send setup" 1075 | return self.send_command(self.create_setup(address_list)) 1076 | 1077 | def _bus_status(self, val): 1078 | "Bus status" 1079 | if self.link is None: 1080 | self.open() 1081 | 1082 | flags = 0 1083 | 1084 | error, data_out = self.client.device_docmd( 1085 | self.link, 1086 | flags, 1087 | self._timeout_ms, 1088 | self._lock_timeout_ms, 1089 | CMD_BUS_STATUS, 1090 | True, 1091 | 2, 1092 | struct.pack('!H', val) 1093 | ) 1094 | 1095 | if error: 1096 | raise Vxi11Exception(error, 'bus_status') 1097 | 1098 | return struct.unpack('!H', data_out)[0] 1099 | 1100 | def test_ren(self): 1101 | "Read REN line" 1102 | return self._bus_status(CMD_BUS_STATUS_REMOTE) 1103 | 1104 | def test_srq(self): 1105 | "Read SRQ line" 1106 | return self._bus_status(CMD_BUS_STATUS_SRQ) 1107 | 1108 | def test_ndac(self): 1109 | "Read NDAC line" 1110 | return self._bus_status(CMD_BUS_STATUS_NDAC) 1111 | 1112 | def is_system_controller(self): 1113 | "Check if interface device is a system controller" 1114 | return self._bus_status(CMD_BUS_STATUS_SYSTEM_CONTROLLER) 1115 | 1116 | def is_controller_in_charge(self): 1117 | "Check if interface device is the controller-in-charge" 1118 | return self._bus_status(CMD_BUS_STATUS_CONTROLLER_IN_CHARGE) 1119 | 1120 | def is_talker(self): 1121 | "Check if interface device is addressed as a talker" 1122 | return self._bus_status(CMD_BUS_STATUS_TALKER) 1123 | 1124 | def is_listener(self): 1125 | "Check if interface device is addressed as a listener" 1126 | return self._bus_status(CMD_BUS_STATUS_LISTENER) 1127 | 1128 | def get_bus_address(self): 1129 | "Get interface device bus address" 1130 | return self._bus_status(CMD_BUS_STATUS_BUS_ADDRESS) 1131 | 1132 | def set_atn(self, val): 1133 | "Set ATN line" 1134 | if self.link is None: 1135 | self.open() 1136 | 1137 | flags = 0 1138 | 1139 | error, data_out = self.client.device_docmd( 1140 | self.link, 1141 | flags, 1142 | self._timeout_ms, 1143 | self._lock_timeout_ms, 1144 | CMD_ATN_CTRL, 1145 | True, 1146 | 2, 1147 | struct.pack('!H', val) 1148 | ) 1149 | 1150 | if error: 1151 | raise Vxi11Exception(error, 'set_atn') 1152 | 1153 | return struct.unpack('!H', data_out)[0] 1154 | 1155 | def set_ren(self, val): 1156 | "Set REN line" 1157 | if self.link is None: 1158 | self.open() 1159 | 1160 | flags = 0 1161 | 1162 | error, data_out = self.client.device_docmd( 1163 | self.link, 1164 | flags, 1165 | self._timeout_ms, 1166 | self._lock_timeout_ms, 1167 | CMD_REN_CTRL, 1168 | True, 1169 | 2, 1170 | struct.pack('!H', val) 1171 | ) 1172 | 1173 | if error: 1174 | raise Vxi11Exception(error, 'set_ren') 1175 | 1176 | return struct.unpack('!H', data_out)[0] 1177 | 1178 | def pass_control(self, addr): 1179 | "Pass control to another controller" 1180 | 1181 | if addr < 0 or addr > 30: 1182 | raise Vxi11Exception("Invalid address", 'pass_control') 1183 | 1184 | if self.link is None: 1185 | self.open() 1186 | 1187 | flags = 0 1188 | 1189 | error, data_out = self.client.device_docmd( 1190 | self.link, 1191 | flags, 1192 | self._timeout_ms, 1193 | self._lock_timeout_ms, 1194 | CMD_PASS_CTRL, 1195 | True, 1196 | 4, 1197 | struct.pack('!L', addr) 1198 | ) 1199 | 1200 | if error: 1201 | raise Vxi11Exception(error, 'pass_control') 1202 | 1203 | return struct.unpack('!L', data_out)[0] 1204 | 1205 | def set_bus_address(self, addr): 1206 | "Set interface device bus address" 1207 | 1208 | if addr < 0 or addr > 30: 1209 | raise Vxi11Exception("Invalid address", 'set_bus_address') 1210 | 1211 | if self.link is None: 1212 | self.open() 1213 | 1214 | flags = 0 1215 | 1216 | error, data_out = self.client.device_docmd( 1217 | self.link, 1218 | flags, 1219 | self._timeout_ms, 1220 | self._lock_timeout_ms, 1221 | CMD_BUS_ADDRESS, 1222 | True, 1223 | 4, 1224 | struct.pack('!L', addr) 1225 | ) 1226 | 1227 | if error: 1228 | raise Vxi11Exception(error, 'set_bus_address') 1229 | 1230 | self._bus_address = addr 1231 | 1232 | return struct.unpack('!L', data_out)[0] 1233 | 1234 | def send_ifc(self): 1235 | "Send IFC" 1236 | if self.link is None: 1237 | self.open() 1238 | 1239 | flags = 0 1240 | 1241 | error, data_out = self.client.device_docmd( 1242 | self.link, 1243 | flags, 1244 | self._timeout_ms, 1245 | self._lock_timeout_ms, 1246 | CMD_IFC_CTRL, 1247 | True, 1248 | 1, 1249 | b'' 1250 | ) 1251 | 1252 | if error: 1253 | raise Vxi11Exception(error, 'send_ifc') 1254 | 1255 | def find_listeners(self, address_list=None): 1256 | "Find devices" 1257 | if self.link is None: 1258 | self.open() 1259 | 1260 | if address_list is None: 1261 | address_list = list(range(31)) 1262 | address_list.remove(self._bus_address) 1263 | 1264 | found = [] 1265 | 1266 | try: 1267 | self.lock() 1268 | for addr in address_list: 1269 | # check for listener at primary address 1270 | cmd = bytearray([GPIB_CMD_UNL, GPIB_CMD_UNT]) 1271 | cmd.append(self._bus_address | GPIB_CMD_TAD) # spec says this is unnecessary, but doesn't appear to work without this 1272 | if type(addr) is tuple: 1273 | addr = addr[0] 1274 | if addr < 0 or addr > 30: 1275 | raise Vxi11Exception("Invalid address", 'find_listeners') 1276 | cmd.append(addr | GPIB_CMD_LAD) 1277 | self.send_command(cmd) 1278 | self.set_atn(False) 1279 | time.sleep(0.0015) # probably not necessary due to network delays 1280 | if self.test_ndac(): 1281 | found.append(addr) 1282 | else: 1283 | # check for listener at any sub-address 1284 | cmd = bytearray([GPIB_CMD_UNL, GPIB_CMD_UNT]) 1285 | cmd.append(self._bus_address | GPIB_CMD_TAD) # spec says this is unnecessary, but doesn't appear to work without this 1286 | cmd.append(addr | GPIB_CMD_LAD) 1287 | for sa in range(31): 1288 | cmd.append(sa | GPIB_CMD_SAD) 1289 | self.send_command(cmd) 1290 | self.set_atn(False) 1291 | time.sleep(0.0015) # probably not necessary due to network delays 1292 | if self.test_ndac(): 1293 | # find specific sub-address 1294 | for sa in range(31): 1295 | cmd = bytearray([GPIB_CMD_UNL, GPIB_CMD_UNT]) 1296 | cmd.append(self._bus_address | GPIB_CMD_TAD) # spec says this is unnecessary, but doesn't appear to work without this 1297 | cmd.append(addr | GPIB_CMD_LAD) 1298 | cmd.append(sa | GPIB_CMD_SAD) 1299 | self.send_command(cmd) 1300 | self.set_atn(False) 1301 | time.sleep(0.0015) # probably not necessary due to network delays 1302 | if self.test_ndac(): 1303 | found.append((addr, sa)) 1304 | self.unlock() 1305 | except: 1306 | self.unlock() 1307 | raise 1308 | 1309 | return found 1310 | 1311 | 1312 | class Instrument(Device): 1313 | "VXI-11 instrument interface client" 1314 | 1315 | def read_stb(self): 1316 | "Read status byte" 1317 | if self.link is None: 1318 | self.open() 1319 | 1320 | flags = 0 1321 | 1322 | error, stb = self.client.device_read_stb( 1323 | self.link, 1324 | flags, 1325 | self._lock_timeout_ms, 1326 | self._timeout_ms 1327 | ) 1328 | 1329 | if error: 1330 | raise Vxi11Exception(error, 'read_stb') 1331 | 1332 | return stb 1333 | 1334 | def remote(self): 1335 | "Send remote command" 1336 | if self.link is None: 1337 | self.open() 1338 | 1339 | flags = 0 1340 | 1341 | error = self.client.device_remote( 1342 | self.link, 1343 | flags, 1344 | self._lock_timeout_ms, 1345 | self._timeout_ms 1346 | ) 1347 | 1348 | if error: 1349 | raise Vxi11Exception(error, 'remote') 1350 | 1351 | def local(self): 1352 | "Send local command" 1353 | if self.link is None: 1354 | self.open() 1355 | 1356 | flags = 0 1357 | 1358 | error = self.client.device_local( 1359 | self.link, 1360 | flags, 1361 | self._lock_timeout_ms, 1362 | self._timeout_ms 1363 | ) 1364 | 1365 | if error: 1366 | raise Vxi11Exception(error, 'local') 1367 | 1368 | --------------------------------------------------------------------------------