Last month, I described a simple stateless web application using Python and Plotly’s Dash module. It created a dashboard allowing users to select waveform channels, update rate, and the number of samples that comprise the x-axis. It used this information to display simulated analog data, which was used to explain how data is stored in an HTML element, and how new data is added to the display. This month, I used the same application framework. However, instead of generating the data, I used an MCC USB 200 series device to capture live data in real time, while maintaining the stateless nature of a browser application. This application has two parts: a lightweight socket server responsible for controlling the hardware device, and a web application that sends commands to the server and displays the data.
The Server:
The server understands five commands: List, Open, Start, Stop, and Exit. List returns a list of USB-200 devices plugged into the computer’s USB ports. The Open command creates a virtual device containing such information as type, name, serial number, etc. The Start command is responsible for programming the device created with Open, managing the data buffer, and uploading the data to a web application. The Stop command stops the acquisition, frees the memory, and releases the device. The Exit command closes the socket created initially.
One of the server’s first tasks after receiving Start is to create the device buffer. Using the sample rate and number of channels, the buffer is sized to be approximately 10 seconds, extra-large just in case. The buffer must also be a multiple of 32, its USB data packet size. The packet size is the number of data points in the device’s FIFO memory that triggers an automatic USB transfer to PC memory.
number_of_channels = len(config.Channels)
sample_rate = config.Rate
one_second = sample_rate * number_of_channels
buffer_size = (one_second - (one_second % 96)) * 10
memHandle = ul.scaled_win_buf_alloc(buffer_size)
Next, the input channels, which may or may not be in order, are sent to the driver. The channel order in ul.a_load_queue overrides the channels given to ul.a_in_scan. As its name suggests, ul.a_in_scan begins scanning the inputs, storing the data in the buffer. The scan options tell it to scan continuously in the background and to scale the A/D counts to voltage.
# set channel order
ul.a_load_queue(board_number, config.Channels, config.Ranges, number_of_channels)
# set the scan option for continuous background operation and scale the data to volts
opts = ScanOptions.SCALEDATA | ScanOptions.CONTINUOUS | ScanOptions.BACKGROUND
# start the acquisition
actual_rate = ul.a_in_scan(board_number, 0, 3, buffer_size, sample_rate, ULRange.BIP10VOLTS, memHandle, opts)
At this time, it’s important to point out that all communication consists of two parts: the length of data to be transmitted and the transmission. For instance, sending a Python list is first converted to a JSON string before the length is determined. The length is then packed into an unsigned integer (‘I’) and sent, followed by sending the encoded string as shown below:
json_string = json.dumps(itm)
packed_data = struct.pack('<' + 'I', len(json_string))
conn.sendall(packed_data)
conn.sendall(json_string.encode('utf-8'))
On the receiving end, the length of the string is read, and then the JSON-encoded string is received. The JSON string is restored using decode(‘utf-8’) and json.loads() converts the JSON string to a Python list.
recv_str = self.socket.recv(ctypes.sizeof(ctypes.c_uint))
unpacked_tuple = struct.unpack('<' + 'I', recv_str)
msg = list(unpacked_tuple)
bytes_to_recv = msg[0]
s = self.socket.recv(bytes_to_recv)
item = json.loads(s.decode('utf-8')
Below is the buffer management loop. First, ul.get_status is called to get the newest scan’s current index (head) location. Because of the large buffer size, most work is updating the position of the head as the data fills the buffer. However, when the buffer gets to the end and rolls over, we must collect the samples at the end and the beginning and add the two lists together. Only then can we determine the length and send the data.
while flag.is_set():
status, curr_count, head = ul.get_status(board_number, FunctionType.AIFUNCTION)
if head > tail:
if (head - tail) > int(sample_rate * number_of_channels / 2):
sending = head - tail
samples = samples + sending
send_msg(conn, sending)
logger.info(f'Transferred {sending:6d}\tTotal Transferred {samples:12d}')
python_list = data[tail: head]
packed_data = struct.pack('<' + 'd' * len(python_list), *python_list)
conn.sendall(packed_data)
tail = head
else:
send = buffer_size - tail + head
if send > int(sample_rate * number_of_channels / 2):
samples = samples + sending
send_msg(conn, sending)
logger.info(f'Transferred {sending:6d}\tTotal Transferred {samples:12d}')
python_list = data[tail:buffer_size] + data[0:head]
packed_data = struct.pack('<' + 'd' * len(python_list), *python_list)
conn.sendall(packed_data)
tail = head
The server prints the commands it receives and the acquisition configuration. It also logs the recent transferred count and the total it has transferred. Below is a sample output. The log shows the commands it received from the web application, the configuration information, and the number of samples being transferred. It uses int(sample_rate * number_of_channels / 2) to determine when to send data approximately every half second. The output below indicates it initially transferred 2108 samples. The minimum number of samples to transfer is 2000 because the acquisition consisted of four channels sampled at 1000 Hz.
2025-08-01 11:31:13,288 – serverInfo – INFO – Info message: serverInfo module loaded successfully.
2025-08-01 11:31:13,293 – serverInfo – INFO – Server listening on 127.0.0.1:65432…
2025-08-01 11:31:16,704 – serverInfo – INFO – Active connections: 0
2025-08-01 11:31:16,751 – serverInfo – INFO – Received from (‘127.0.0.1’, 46075): Command: list
2025-08-01 11:31:17,628 – serverInfo – INFO – Active connections: 1
2025-08-01 11:31:17,642 – serverInfo – INFO – Received from (‘127.0.0.1’, 46076): Command: list
2025-08-01 11:31:22,317 – serverInfo – INFO – Received from (‘127.0.0.1’, 46076): Command: open
2025-08-01 11:31:22,317 – serverInfo – INFO – Board number: 0
2025-08-01 11:31:22,317 – serverInfo – INFO – 1FFC8D1
2025-08-01 11:31:23,107 – serverInfo – INFO – Received from (‘127.0.0.1’, 46076): Command: start
2025-08-01 11:31:23,107 – serverInfo – INFO – Input Channels: [0, 1, 2, 3]
2025-08-01 11:31:23,107 – serverInfo – INFO – Input Ranges: [1, 1, 1, 1]
2025-08-01 11:31:23,107 – serverInfo – INFO – Sample Rate: 1000
2025-08-01 11:31:23,107 – serverInfo – INFO – Samples to Display: 1000
2025-08-01 11:31:23,107 – serverInfo – INFO – Board number: 0
2025-08-01 11:31:23,107 – serverInfo – INFO – Board descriptor: USB-201
2025-08-01 11:31:23,647 – serverInfo – INFO – Transferred 2108 Total Transferred 2108
2025-08-01 11:31:24,251 – serverInfo – INFO – Transferred 2368 Total Transferred 4476
2025-08-01 11:31:24,755 – serverInfo – INFO – Transferred 2048 Total Transferred 6524
2025-08-01 11:31:25,260 – serverInfo – INFO – Transferred 2048 Total Transferred 8572
2025-08-01 11:31:25,872 – serverInfo – INFO – Transferred 2432 Total Transferred 11004
2025-08-01 11:31:26,478 – serverInfo – INFO – Transferred 2432 Total Transferred 13436
2025-08-01 11:31:27,083 – serverInfo – INFO – Transferred 2368 Total Transferred 15804
2025-08-01 11:31:27,588 – serverInfo – INFO – Transferred 2048 Total Transferred 17852
2025-08-01 11:31:28,093 – serverInfo – INFO – Transferred 2048 Total Transferred 19900
2025-08-01 11:31:28,606 – serverInfo – INFO – Transferred 2048 Total Transferred 21948
2025-08-01 11:31:29,118 – serverInfo – INFO – Transferred 2048 Total Transferred 23996
2025-08-01 11:31:29,722 – serverInfo – INFO – Transferred 2368 Total Transferred 26364
2025-08-01 11:31:30,231 – serverInfo – INFO – Transferred 2048 Total Transferred 28412
2025-08-01 11:31:30,736 – serverInfo – INFO – Transferred 2048 Total Transferred 30460
2025-08-01 11:31:31,343 – serverInfo – INFO – Transferred 2432 Total Transferred 32892
2025-08-01 11:31:31,857 – serverInfo – INFO – Transferred 2048 Total Transferred 34940
2025-08-01 11:31:32,462 – serverInfo – INFO – Transferred 2432 Total Transferred 37372
2025-08-01 11:31:32,973 – serverInfo – INFO – Transferred 2048 Total Transferred 39420
2025-08-01 11:31:33,584 – serverInfo – INFO – Transferred 2432 Total Transferred 41852
2025-08-01 11:31:34,097 – serverInfo – INFO – Transferred 2048 Total Transferred 43900
2025-08-01 11:31:34,695 – serverInfo – INFO – Received from (‘127.0.0.1’, 46076): Command: stop
2025-08-01 11:31:34,720 – serverInfo – INFO – Board 0 closed.
2025-08-01 11:31:45,608 – serverInfo – INFO – Received from (‘127.0.0.1’, 46076): Command: exit
2025-08-01 11:31:45,608 – serverInfo – INFO – Connection with (‘127.0.0.1’, 46076) closed.
2025-08-01 11:31:45,698 – serverInfo – INFO – Received from (‘127.0.0.1’, 46075): Command: exit
2025-08-01 11:31:45,698 – serverInfo – INFO – Connection with (‘127.0.0.1’, 46075) closed.
The Web Application
The List command is sent to the server when your browser loads the web application’s HTML page. The information contained in the list populates the Select Device dcc dropdown box. If more than one USB-200 series device is plugged into the computer, the dropdown can be used to select the desired device. Up to eight channels can be used simultaneously using the Active Channels checkboxes. Valid numbers for sample rates are 100-12,500 Hz. This is so that when all eight channels are selected, the aggregate rate would not exceed 100 KHz, the maximum rate of a USB-201. The aggregate rate is checked in the start_stop_click callback for the ‘Configure’ button and can be easily changed if you’re using the faster USB-205. The Samples to Display and Sample Rate numbers determine the x-axis timespan. Valid numbers for samples displayed are 100-10,000. Adjust the sample rate and samples to display numbers to adjust the x-axis time.
Before anything else, the web application must know the server’s IP address and Port number. If running locally on a computer, use ‘127.0.0.1’ for the IP address and ensure the server and web application use different port numbers. The web application establishes the connection with the server in the create_board_selector function, which is also responsible for sending the List command to the server. For each device in the list, it creates a tuple, converts it to a dictionary, and then to a JSON string. After it has processed all the devices, it returns a list containing JSON strings to the dcc dropdown element in an HTML web page.
def create_board_selector():
board_selection_options = []
if daq_socket_manager.connect(HOST, PORT) is True:
dev_list = daq_socket_manager.get_device_list()
if len(dev_list) > 0:
for device in dev_list:
board = namedtuple('board', ['board_num',
'serial_num',
'type',
'product_name'])
bd = board(board_num=device['Board_Number'],
serial_num=device['Serial_Number'],
type=device['Product_ID'],
product_name=device['Name'])
label = '{0}: {1}'.format(bd.board_num, bd.product_name)
option = {'label': label, 'value': json.dumps(bd._asdict())}
board_selection_options.append(option)
selection = None
if board_selection_options:
selection = board_selection_options[0]['value']
return dcc.Dropdown(
id='daqSelector', options=board_selection_options,
value=selection, clearable=False, style={'width': 200,
'height': 40,
'border-radius': '10px',
'font-size': '16px'})
else:
label = '{0}: {1}'.format(-1, 'no devices')
option = {'label': label, 'value': 0}
board_selection_options.append(option)
return dcc.Dropdown(
id='daqSelector', options=board_selection_options,
clearable=False)
Pressing the ‘Configure’ button sends the Open command and the board number from the item shown in the Device Selector dcc dropdown. The server uses the board number to create a virtual device. If you want to make changes, press ‘Cancel’. Pressing ‘Start’ sends the Start command to the server to acquire data. Pressing ‘Cancel’ sends the Stop command, which performs the cleanup duties. The Exit command is sent to the server when the Dash server is closed with Ctrl-C or Ctrl-Break. The Exit command makes the server close the socket, but not the server as a whole. Use Ctrl-Break to kill the server completely.
When displaying live data, the web application performs two simultaneous actions. A thread function is started to retrieve data from the server, and an interval timer is started, which initiates the callback to update the chart with new data. The timer runs slightly faster than the incoming data, so it doesn’t fall behind when plotting it. Below is the thread loop responsible for getting the data. It can wait up to two seconds for data to arrive, but it should never have to. When data is sent, it reads the number of samples to expect. It then calculates the number of bytes and reads them until the count is satisfied.
size_of_double = ctypes.sizeof(ctypes.c_double)
while self.stop_event.is_set():
# get number of samples to receive
samples_to_read = self.get_msg('int')
chunk_size = size_of_double * samples_to_read
# read the samples
recv_str = b""
while len(recv_str) < chunk_size:
diff = min(chunk_size - len(recv_str), chunk_size)
chunk = self.socket.recv(diff)
recv_str += chunk
l = (len(recv_str) // struct.calcsize('d'))
unpacked_tuple = struct.unpack('<' + 'd' * l, recv_str)
self.data_list = list(unpacked_tuple)
self.new_data = True
I considered enhancing the web application further by adding a file-saving option, but time ran out. You could use a dcc download element to open a file dialog, allowing the user to specify a file or file name. Add a dcc input box to limit the number of scans saved to the file. I also considered changing the Smples to Display input box to a knob control to adjust the x-axis time. The dash_daq module used for the LED displays also has a knob control for something like this. Perhaps, at a future date, I’ll do this and post an update.
The code for this discussion can be downloaded at: Dash-Data-Acquisition-Main.zip
Before running the server and web application Python code, ensure you have the following.
- Install MCC’s InstaCal utility: InstaCal
- Install MCC’s Windows Python support: MCC UL Python
- Install Plotly’s Dash and Dash_Daq: pip install dash, and pip install dash_daq.
The server module uses a custom module to locate MCC USB devices. Ensure getBoard.py is in the server.py folder. The browser uses the daqSocketManager.py custom module to handle the server’s communication complexities. Ensure it is in the same folder as web-app.py. There is also an Assets folder for CSS styling that must be included in the web-app.py folder. Start two command prompts from the folder containing all the files. If required, activate your virtual environment. Start the server by entering python server.py on the command line. In the other terminal, enter python web-app.py. When it runs, it displays an IP address to enter into your favorite browser’s address bar to start the application.
Disclaimer: The code discussed here is provided as-is. It has not been thoroughly tested or validated as a product for use in a deployed application, system, or hazardous environment. You assume all risks when using the example code.