In Retrofitting your Old Power Supply to be Smartphone Controllable we breathed in new life into an old power supply by giving it IoT capabilities. In this article we’re going to look at a more streamlined way to bring IoT capabilities to all of your lab instruments (specifically ones controlled using VISA). Rather than build it from scratch we’re going to leverage Generative AI to do most of the heavy lifting. After walking through this tutorial you should be able to take these concepts to build web enabled drivers for all of your lab instruments and speed up your overall development as well.
In this article we’re going to focus on putting together the web service that serves as an intermediary between the instrument itself and the Internet. The following is an illustration of the end-to-end connection between the instrument and the internet (i.e. access via website).
Figure 1: End-to-end communication between the instrument and the Internet
Before we do that it's important to establish that it's not completely necessary to write all the instrument drivers ourselves. We can leverage what's already done online from the manufacturers or we can leverage open source repositories. For the sake of brevity I am going to leverage what I already found online but use generative AI to put together a framework that I am happy with.
I am using the DP832 power supply and DL3021 electronic load from Rigol. After a quick search on GitHub, I found Python libraries for the DP832 power supply that contains all the necessary SCPI commands to get started. An alternative approach would be to take a list of SCPI commands from the DP832 manual, hand it to a large language model (LLM), for example ChatGPT or Gemini, and have it generate the functions for me. I’m a bit picky so I’m going to define my own PyVISA setup process and then leverage the majority of what’s already been done. Here is my generic PyVISA setup process (encapsulated in a separate class):
class CommBase:
USBConnType = Literal["USBTMC", "VISA", "Socket"]
def __init__(self, usb_conn_type: USBConnType, vid: int = None, pid: int = None, visa_resource_prefix: str = None):
self.visa_resource_prefix = visa_resource_prefix
self.usb_conn_type = usb_conn_type
if usb_conn_type == "USBTMC":
self.configure_usbtmc(vid, pid)
elif usb_conn_type == "VISA":
self.configure_visa(vid, pid)
elif usb_conn_type == "Socket":
pass
else:
raise ValueError(f"Invalid USB connection type: {usb_conn_type}. Valid types are {self.VALID_USB_CONN_TYPES}")
def configure_usbtmc(self, vid: int, pid: int):
self.inst = usbtmc.Instrument(vid, pid)
def configure_visa(self, vid: int, pid: int):
self.rm = pyvisa.ResourceManager()
instrument_list = self.rm.list_resources()
visa_address = self.find_visa_resource(vid, pid, instrument_list, prefix=self.visa_resource_prefix)
if visa_address is not None:
self.inst = self.rm.open_resource(visa_address, read_termination="\n")
else:
raise IOError(f'No VISA devices found using vid "{vid}" and pid "{pid}" but the following VISA devices have been found: {instrument_list}')
@staticmethod
def find_visa_resource(vid: int, pid: int, resource_strings: list, prefix: str) -> str:
hex_vid, hex_pid = f"0x{vid:X}", f"0x{pid:X}"
dec_vid, dec_pid = str(vid), str(pid)
for resource in resource_strings:
parts = resource.split("::")
if len(parts) >= 4:
serial_and_more = parts[3]
if (any(x in resource for x in (hex_vid, dec_vid)) and any(x in resource for x in (hex_pid, dec_pid)) and serial_and_more.startswith(prefix)):
return resource
return None
def query_device(self, command: str) -> str:
if self.usb_conn_type == "USBTMC":
return self.inst.ask(command)
elif self.usb_conn_type == "VISA":
return self.inst.query(command)
else:
raise NotImplementedError(f"Query method for {self.usb_conn_type} not found.")
def write_device(self, command: str):
self.inst.write(command)
def close(self):
self.inst.close()
if self.usb_conn_type == "VISA":
self.rm.close()
def id(self) -> dict:
id_str = self.query_device("*IDN?").strip().split(",")
return {
"manufacturer": id_str[0],
"model": id_str[1],
"serial_number": id_str[2],
"version": id_str[3],
}
I’ve removed all the comments, extra spacing, and even some setup sequences (such as logging) for brevity. As you can see I have some generic functions plus support for both PyVISA and USBTMC. Most SCPI based Python libraries that you find online won’t have this base functionality. That’s fine because I can extend this class into my new class:
from base.CommBase import CommBase
class DP(CommBase):
def channel_check(self, channel):
assert NotImplementedError
def get_output_mode(self, channel: int) -> str:
self.channel_check(channel)
return self.query_device(f":OUTP:MODE? CH{channel}").strip()
# … Code has been removed for brevity
def measure_current(self, channel):
self.channel_check(channel)
meas = self.query_device(f":MEAS:CURR? CH{channel}").strip()
return float(meas)
def measure_voltage(self, channel):
self.channel_check(channel)
meas = self.query_device(f":MEAS? CH{channel}").strip()
return float(meas)
def measure_all(self, channel):
self.channel_check(channel)
meas = self.query_device(f":MEAS:ALL? CH{channel}").strip().split(",")
return {
"voltage": float(meas[0]),
"current": float(meas[1]),
"power": float(meas[2]),
}
class DP712(DP):
def channel_check(self, channel):
assert channel in [1, ""], f"Output channel {channel} not supported"
class DP821(DP):
def channel_check(self, channel):
assert channel in [1, 2, ""], f"Output channel {channel} not supported"
class DP832(DP):
def channel_check(self, channel):
assert channel in [1, 2, 3, ""], f"Output channel {channel} not supported"
I’ve taken a few different examples online and passed them into ChatGPT. I’ve prompted it to:
Leveraging Generative AI for this task cut a multi-hour job into 60 seconds. I was also, very quickly, able to validate the drivers I found online by running the auto generated tests that ChatGPT wrote for me. For example, I discovered, very quickly, that one of the SCPI commands for the DL3021 to set the operation mode of the electronic load was actually incorrect. Watching the tests run in real-time I noticed that the mode wasn’t changing on my instrument. A quick lookup in the SCPI manual and I was able to correct it.
At this point we have a good foundation for our Python library to control our instruments. The goal now is to place a web service in front of the instrument library, giving us the ability to control them over the web. Assuming I know nothing about web frameworks, I can simply ask ChatGPT (or any LLM) to perform the whole function for me. Here’s the prompt that I used to get started:
I need to create a web service that will control my instrument using basic urls (via a web browser). I don't know much about web services or frameworks but I am familiar with Python. Write the full Python code to achieve this task.
Here is the class that controls my instrument:
<pre><code>```
{code from above}
```
</code></pre>
And the (partial response):
Figure 2: Response from ChatGPT prompt
My favorite part about the sophisticated (paid) LLMs is that they really break down the task for you, educate you, and provide quite a nice solution as a first iteration. In Gemini vs. ChatGPT: Who Writes Better Code? I stacked up the two LLMs against each other and (spoiler alert) discovered that the latest, most advanced, version of Google’s Gemini was actually quite good (if not on par with ChatGPT 4). Using either one of these LLMs will yield a similar result like the one shown above.
If I want to put this into a class or reformat it I can simply respond to the chat and make my request. Want your code commented? No problem, just ask!
Prompt: Add docstring style comment blocks to each function. Rewrite the whole class as a result so I can copy and paste it back into my code editor.
Figure 3: Next response from ChatGPT prompt
Testing
Now that we’ve had Generative AI create the Python module we can either ask it to write tests for us or show us how to test manually:
Figure 4: Manual test URL from ChatGPT
Figure 5: Automated test written by ChatGPT
After running (or tweaking then running) these tests we should be able to validate our new Python module that interfaces between the web and our instrument.
Conclusion
In this article we put together a Python module that controlled our instrument leveraging existing code on the web and Generative AI. After our LLM completed the module we then introduced a new module that acted as an intermediary between the Internet and the instrument itself (via a web service). We also took a quick look at how Generative AI can walk us through the whole process including manual or automated testing. The best part about this exercise is that, if done right, it should have taken you very little time to put together. The first time might require some extra effort but repeating this process for many instruments should be a breeze (and incur very little development time). It’s clear that Generative AI has completely changed the landscape for all of us and this article really demonstrates that. Hopefully you will be inspired to create the next set of instrument libraries leveraging Generative AI and contribute them back to the rest of the community.
All the source code used in this project can be found at: https://gitlab.com/ai-examples/instrument-controllables.