{"id":31790,"date":"2025-08-11T10:11:21","date_gmt":"2025-08-11T17:11:21","guid":{"rendered":"https:\/\/digilent.com\/blog\/?p=31790"},"modified":"2025-08-11T10:11:21","modified_gmt":"2025-08-11T17:11:21","slug":"make-your-internet-browser-capture-live-data","status":"publish","type":"post","link":"https:\/\/digilent.com\/blog\/make-your-internet-browser-capture-live-data\/","title":{"rendered":"Make Your Internet Browser Capture Live Data"},"content":{"rendered":"<p>Last month, I described a simple stateless web application using Python and Plotly&#8217;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.<\/p>\n<h4>The Server:<\/h4>\n<p>The server understands five commands: <strong>List, Open, Start, Stop<\/strong>, and <strong>Exit<\/strong>. <strong>List<\/strong> returns a list of USB-200 devices plugged into the computer&#8217;s USB ports. The <strong>Open<\/strong> command creates a virtual device containing such information as type, name, serial number, etc. The <strong>Start<\/strong> command is responsible for programming the device created with <strong>Open<\/strong>, managing the data buffer, and uploading the data to a web application. The <strong>Stop<\/strong> command stops the acquisition, frees the memory, and releases the device. The <strong>Exit<\/strong> command closes the socket created initially.<\/p>\n<p>One of\u00a0<span style=\"margin: 0px; padding: 0px;\">the server&#8217;s first tasks after receiving\u00a0<strong>Start<\/strong> 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&#8217;s FIFO memory that triggers<\/span>\u00a0an automatic USB transfer to PC memory.<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-plain\" data-lang=\"Plain Text\"><code>number_of_channels = len(config.Channels)\r\nsample_rate = config.Rate\r\none_second = sample_rate * number_of_channels\r\nbuffer_size = (one_second - (one_second % 96)) * 10\r\nmemHandle = ul.scaled_win_buf_alloc(buffer_size)\r\n<\/code><\/pre>\n<\/div>\n<p>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.\u00a0 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.<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-plain\" data-lang=\"Plain Text\"><code># set channel order\r\nul.a_load_queue(board_number, config.Channels, config.Ranges, number_of_channels)\r\n# set the scan option for continuous background operation and scale the data to volts\r\nopts = ScanOptions.SCALEDATA | ScanOptions.CONTINUOUS | ScanOptions.BACKGROUND\r\n# start the acquisition\r\nactual_rate = ul.a_in_scan(board_number, 0, 3, buffer_size, sample_rate, ULRange.BIP10VOLTS, memHandle, opts)<\/code><\/pre>\n<\/div>\n<p>At this time, it&#8217;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 (&#8216;I&#8217;) and sent, followed by sending the encoded string as shown below:<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-plain\" data-lang=\"Plain Text\"><code>json_string = json.dumps(itm)\r\npacked_data = struct.pack('&lt;' + 'I', len(json_string))\r\nconn.sendall(packed_data)\r\nconn.sendall(json_string.encode('utf-8'))<\/code><\/pre>\n<\/div>\n<p>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(&#8216;utf-8&#8217;) and json.loads() converts the JSON string to a Python list.<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-plain\" data-lang=\"Plain Text\"><code>recv_str = self.socket.recv(ctypes.sizeof(ctypes.c_uint))\r\nunpacked_tuple = struct.unpack('&lt;' + 'I', recv_str)\r\nmsg = list(unpacked_tuple)\r\nbytes_to_recv = msg[0]\r\ns = self.socket.recv(bytes_to_recv)\r\nitem = json.loads(s.decode('utf-8')<\/code><\/pre>\n<\/div>\n<p>Below is the buffer management loop. First, ul.get_status is called to get the newest scan&#8217;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.<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-plain\" data-lang=\"Plain Text\"><code>while flag.is_set():\r\n\r\nstatus, curr_count, head = ul.get_status(board_number, FunctionType.AIFUNCTION)\r\nif head &gt; tail:\r\n    if (head - tail) &gt; int(sample_rate * number_of_channels \/ 2):\r\n        sending = head - tail\r\n        samples = samples + sending\r\n        send_msg(conn, sending)\r\n\r\n        logger.info(f'Transferred {sending:6d}\\tTotal Transferred {samples:12d}')\r\n\r\n        python_list = data[tail: head]\r\n        packed_data = struct.pack('&lt;' + 'd' * len(python_list), *python_list)\r\n        conn.sendall(packed_data)\r\n        tail = head\r\nelse:\r\n    send = buffer_size - tail + head\r\n    if send &gt; int(sample_rate * number_of_channels \/ 2):\r\n        samples = samples + sending\r\n        send_msg(conn, sending)\r\n\r\n        logger.info(f'Transferred {sending:6d}\\tTotal Transferred {samples:12d}')\r\n\r\n        python_list = data[tail:buffer_size] + data[0:head]\r\n        packed_data = struct.pack('&lt;' + 'd' * len(python_list), *python_list)\r\n        conn.sendall(packed_data)\r\n        tail = head<\/code><\/pre>\n<\/div>\n<p>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.<\/p>\n<p>2025-08-01 11:31:13,288 &#8211; serverInfo &#8211; INFO &#8211; Info message: serverInfo module loaded successfully.<br \/>\n2025-08-01 11:31:13,293 &#8211; serverInfo &#8211; INFO &#8211; Server listening on 127.0.0.1:65432&#8230;<br \/>\n2025-08-01 11:31:16,704 &#8211; serverInfo &#8211; INFO &#8211; Active connections: 0<br \/>\n2025-08-01 11:31:16,751 &#8211; serverInfo &#8211; INFO &#8211; Received from (&#8216;127.0.0.1&#8217;, 46075): Command: list<br \/>\n2025-08-01 11:31:17,628 &#8211; serverInfo &#8211; INFO &#8211; Active connections: 1<br \/>\n2025-08-01 11:31:17,642 &#8211; serverInfo &#8211; INFO &#8211; Received from (&#8216;127.0.0.1&#8217;, 46076): Command: list<br \/>\n2025-08-01 11:31:22,317 &#8211; serverInfo &#8211; INFO &#8211; Received from (&#8216;127.0.0.1&#8217;, 46076): Command: open<br \/>\n2025-08-01 11:31:22,317 &#8211; serverInfo &#8211; INFO &#8211; Board number: 0<br \/>\n2025-08-01 11:31:22,317 &#8211; serverInfo &#8211; INFO &#8211; 1FFC8D1<br \/>\n2025-08-01 11:31:23,107 &#8211; serverInfo &#8211; INFO &#8211; Received from (&#8216;127.0.0.1&#8217;, 46076): Command: start<br \/>\n2025-08-01 11:31:23,107 &#8211; serverInfo &#8211; INFO &#8211; Input Channels: [0, 1, 2, 3]<br \/>\n2025-08-01 11:31:23,107 &#8211; serverInfo &#8211; INFO &#8211; Input Ranges: [1, 1, 1, 1]<br \/>\n2025-08-01 11:31:23,107 &#8211; serverInfo &#8211; INFO &#8211; Sample Rate: 1000<br \/>\n2025-08-01 11:31:23,107 &#8211; serverInfo &#8211; INFO &#8211; Samples to Display: 1000<br \/>\n2025-08-01 11:31:23,107 &#8211; serverInfo &#8211; INFO &#8211; Board number: 0<br \/>\n2025-08-01 11:31:23,107 &#8211; serverInfo &#8211; INFO &#8211; Board descriptor: USB-201<br \/>\n2025-08-01 11:31:23,647 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2108 Total Transferred 2108<br \/>\n2025-08-01 11:31:24,251 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2368 Total Transferred 4476<br \/>\n2025-08-01 11:31:24,755 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2048 Total Transferred 6524<br \/>\n2025-08-01 11:31:25,260 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2048 Total Transferred 8572<br \/>\n2025-08-01 11:31:25,872 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2432 Total Transferred 11004<br \/>\n2025-08-01 11:31:26,478 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2432 Total Transferred 13436<br \/>\n2025-08-01 11:31:27,083 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2368 Total Transferred 15804<br \/>\n2025-08-01 11:31:27,588 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2048 Total Transferred 17852<br \/>\n2025-08-01 11:31:28,093 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2048 Total Transferred 19900<br \/>\n2025-08-01 11:31:28,606 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2048 Total Transferred 21948<br \/>\n2025-08-01 11:31:29,118 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2048 Total Transferred 23996<br \/>\n2025-08-01 11:31:29,722 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2368 Total Transferred 26364<br \/>\n2025-08-01 11:31:30,231 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2048 Total Transferred 28412<br \/>\n2025-08-01 11:31:30,736 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2048 Total Transferred 30460<br \/>\n2025-08-01 11:31:31,343 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2432 Total Transferred 32892<br \/>\n2025-08-01 11:31:31,857 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2048 Total Transferred 34940<br \/>\n2025-08-01 11:31:32,462 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2432 Total Transferred 37372<br \/>\n2025-08-01 11:31:32,973 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2048 Total Transferred 39420<br \/>\n2025-08-01 11:31:33,584 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2432 Total Transferred 41852<br \/>\n2025-08-01 11:31:34,097 &#8211; serverInfo &#8211; INFO &#8211; Transferred 2048 Total Transferred 43900<br \/>\n2025-08-01 11:31:34,695 &#8211; serverInfo &#8211; INFO &#8211; Received from (&#8216;127.0.0.1&#8217;, 46076): Command: stop<br \/>\n2025-08-01 11:31:34,720 &#8211; serverInfo &#8211; INFO &#8211; Board 0 closed.<br \/>\n2025-08-01 11:31:45,608 &#8211; serverInfo &#8211; INFO &#8211; Received from (&#8216;127.0.0.1&#8217;, 46076): Command: exit<br \/>\n2025-08-01 11:31:45,608 &#8211; serverInfo &#8211; INFO &#8211; Connection with (&#8216;127.0.0.1&#8217;, 46076) closed.<br \/>\n2025-08-01 11:31:45,698 &#8211; serverInfo &#8211; INFO &#8211; Received from (&#8216;127.0.0.1&#8217;, 46075): Command: exit<br \/>\n2025-08-01 11:31:45,698 &#8211; serverInfo &#8211; INFO &#8211; Connection with (&#8216;127.0.0.1&#8217;, 46075) closed.<\/p>\n<h4><strong>The Web Application<\/strong><\/h4>\n<p><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/digilent.com\/blog\/wp-content\/uploads\/2025\/07\/MyVideo-1.gif\" alt=\"\" width=\"600\" height=\"320\" class=\"alignnone size-full wp-image-31827\" \/><\/p>\n<p><span style=\"font-size: 1rem;\">The <\/span><strong style=\"font-size: 1rem;\">List<\/strong><span style=\"font-size: 1rem;\"> command is sent to the server when your browser loads the web application&#8217;s HTML page<\/span>. 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.\u00a0 Up to eight channels can be used simultaneously using the Active Channels checkboxes. Valid numbers for sample rates are 100-12,500 Hz.\u00a0 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 <strong>start_stop_click<\/strong> callback for the &#8216;Configure&#8217; button and can be easily changed if you&#8217;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.<\/p>\n<p>Before anything else, the web application must know the server&#8217;s IP address and Port number. If running locally on a computer, use &#8216;127.0.0.1&#8217; for the IP address<span style=\"margin: 0px; padding: 0px;\"><span style=\"margin: 0px; padding: 0px;\"><span style=\"margin: 0px; padding: 0px;\"> and ensure the server and web application use different port numbers. <\/span><\/span><span style=\"margin: 0px; padding: 0px;\">The web application establishes the connection with the server in the <strong>create_board_selector<\/strong>\u00a0function, which is also responsible for sending the\u00a0<strong>List<\/strong> command to the server.<\/span> For each device in the list, it creates a tuple, converts it to a dictionary, and then to a JSON string.\u00a0 After it has processed all the devices, it returns a list containing JSON strings to the dcc dropdown element in an HTML web page.\u00a0<\/span><\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-plain\" data-lang=\"Plain Text\"><code>def create_board_selector():\r\n\r\n    board_selection_options = []\r\n    if daq_socket_manager.connect(HOST, PORT) is True:\r\n\r\n        dev_list = daq_socket_manager.get_device_list()\r\n\r\n        if len(dev_list) &gt; 0:\r\n            for device in dev_list:\r\n                board = namedtuple('board', ['board_num',\r\n                                             'serial_num',\r\n                                             'type',\r\n                                             'product_name'])\r\n\r\n                bd = board(board_num=device['Board_Number'],\r\n                           serial_num=device['Serial_Number'],\r\n                           type=device['Product_ID'],\r\n                           product_name=device['Name'])\r\n\r\n                label = '{0}: {1}'.format(bd.board_num, bd.product_name)\r\n                option = {'label': label, 'value': json.dumps(bd._asdict())}\r\n                board_selection_options.append(option)\r\n\r\n        selection = None\r\n        if board_selection_options:\r\n            selection = board_selection_options[0]['value']\r\n\r\n        return dcc.Dropdown(\r\n            id='daqSelector', options=board_selection_options,\r\n            value=selection, clearable=False,  style={'width': 200,\r\n                                                      'height': 40,\r\n                                                      'border-radius': '10px',\r\n                                                      'font-size': '16px'})\r\n    else:\r\n\r\n        label = '{0}: {1}'.format(-1, 'no devices')\r\n        option = {'label': label, 'value': 0}\r\n        board_selection_options.append(option)\r\n        return dcc.Dropdown(\r\n            id='daqSelector', options=board_selection_options,\r\n            clearable=False)\r\n<\/code><\/pre>\n<\/div>\n<p>Pressing the &#8216;Configure&#8217; button sends the<strong> Open<\/strong> 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 &#8216;Cancel&#8217;. Pressing &#8216;Start&#8217; sends the <strong>Start<\/strong> command to the server to acquire data. Pressing &#8216;Cancel&#8217; sends the <strong>Stop<\/strong> command, which performs the cleanup duties. The<strong> Exit<\/strong> command is sent to the server when the Dash server is closed with Ctrl-C or Ctrl-Break.\u00a0 The <strong>Exit<\/strong> command makes the server close the socket, but not the server as a whole. Use Ctrl-Break to kill the server completely.<\/p>\n<p>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&#8217;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,\u00a0 it reads the number of samples to expect. It then calculates the number of bytes and reads them until the count is satisfied.<\/p>\n<div class=\"hcb_wrap\">\n<pre class=\"prism line-numbers lang-python\" data-lang=\"Python\"><code>size_of_double = ctypes.sizeof(ctypes.c_double)\r\n\r\nwhile self.stop_event.is_set():\r\n\r\n    # get number of samples to receive\r\n    samples_to_read = self.get_msg('int')\r\n    chunk_size = size_of_double * samples_to_read\r\n\r\n    # read the samples\r\n    recv_str = b\"\"\r\n    while len(recv_str) &lt; chunk_size:\r\n        diff = min(chunk_size - len(recv_str), chunk_size)\r\n        chunk = self.socket.recv(diff)\r\n        recv_str += chunk\r\n\r\n        l = (len(recv_str) \/\/ struct.calcsize('d'))\r\n        unpacked_tuple = struct.unpack('&lt;' + 'd' * l, recv_str)\r\n\r\n        self.data_list = list(unpacked_tuple)\r\n        self.new_data = True\r\n\r\n<\/code><\/pre>\n<\/div>\n<p>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&#8217;ll do this and post an update.<\/p>\n<p>The code for this discussion can be downloaded at: <a href=\"https:\/\/github.com\/JRYS\/Dash-Data-Acquisition\">Dash-Data-Acquisition-Main.zip<\/a><\/p>\n<p>Before running the server and web application Python code, ensure you have the following.<\/p>\n<ul>\n<li>Install MCC&#8217;s InstaCal utility: <a href=\"https:\/\/files.digilent.com\/#downloads\/InstaCal\">InstaCal<\/a><\/li>\n<li>Install MCC&#8217;s Windows Python support: <a href=\"https:\/\/github.com\/mccdaq\/mcculw\/\">MCC UL Python<\/a><\/li>\n<li>Install Plotly&#8217;s Dash and Dash_Daq:<em><strong>\u00a0pip install dash<\/strong><\/em>, and<strong><em> pip install dash_daq<\/em><\/strong>.<\/li>\n<\/ul>\n<p>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&#8217;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.\u00a0 Start two command prompts from the folder containing all the files. If required, activate your virtual environment. Start the server by entering <em>python server.py<\/em> on the command line. In the other terminal, enter python <em>web-app.py<\/em>. When it runs, it displays an IP address to enter into your favorite browser&#8217;s address bar to start the application.<\/p>\n<p><span>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<\/span><span>\u00a0the example code<\/span><span>.<\/span><\/p>\n<div class='watch-action'><div class='watch-position align-left'><div class='action-like'><a class='lbg-style6 like-31790 jlk' data-task='like' data-post_id='31790' data-nonce='8896bc70a6' rel='nofollow'><img src='https:\/\/digilent.com\/blog\/wp-content\/plugins\/wti-like-post-pro\/images\/pixel.gif' title='Like' \/><span class='lc-31790 lc'>0<\/span><\/a><\/div><div class='action-unlike'><a class='unlbg-style6 unlike-31790 jlk' data-task='unlike' data-post_id='31790' data-nonce='8896bc70a6' rel='nofollow'><img src='https:\/\/digilent.com\/blog\/wp-content\/plugins\/wti-like-post-pro\/images\/pixel.gif' title='Unlike' \/><span class='unlc-31790 unlc'>0<\/span><\/a><\/div><\/div> <div class='status-31790 status align-left'>Be the 1st to vote.<\/div><\/div><div class='wti-clear'><\/div>","protected":false},"excerpt":{"rendered":"<p>Last month, I described a simple stateless web application using Python and Plotly&#8217;s Dash module. It created a dashboard allowing users to select waveform channels, update rate, and the number &hellip; <\/p>\n","protected":false},"author":60,"featured_media":31799,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[4361,20],"tags":[5191,4359,5170,5190,4626,5187,5189,5188],"ppma_author":[4461],"class_list":["post-31790","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-data-acquisition","category-products","tag-capture-live-data","tag-daq","tag-dash","tag-mcc-usb-200","tag-python","tag-server","tag-usb-200","tag-web-application"],"jetpack_featured_media_url":"https:\/\/digilent.com\/blog\/wp-content\/uploads\/2025\/07\/web_browser.png","jetpack_sharing_enabled":true,"authors":[{"term_id":4461,"user_id":60,"is_guest":0,"slug":"jrys","display_name":"John Rys","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/b5f589ceec35aa72d93bdb885888fe28157d060d3a9bd5484c6388d3f10fc51d?s=96&d=mm&r=g","1":"","2":"","3":"","4":"","5":"","6":"","7":"","8":"","9":"","10":""}],"post_mailing_queue_ids":[],"_links":{"self":[{"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/posts\/31790","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/users\/60"}],"replies":[{"embeddable":true,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/comments?post=31790"}],"version-history":[{"count":115,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/posts\/31790\/revisions"}],"predecessor-version":[{"id":31930,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/posts\/31790\/revisions\/31930"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/media\/31799"}],"wp:attachment":[{"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/media?parent=31790"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/categories?post=31790"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/tags?post=31790"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/digilent.com\/blog\/wp-json\/wp\/v2\/ppma_author?post=31790"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}