From 826efa96c97a20da9703622cfdd8011ab1cd8fc9 Mon Sep 17 00:00:00 2001 From: gboedecker <125995107+gboedecker@users.noreply.github.com> Date: Mon, 25 Aug 2025 14:39:11 +0200 Subject: [PATCH 1/2] New ERM200 PyVisa example --- .../ERM200_PyVisa.py | 60 +++++++++++++++++++ .../README.md | 4 ++ 2 files changed, 64 insertions(+) create mode 100644 Python/Thorlabs ERM2xx Extinction Ratio Meters/ERM200_PyVisa.py diff --git a/Python/Thorlabs ERM2xx Extinction Ratio Meters/ERM200_PyVisa.py b/Python/Thorlabs ERM2xx Extinction Ratio Meters/ERM200_PyVisa.py new file mode 100644 index 0000000..f1ebf34 --- /dev/null +++ b/Python/Thorlabs ERM2xx Extinction Ratio Meters/ERM200_PyVisa.py @@ -0,0 +1,60 @@ +""" +ERM200_SCPI +Example Date of Creation: 2025-08-25 +Example Date of Last Modification on Github: 2025-08-25 +Version of Python: 3.13 +Version of the Thorlabs SDK used: - +Tested with ERM210 +================== +Example Description: The example shows how to use SCPI commands in Python with pyvisa +""" + +import pyvisa +import time + +def main(): + rm = None + device = None + try: + #Opens a resource manager + rm = pyvisa.ResourceManager() + + + #Opens the connection to the device. The variable instr is the handle for the device. + # !!! In the USB number the serial number (M01...) and PID (0x8032) needs to be changed to the one of the connected device. + #Check with the Windows DEvice Manager + instr = rm.open_resource('USB0::0x1313::0x8032::M01099532::INSTR') + + + #print the device information + print(instr.query("*IDN?")) + + result = instr.query("MEAS?") + er, phi = result.split(",") + print("Measured ER: ", er) + print("Measured Phi: ", phi) + + finally: + #Close device in any case + if device is not None: + try: + device.close() + except Exception: + pass + + #Close resource manager in any case + if rm is not None: + try: + instr.close() + except Exception: + pass + + #close out session + rm.close() + + + import sys + print(sys.version) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Python/Thorlabs ERM2xx Extinction Ratio Meters/README.md b/Python/Thorlabs ERM2xx Extinction Ratio Meters/README.md index 9ed0f5e..32333a7 100644 --- a/Python/Thorlabs ERM2xx Extinction Ratio Meters/README.md +++ b/Python/Thorlabs ERM2xx Extinction Ratio Meters/README.md @@ -8,3 +8,7 @@ Please note that the code connects to the first available ERM2xx device. If you lib.TLERM200_getRsrcName(erm_handle, 0, resource) +### ERM200_PyVisa.py +This sample code shows how you can control a Thorlabs ERM200, ERM210 or ERM220 extinction ratio meter in Python with SCPI commands and PyVisa. +Use Visa drivers to run the example. + From 9b151c598572c28d93439432549e34741bad4964 Mon Sep 17 00:00:00 2001 From: gboedecker <125995107+gboedecker@users.noreply.github.com> Date: Thu, 28 Aug 2025 09:53:28 +0200 Subject: [PATCH 2/2] raw socket communication example --- .../SCPI/ethernetRawSockets/Readme.md | 22 ++ .../SCPI/ethernetRawSockets/pmNetFormat.py | 194 ++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 Python/Thorlabs PMxxx Power Meters/SCPI/ethernetRawSockets/Readme.md create mode 100644 Python/Thorlabs PMxxx Power Meters/SCPI/ethernetRawSockets/pmNetFormat.py diff --git a/Python/Thorlabs PMxxx Power Meters/SCPI/ethernetRawSockets/Readme.md b/Python/Thorlabs PMxxx Power Meters/SCPI/ethernetRawSockets/Readme.md new file mode 100644 index 0000000..28eeeb6 --- /dev/null +++ b/Python/Thorlabs PMxxx Power Meters/SCPI/ethernetRawSockets/Readme.md @@ -0,0 +1,22 @@ +# Ethernet Raw Socket Communication +This python command line sample demonstrates how to communicate with a Thorlabs Ethernet capable power meter using +raw python socket communication without any additional library like anyvisa. + +# Details +The powermeter uses a binary protocol to frame large request/response messages to multiple TCP packets. The given codes +uses synchronous IO to implement this binary protocol for data exchange. The example does not contain any network device +discovery. You need to know the IP and port number of the power meter to connect. Once the connection is established you +can use the methods to send and receive text and binary request and response data. The implementation is totally platform +independent. + +For more technical background information about fast mode refer to SCPI command description. You can find a description for +every Meter in the [commandDocu](../commandDocu) folder. For example the PM103E [SCPI command description](https://htmlpreview.github.io/?https://github.com/Selanarixx/Light_Analysis_Examples/blob/develop/Python/Thorlabs%20PMxxx%20Power%20Meters/scpi/commandDocu/pm103E.html) html file. + +## Limitations +Please be aware the power meter will close the connection automatically if no communication is ongoing for 30 seconds. +If you send an ernous (e.g. an unkown SCPI command) request, the receive function will issue a timeout error as the +device does not send any response data. + +## Supported Meters +- PM103E +- PM5020 diff --git a/Python/Thorlabs PMxxx Power Meters/SCPI/ethernetRawSockets/pmNetFormat.py b/Python/Thorlabs PMxxx Power Meters/SCPI/ethernetRawSockets/pmNetFormat.py new file mode 100644 index 0000000..b180b6f --- /dev/null +++ b/Python/Thorlabs PMxxx Power Meters/SCPI/ethernetRawSockets/pmNetFormat.py @@ -0,0 +1,194 @@ +""" +Example Thorlabs Power Meter raw ethernet socket communication +Example Date of Creation 2024-08-07 +Example Date of Last Modification on Github 2024-08-07 +Version of Python 3.11.2 +Version of the Thorlabs SDK used none +================== +This examples shows how to communicate with a Thorlabs ethernet capable +Power Meter using raw python socket communication. The sample implements +the binary framing protocol to transfer SCPI request and response data +as payload. +""" +import struct +import socket + +#Maximal size of a TCP segment of the Thorlabs power meter +TCP_MAX_SEG_SIZE = 1440 +#Size of the header in bytes of a TCP segment of the Thorlabs power meter +TL_TCP_SEG_HEADER_SIZE = 4 +#Maximal amount of payload bytes for a single frame +MAX_FRAME_LEN = TCP_MAX_SEG_SIZE - TL_TCP_SEG_HEADER_SIZE + +def TL_TCP_frame_transmit_bin(socket: socket, request: bytearray): + """ + Transmits a binary request to the Thorlabs power meter. + The request is split into multiple frames if it is too large. + + :param socket: The openend socket to use for transmission + :param request: The binary request to transmit to power meter + """ + reqLen = len(request) + writtenBytes = 0 + frameCnt = 0 + while writtenBytes < reqLen: + #Limit max lenth of request + frameLen = reqLen - writtenBytes + if frameLen > MAX_FRAME_LEN: + frameLen = MAX_FRAME_LEN + + binReq = [] + if frameCnt == 0: + binReq = struct.pack(' MAX_FRAME_LEN: + respBinSegLen = MAX_FRAME_LEN + respHeadSeq = 1 + resp = bytearray(respBinTotalLen) + respIdx = 0 + + #Finally reset header parsing memory + respHeaderLen = 0 + else: + #Wait for start of conintous header? + if respBinSegLen == 0: + #Wait for start of continous frame + if d != 0xCB and respHeaderLen == 0: + continue + respHeader[respHeaderLen] = d + respHeaderLen += 1 + + #Parsed entire header? + if respHeaderLen == TL_TCP_SEG_HEADER_SIZE: + #Verify header sequence number + if respHeader[1] != respHeadSeq: + respBinSegLen = respBinTotalLen = 0 + else: + respBinSegLen = struct.unpack(' respBinTotalLen: + respBinSegLen = respBinTotalLen = 0 + + #Empty header? We are done -> go back to init + if respBinSegLen == 0: + respBinSegLen = respBinTotalLen = 0 + + #Finally reset header parsing memory + respHeaderLen = 0 + + else: + #Append data to response + resp[respIdx] = d + respIdx += 1 + respBinSegLen -= 1 + respBinTotalLen -= 1 + + if respBinTotalLen == 0: + return resp + +def TL_TCP_frame_receive(socket: socket): + """ + Receives a text response from the Thorlabs power meter. + The response is assembled from multiple frames. + For closer details read TL_TCP_frame_receive_bin() + comments. + + :param socket: The openend socket to use for reception + """ + binResp = TL_TCP_frame_receive_bin(socket) + return binResp.decode('ASCII') + +def TL_TCP_frame_transmit(socket: socket, req:str): + """ + Transmits a text request to the Thorlabs power meter.. + The request is split into multiple frames if it is too large. + + :param socket: The openend socket to use for transmission + :param req: The text request to transmit to power meter + """ + return TL_TCP_frame_transmit_bin(socket, req.encode('ASCII')) + +def TL_TCP_query(socket: socket, req:str): + """ + Sends a text request to the Thorlabs power meter and returns the text response. + + :param socket: The openend socket to use for transmission + :param req: The text request to transmit to power meter + """ + TL_TCP_frame_transmit(socket, req) + return TL_TCP_frame_receive(socket).strip() + +def TL_TCP_query_bin(socket: socket, req:str): + """ + Sends a text request to the Thorlabs power meter and returns the binary response. + + :param socket: The openend socket to use for transmission + :param req: The text request to transmit to power meter + """ + TL_TCP_frame_transmit(socket, req) + return TL_TCP_frame_receive_bin(socket) + +if __name__ == '__main__': + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + #Set connection timeout + s.settimeout(2) + #Connect to PM with given IP and port number. Default port is 2000. + s.connect(("10.10.4.22", 2000)) + + #Set read timeout + s.settimeout(1) + print(TL_TCP_query(s,"*IDN?\n")) + print(TL_TCP_query(s,"SYST:ERR?\n")) \ No newline at end of file