【DigiKey Creative Contest】Multi-channel Micro Gas Chromatography Acquisition Unit - Submission
[Copy link]
Work title: Multi-channel micro gas chromatography acquisition unit
Author: sunduoze
1. Introduction
This work mainly develops a multi-channel data acquisition unit for a micro gas chromatography system. The EVAL-AD7606CFMCZ board recommended by the competition is used as the core of signal acquisition. The entire system is powered by PD and uses ESP32 as the main control and its Wi-Fi to achieve compatibility with three different types of synchronous or asynchronous chromatography acquisition: TCD, PID, and DID. The spectrum data is uploaded to computers and other devices through Wi-Fi to achieve IoT, which is convenient for users to view the spectrum measured by the detector in the gas chromatography in real time and can assist in peak identification, data export and other functions. In addition, it also has voltage and current auxiliary measurement functions, and can switch the display of ADC data, channel calibration, and adjust TCD power supply through the OLED menu.
Glossary:
Micro gas chromatography: It is a miniaturized, portable gas chromatography (GC, a chemical analysis technology for separating and analyzing components in a mixture) system. Through miniaturized chromatographic columns and micro detectors, it can efficiently separate and detect components in gas mixtures, and is widely used in the field of rapid and real-time gas analysis.
Thermal conductivity detector TCD : One of the commonly used detectors in gas chromatographs, it detects and quantitatively separates compounds in the column by measuring the changes in the heat conducted by the gas in the detector, and is widely used to analyze components in gas mixtures.
Photoionization detector PID : A detector in gas chromatographs that uses the ion current generated by ultraviolet light irradiating the sample to detect and quantitatively separate volatile organic compounds in the column, and is particularly suitable for detecting low concentrations of volatile organic compounds.
Discharge ionization detector DID : A detector in gas chromatographs that mainly uses the method of ion current generated by discharge to indirectly ionize compounds to detect and quantitatively separate compounds in the column, and can detect almost all compounds.
2. System Block Diagram
The entire system development includes the hardware of the acquisition unit (Detector DAS Board & Eval-AD7606C board) and its embedded software based on ESP32 and computer software (Data Acquisition System). After the acquisition unit is started, it connects to the router via Wi-Fi and starts the TCP Client. The computer enters the IP address of the server of the host software and connects to the acquisition unit. The acquisition unit sends the collected data to the host software, which displays the data, graphs and exports the data through the UI.
2.1 Hardware Architecture
The entire system is developed based on the ADI EVAL-AD7606CFMCZ evaluation kit | Analog Devices (analog.com) evaluation board to develop the control, interaction, IoT, and detector interface parts to realize the hardware part of the entire system.
The whole system uses ESP32 as the control core, adopts TYPE-C interface for power supply and burning (and serial communication), uses Type-C controller to communicate with adapter for cc to realize PD charging, switches to 20V voltage input, and uses spiral coding button, OLED, and buzzer for simple hardware UI interaction. Use digital potentiometer to control the high-performance voltage source Vs1 with adjustable output to power TCD, and Vs2 which is stepped down to 5.6V by DC/DC to power EVAL-AD7606C, PID and DID. The 6 channels of AD7606C collect TCD, PID, DID, Vs1, Vs2, VBUS (20V) signals or voltages respectively, and the remaining 2 channels are reserved for auxiliary voltage measurement (configurable up to ±20V differential input range) and current measurement (0.1-9.8A) functions.
2.2 Embedded Software Architecture
Since it can choose Arduino mode programming and has a rich library that can be easily called, it can greatly improve development efficiency. It realizes the functions of Wi-Fi Client receiving and sending data, controlling OLED menu switching through spiral coding buttons, calibrating collected data, and displaying values and curves.
The embedded software is developed using Platform IO+VSCODE for ESP32. ESP32 has two cores. In order to maximize the use, FreeRTOS is used to schedule the two cores for ADC real-time data acquisition and other tasks (including Wi-Fi client, OLED menu processing and display, debugging thread, etc.).
The overall embedded software is based on the ESP32 platform and runs the Free RTOS system to facilitate development. It mainly includes three layers. The application layer mainly includes the transceiver module for implementing Wi-Fi client data and the interactive module for OLED; the middle layer includes the U8g2 graphics library, Wi-Fi Client library, etc. to facilitate the interaction between the application layer and the bottom layer (LL, HAL, driver layer); the bottom layer includes driver libraries such as AD7606C, AD5272, FUSB302, and HAL libraries such as I2C, SPI, and GPIO.
2.3 Host computer software architecture
The host computer software also uses VSCODE and is developed using Python. The interface part is implemented using PyQt5. With the help of its mature wheels, the entire application development with preliminary functions can be realized with 300+ lines of code.
A data acquisition system (DAS) with a graphical user interface using PyQt5 for real-time data visualization, control, and analysis. The software architecture includes the following components:
DataReceiver class:
Listens for incoming data on the specified IP address and port.
Emits a signal with received data.
Manages storage of data buffers and optionally writes data to CSV files.
PlotUpdater class:
Use PyQtGraph to plot updates of received data in real time and send signals to update the plot and text display.
SpectrumAnalysisThread 类:
Perform spectrum analysis in a separate thread using the 'spectrum_analysis.py' script.
MainWindow class:
The main GUI window is implemented using PyQt5.
Integrates a PlotWidget for real-time plotting and a QTextEdit for displaying data.
Handles user interactions such as starting/stopping the data server, clearing charts, and starting spectrum analysis.
Use separate threads for data reception, plotting, spectrum analysis, and opening a web browser to display the Dash app.
Dash Integration:
The code includes a Dash application for spectrum analysis.
When the user clicks the "Stop" button, the Dash app is started in a separate thread and a web browser is opened to visualize the analysis results.
Overall, the architecture adopts a modular and multi-threaded design to separate concerns related to data ingestion, visualization, and analysis to improve performance and responsiveness. The GUI integrates real-time plotting and Dash-based spectrum analysis to enable comprehensive data exploration.
3. Functional description of each part
3.1 Main Hardware Components
3.1.1 Controller and communication unit
ESP32 is a low-cost, high-performance microcontroller chip developed by Espressif Systems, mainly used in Internet of Things (IoT) applications. ESP32 inherits the characteristics of its predecessor ESP8266 and improves on performance, power consumption and functionality. Here are some important features and introductions of ESP32:
- Dual-core processor: ESP32 is equipped with two processor cores, which can be used for application processing and Wi-Fi network protocol stack management respectively, improving the overall performance and response speed of the system.
- Wi-Fi and Bluetooth connectivity: ESP32 supports 2.4GHz Wi-Fi connectivity, which is suitable for wireless network communications. In addition, it also integrates Bluetooth 4.2/5.0 technology, which can be used to connect to other Bluetooth devices or implement Bluetooth Mesh networks.
- Rich interfaces: ESP32 provides a variety of general-purpose input and output interfaces (GPIO), SPI, I2C, UART, etc., making it easy to connect with other hardware devices and sensors to meet the needs of various application scenarios.
- Low-power design: ESP32 is designed with power optimization in mind and supports multiple low-power modes, enabling long-term operation in battery-powered applications.
- ADC Analog-to-Digital Converter
The system uses ADI's EVAL-AD7606CFMCZ evaluation board to implement the ADC function. The AD7606C-18 is an 18-bit, 1 MSPS SAR-ADC (all channels) with an input buffer with a minimum analog input impedance (RIN) of 1MΩ, which can eliminate the previous front-end circuit design and greatly facilitate the collection of parallel data. At the same time, the ADC mode (differential, single-ended) and range can be switched through register configuration, which greatly simplifies the Front-End design cost and simplifies the complexity of system design.
This design uses single-wire SPI communication to save pin occupancy. In addition, the evaluation board integrates the ADR4525 Class B reference source, 2 ppm/°C temperature drift performance, and 1.25μV pp low-frequency output noise, which greatly reduces the error and noise of the entire system.
Since the chip has 8-channel acquisition function, in order to fully utilize its channels, in addition to the detector signal, auxiliary voltage and current measurement functions are reserved, and the system has different detector power supply voltage monitoring functions. The following is the actual channel configuration information:
Evaluation board jumper cap configuration:
3.1.3 USB to Serial (One-click burning of ESPxx without external transistors)
CH343 is a USB bus adapter chip, which realizes USB to high-speed asynchronous serial port, supports automatic identification and dynamic adaptation of communication baud rate of 115200bps and below, and provides common MODEM communication signals, which are used to expand asynchronous serial port for computer, or upgrade ordinary serial port equipment or MCU directly to USB bus. Through RTS pin and DTRTNOW pin, ESPxx's EN pin and IO00 pin are connected, and flow control is used to realize automatic burning of program.
3.1.4 Other peripherals
1. YOU ARE
The system uses OLED (SSD1306) as an auxiliary display screen to facilitate user function switching.
2. Spiral coding button, which is used to interact with OLED UI, greatly facilitating debugging and use
3.2 Power Supply Design
3.2.1 Power Requirements
- The DID & PID in the system require a power supply of 5.6V or above, and the AD7606C EVAL board also requires a power supply of 5V or above.
- The TCD detector requires an 18-20V power supply.
- MCU, OLED and other peripherals all require 3.3V power supply
3.2.2 Power Management
The system uses the Type-C interface and the PD protocol to communicate with the adapter through the cc line, outputting 20V voltage to power the entire system. The buck circuit is used to step down the voltage to 5.6V to power the DID and PID detectors and the AD7606C EVAL board; the ultra-low noise and ultra-high PSRR LDO and programmable resistors are used to generate 18-20V programmable voltage output; the LDO is used to convert 5.6V to 3.3V to power the MCU and peripherals.
- TYPE-C and PD
This system uses FUSB302B to achieve USB Type-C detection, including connection and orientation, with less programming. The design can achieve power delivery of up to 60 W.
- Adjustable power supply for TCD
This power supply uses ADI's LT3045 ultra-low noise and PSRR LDO to provide power to the detector. Due to actual needs (the design here exceeds its typical value but does not exceed its absolute maximum value), it is required to have a small range of voltage regulation function, so AD5272 is used to adjust the maximum voltage (maximum 20kOhm)
20V to 5.6V step-down Buck circuit and 5.6V to 3.3V LDO
3.2.3 Current acquisition (measurement range: 0.1A – 9.8A)
The current acquisition part uses the current monitoring amplifier INA290A3 that supports high common-mode voltage. The voltage across the Vishay Y14730R00500B0R ±5ppm/°C 5 mOhms ±0.1% 3W precision resistor is amplified 100 times and sent to ADC-CH8 for acquisition (here the universal interface of ADI's op amp evaluation board is used to connect to the ADC board). In theory, a minimum resolution of 76.3uA can be achieved (the actual design connector pin1 does not correspond to pin1 of the ADI board. ADC-CH8 should be configured for 0-5V unipolar sampling to achieve a current resolution of ~38uA).
3.3 Physical Design
3.3.1 Internal Connections
In order to facilitate the use of the board and repeated development, the board stacking method is adopted. SMB connectors, plug-in connectors and cables, pin headers and female headers are actually used for connection.
3.3.2 External interfaces
PID and DID both use Type-C interfaces (no foolproofing is done here, but the PD adapter will not be damaged when inserted), and TCD is connected to it with an XH-2.54 connector. Auxiliary In order to facilitate the use of the board and repeated development, the board stacking method is adopted. SMB connectors, plug-in connectors and cables, pin headers and female headers are actually used for connection.
3.3.3 Others
4. Source Code
Hardware information:
Embedded software source code:
Main logic code: (For all codes, see the download link)
#include "task.h"
#include <WiFi.h>
#ifdef U8X8_HAVE_HW_I2C
#include <Wire.h>
#endif
#include "soc/rtc_wdt.h" // 设置看门狗应用
#include "PD_UFP.h"
#include "kalman_filter.h"
#include "event.h"
#include "rotary.h"
#include "beep.h"
#include "menu.h"
uint8_t *C_table[] = {c1, c2, c3, Lightning, c5, c6, c7};
uint8_t rotary_dir = false;
uint8_t volume = true;
int32_t adc_raw_data[ADC_ALL_CH];
float adc_disp_val[ADC_ALL_CH];
uint8_t conn_wifi = 0;
adc_calibration_ adc_cali;
char ssid[] = "xxxxx"; // wifi名
char password[] = "xxxxx"; // wifi密码
char ssid_bk[] = "xxxxx"; // wifi名
char password_bk[] = "xxxxx"; // wifi密码
const IPAddress serverIP(192, 168, 1, 1); // 欲访问的服务端IP地址
uint16_t serverPort = 1234; // 服务端口号
uint64_t ChipMAC;
char ChipMAC_S[19] = {0};
char CompileTime[20];
KalmanFilter kf_disp(0.00f, 1.0f, 10.0f, 100.0f);
KalmanFilter kf_main_ui(0.00f, 1.0f, 100.0f, 20000.0f);
WiFiClient client; // 声明一个ESP32客户端对象,用于与服务器进行连接
AD7606C_Serial AD7606C_18(ADC_CONVST, ADC_BUSY);
U8G2_SSD1306_128X64_NONAME_F_HW_I2C oled(U8G2_R0, /* reset=*/U8X8_PIN_NONE, /* clock=*/SCL, /* data=*/SDA); // ESP32 Thing, HW I2C with pin remapping
AD5272 digital_pot(POT_ADDR_NC);
PD_UFP_c PD_UFP;
uint8_t pd_init(void)
{
uint8_t ret = 0;
uint16_t timeout = 2000;
PD_UFP.init(PD_POWER_OPTION_MAX_20V);
Serial.printf("PD_UFP.init-ing\r\n");
while (1)
{
PD_UFP.run();
if (PD_UFP.is_power_ready())
{
if (PD_UFP.get_voltage() == PD_V(20.0) && PD_UFP.get_current() >= PD_A(1.5))
{
PD_UFP.set_output(1); // Turn on load switch
Serial.printf("PD 20V ENABLE Sucess\r\n");
SetSound(BootSound); // 播放音效
break;
}
else
{
PD_UFP.set_output(0); // Turn off load switch
Serial.printf("DISABLE\r\n");
ret = 1;
break;
}
}
if (timeout-- <= 1)
{
Serial.printf("PD power not ready\r\n");
ret = 2;
break;
}
vTaskDelay(1);
}
return ret;
}
uint8_t wifi_init()
{
uint8_t ret = 0;
WiFi.mode(WIFI_STA);
WiFi.setSleep(false); // 关闭STA模式下wifi休眠,提高响应速度
WiFi.begin(ssid, password);
uint8_t i = 100;
while (WiFi.status() != WL_CONNECTED)
{
delay(200);
Serial.print(".");
if (i-- == 50)
{
Serial.printf("Connecting to %s\r\n", ssid_bk);
WiFi.begin(ssid_bk, password_bk);
Serial.println("WiFi connected");
}
if (i <= 1)
{
Serial.print("\r\nWifi connect failed\r\n");
ret = 1;
break;
}
}
if (ret == 0)
{
if (i > 50)
{
Serial.printf("Connecting to %s\r\n", ssid);
}
Serial.println("IP address: ");
Serial.println(WiFi.localIP());
Serial.println("MAC address: ");
Serial.println(WiFi.macAddress());
Serial.println("DNS address: ");
Serial.println(WiFi.dnsIP());
Serial.println("Gateway address: ");
Serial.println(WiFi.gatewayIP());
Serial.println("Subnet mask: ");
Serial.println(WiFi.subnetMask());
}
return ret;
}
void adc_init()
{
adc_cali.adc_gain_ch[ADC_CH1] = 1.0f;
adc_cali.adc_offset_ch[ADC_CH1] = -0.017699; //-0.017547;
adc_cali.adc_gain_ch[ADC_CH2] = 0.99664f; // 未校准
adc_cali.adc_offset_ch[ADC_CH2] = -0.017547; // 未校准
adc_cali.adc_gain_ch[ADC_CH3] = 0.997993; // 0.990973;
adc_cali.adc_offset_ch[ADC_CH3] = -0.01098;
adc_cali.adc_gain_ch[ADC_CH4] = 1.000207f;
adc_cali.adc_offset_ch[ADC_CH4] = -0.00011;
adc_cali.adc_gain_ch[ADC_CH5] = 1.000132f;
adc_cali.adc_offset_ch[ADC_CH5] = 0.0;
adc_cali.adc_gain_ch[ADC_CH6] = 0.999882f;
adc_cali.adc_offset_ch[ADC_CH6] = 0.0;
adc_cali.adc_gain_ch[ADC_CH7] = 5.0f * 1.000188f;
adc_cali.adc_offset_ch[ADC_CH7] = 0.0;
adc_cali.adc_gain_ch[ADC_CH8] = -2.0f * 1.000188f; // 未校准增益 curr = volt / 100 / 0.005R
adc_cali.adc_offset_ch[ADC_CH8] = 0.00605;
AD7606C_18.config();
}
void oled_init()
{
oled.begin();
oled.setBusClock(4e5);
oled.enableUTF8Print();
oled.setFontDirection(0);
oled.setFontPosTop();
oled.setFont(u8g2_font_wqy12_t_gb2312);
oled.setDrawColor(1);
oled.setFontMode(1);
}
void ad527x_init()
{
Wire.begin(SDA, SCL, 1e5);
int ret = digital_pot.set_res_val(0);
if (ret != 0) // check if data is sent successfully
Serial.println("[Error]digital_pot init !");
Wire.begin(SDA, SCL, 1e6);
}
void hardware_init(void)
{
beep_init();
WRITE_PERI_REG(RTC_CNTL_BROWN_OUT_REG, 0); // 关闭断电检测
Serial.begin(5e5);
Wire.begin(SDA, SCL, 4e5);
if (pd_init() == 0)
{
pd_status = true;
}
// FilesSystemInit(); // 启动文件系统,并读取存档 **bug***
Serial.printf("\r\n\r\nDetector DAS based on EVAL-AD7606CFMCZ !\r\n");
oled_init();
disp_boot_info();
delay(500);
enter_logo();
if (digital_pot.init() != 0)
{
Serial.println("Cannot send data to the digital_pot.");
}
ad527x_init();
rotary_init(); // 初始化编码器
adc_init();
// ble_phyphox_init();
rtc_wdt_protect_off(); // 看门狗写保护关闭
rtc_wdt_enable();
rtc_wdt_set_time(RTC_WDT_STAGE0, 3000); // wdt timeout
get_sin();
}
uint8_t sin_tab[SIN_TB_SIZE];
void get_sin(void)
{
const uint16_t uPoints = SIN_TB_SIZE;
float x, uAng;
uAng = 360.000 / uPoints;
for (int i = 0; i < uPoints; i++)
{
x = uAng * i;
x = x * (3.14159 / 180); // 弧度=角度*(π/180)
sin_tab[i] = (255 / 2) * sin(x) + (255 / 2);
// printf("sin tab[%d]: %f\n", i, sin_tab[i]);
}
}
void xTask_oled(void *xTask)
{
static double rotary;
static double rotary_hist;
while (1)
{
sys_KeyProcess();
// TimerEventLoop();
rotary = sys_Counter_Get();
#ifdef ROTARY_DEBUG
if (rotary != rotary_hist)
{
Serial.printf("rotary:%lf\n", rotary);
}
#endif // ROTARY_DEBUG
rotary_hist = rotary;
// 刷新UI
System_UI();
vTaskDelay(1);
}
}
/*
*
*/
void xTask_dbgx(void *xTask)
{
// Serial.print("core[");
// Serial.print(xTaskGetAffinity(xTask));
// Serial.printf("]xTask_dbg \r\n");
while (1)
{
// Serial.printf("[%6d %6d %6d %6d][%6d %6d %6d %.6f]\r\n", adc_raw_data[0], adc_raw_data[1], adc_raw_data[2], adc_raw_data[3], adc_raw_data[4], adc_raw_data[5], adc_raw_data[6], C2V(adc_r_d_avg[ADC_CH8], PN5V00));
vTaskDelay(100);
}
}
void xTask_adcx(void *xTask)
{
static uint16_t cnt;
// Serial.print("core[");
// Serial.print(xTaskGetAffinity(xTask));
// Serial.printf("]xTask_adc \r\n");
// Serial.print("tsk_adc: ");
// Serial.println(millis());
while (1)
{
AD7606C_18.fast_read(adc_raw_data);
adc_disp_val[ADC_CH1] = (C2V(adc_raw_data[ADC_CH1], PN20V0) + adc_cali.adc_offset_ch[ADC_CH1]) * adc_cali.adc_gain_ch[ADC_CH1];
adc_disp_val[ADC_CH2] = (C2V(adc_raw_data[ADC_CH2], PN20V0) + adc_cali.adc_offset_ch[ADC_CH2]) * adc_cali.adc_gain_ch[ADC_CH2];
adc_disp_val[ADC_CH3] = (C2V(adc_raw_data[ADC_CH3], PN20V0) + adc_cali.adc_offset_ch[ADC_CH3]) * adc_cali.adc_gain_ch[ADC_CH3];
adc_disp_val[ADC_CH4] = (C2V(adc_raw_data[ADC_CH4], PP5V00) + adc_cali.adc_offset_ch[ADC_CH4]) * adc_cali.adc_gain_ch[ADC_CH4];
adc_disp_val[ADC_CH5] = (C2V(adc_raw_data[ADC_CH5], PP5V00) + adc_cali.adc_offset_ch[ADC_CH5]) * adc_cali.adc_gain_ch[ADC_CH5];
adc_disp_val[ADC_CH6] = (C2V(adc_raw_data[ADC_CH6], PP10V0) + adc_cali.adc_offset_ch[ADC_CH6]) * adc_cali.adc_gain_ch[ADC_CH6];
adc_disp_val[ADC_CH7] = (C2V(adc_raw_data[ADC_CH7], PP5V00) + adc_cali.adc_offset_ch[ADC_CH7]) * adc_cali.adc_gain_ch[ADC_CH7];
adc_disp_val[ADC_CH8] = (C2V(adc_raw_data[ADC_CH8], PN5V00) + adc_cali.adc_offset_ch[ADC_CH8]) * adc_cali.adc_gain_ch[ADC_CH8];
delayMicroseconds(730);
}
}
void xTask_wifi(void *xTask)
{
if (wifi_init() == 0)
{
wifi_status = true;
}
while (1)
{
// Serial.println("connect server -ing");
if (client.connect(serverIP, serverPort)) // 连接目标地址1
{
Serial.println("connect success!");
// client.print("Hello world!"); // 向服务器发送数据
while (client.connected() || client.available()) // 如果已连接或有收到的未读取的数据
{
conn_wifi = 1;
client.printf("%2.6f, %2.6f, %2.6f, %2.6f, %2.6f, %2.6f, %2.6f, %2.6f\r\n",
adc_disp_val[ADC_CH1], adc_disp_val[ADC_CH2],
adc_disp_val[ADC_CH3], adc_disp_val[ADC_CH4],
adc_disp_val[ADC_CH5], adc_disp_val[ADC_CH6],
adc_disp_val[ADC_CH7], adc_disp_val[ADC_CH8]);
vTaskDelay(100);
}
conn_wifi = 0;
Serial.println("close clent");
client.stop(); // 关闭客户端
}
else
{
conn_wifi = 0;
// Serial.println("connect fail!");
client.stop(); // 关闭客户端
}
vTaskDelay(1000);
}
}
void ble_phyphox_init()
{
PhyphoxBLE::start("Detector-DAS");
PhyphoxBleExperiment MultiGraph;
MultiGraph.setTitle("Detector-DAS");
MultiGraph.setCategory("DigiKey创意大赛:多通道微型气相色谱采集单元");
MultiGraph.setDescription("基于AD7606C-18的多通道检测器数据采集系统");
PhyphoxBleExperiment::View firstView;
firstView.setLabel("FirstView"); // Create a "view"
PhyphoxBleExperiment::Graph detector_data_graph;
PhyphoxBleExperiment::Graph::Subgraph pid_data;
pid_data.setChannel(1, 2);
pid_data.setColor("ff00ff");
pid_data.setStyle(STYLE_LINES);
pid_data.setLinewidth(2);
detector_data_graph.addSubgraph(pid_data);
PhyphoxBleExperiment::Graph::Subgraph did_data;
did_data.setChannel(1, 3);
did_data.setColor("0000ff");
did_data.setStyle(STYLE_LINES);
did_data.setLinewidth(2);
detector_data_graph.addSubgraph(did_data);
PhyphoxBleExperiment::Graph::Subgraph tcd_data;
tcd_data.setChannel(1, 4);
tcd_data.setColor("0ee0ff");
tcd_data.setStyle(STYLE_LINES);
tcd_data.setLinewidth(2);
detector_data_graph.addSubgraph(tcd_data);
firstView.addElement(detector_data_graph);
MultiGraph.addView(firstView);
PhyphoxBLE::addExperiment(MultiGraph);
// PhyphoxBLE::printXML(&Serial);
}
float periodTime2 = 2.0; // in s
float generateSin2(float x)
{
return 1.0 * sin(x * 2.0 * PI / periodTime2);
}
void xTask_blex(void *xTask)
{
float currentTime = millis() / 1000.0;
float sinus = generateSin2(currentTime);
float cosinus = generateSin2(currentTime + 0.5 * periodTime2);
PhyphoxBLE::write(currentTime, sinus, cosinus, cosinus);
delay(100);
// PhyphoxBLE::poll(); //Only required for the Arduino Nano 33 IoT, but it does no harm for other boards.
}
void xTask_blex2(void *xTask)
{
float time;
float data0, data3, data4;
while (1)
{
time = millis() / 1000.0;
// data0 = C2V(adc_r_d_avg[0], PN10V0);
// data3 = C2V(adc_r_d_avg[3], PN10V0);
// data4 = C2V(adc_r_d_avg[4], PN10V0);
PhyphoxBLE::write(time, data0, data3, data4);
vTaskDelay(100);
PhyphoxBLE::poll(); // Only required for the Arduino Nano 33 IoT, but it does no harm for other boards.
}
}
float map_f(float x, float in_min, float in_max, float out_min, float out_max)
{
const float run = in_max - in_min;
if (run == 0)
{
log_e("map(): Invalid input range, min == max");
return -1; // AVR returns -1, SAM returns 0
}
const float rise = out_max - out_min;
const float delta = x - in_min;
return (delta * rise) / run + out_min;
}
void i2c_dev_scan()
{
Wire.begin(SDA, SCL, 1e3);
Serial.begin(5e5);
uint8_t error, address;
int nDevices;
Serial.println("Scanning...");
nDevices = 0;
for (address = 1; address < 127; address++)
{
Wire.beginTransmission(address);
error = Wire.endTransmission();
if (error == 0)
{
Serial.print("I2C device found at address 0x");
if (address < 16)
Serial.print("0");
Serial.print(address, HEX);
Serial.println(" !");
nDevices++;
}
else if (error == 4)
{
Serial.print("Unknow error at address 0x");
if (address < 16)
Serial.print("0");
Serial.println(address, HEX);
}
}
if (nDevices == 0)
Serial.println("No I2C devices found\n");
else
Serial.println("done\n");
delay(5000); // wait 5 seconds for next scan
}
Upper computer software source code:
import sys
import socket
import subprocess
from PyQt5 import sip
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QTextEdit, QPushButton, QLabel, \
QHBoxLayout, QLineEdit, QScrollArea
from PyQt5.QtCore import pyqtSignal, QObject, QThread, QTimer, QDateTime
import pyqtgraph as pg
import numpy as np
from dash import Dash, dcc, html, dash_table
import webbrowser
import time
class DataReceiver(QObject):
data_received = pyqtSignal(list)
def __init__(self, host, port):
super().__init__()
self.host = host
self.port = port
self.server_socket = None
self.running = False
self.data_buffer = []
self.csv_file = None
self.start_time = None
def start_server(self):
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_socket.bind((self.host, self.port))
self.server_socket.listen(1)
self.running = True
self.data_buffer = []
while self.running:
client_socket, address = self.server_socket.accept()
with client_socket:
print('Connected by', address)
buffer = ""
while self.running:
data = client_socket.recv(1024).decode('utf-8')
if not data:
break
buffer += data
messages = buffer.split('\r\n')
for msg in messages[:-1]:
data_values = [float(value) for value in msg.split(',')]
self.data_received.emit(data_values)
self.data_buffer.append(data_values)
if self.csv_file is not None:
timestamp = (QDateTime.currentMSecsSinceEpoch() - self.start_time) / 1000.0
row = [f'{timestamp:.3f}'] + list(map(str, data_values))
self.csv_file.write(','.join(row) + '\n')
buffer = messages[-1]
def stop_server(self):
self.running = False
if self.server_socket:
self.server_socket.close()
if self.csv_file:
self.csv_file.close()
self.csv_file = None
def start_csv_file(self):
timestamp = QDateTime.currentDateTime().toString("yyyyMMddhhmmss")
filename = f"DAS_{timestamp}.csv"
self.csv_file = open(filename, 'w')
self.csv_file.write("Timestamp,CH1,CH2,CH3,CH4,CH5,CH6,CH7,CH8\n")
self.start_time = QDateTime.currentMSecsSinceEpoch()
class PlotUpdater(QObject):
update_plot_signal = pyqtSignal(list)
update_text_signal = pyqtSignal(str)
def __init__(self, curve_dict, text_edit):
super().__init__()
self.curve_dict = curve_dict
self.text_edit = text_edit
def update_plot(self, data_values):
self.update_plot_signal.emit(data_values)
def update_text(self, text):
self.update_text_signal.emit(text)
class SpectrumAnalysisThread(QThread):
def run(self):
try:
subprocess.run(['python', '*.py'], timeout=60 * 1) # Run for 1 minute (60 seconds)
except subprocess.TimeoutExpired:
print("Spectrum Analysis completed.")
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.run_dash_flag = 0
self.setWindowTitle('Data Acquisition System by sunduoze 20231231')
self.setFixedSize(1800, 1500)
self.setGeometry(100, 100, 800, 600)
self.central_widget = QWidget()
self.setCentralWidget(self.central_widget)
self.layout = QVBoxLayout(self.central_widget)
# 禁止拖动图表,并启用抗锯齿
pg.setConfigOptions(background='k', foreground='y', leftButtonPan=False, antialias=False, useOpenGL=True,
useNumba=True)
# 创建水平布局
self.input_layout = QHBoxLayout()
# 创建 IP 和端口输入框
self.ip_port_input = QLineEdit(self)
self.ip_port_input.setText("192.168.1.1:1234")
self.input_layout.addWidget(self.ip_port_input)
# 创建按钮
self.start_button = QPushButton('Start', self)
self.stop_button = QPushButton('Stop', self)
self.clear_chart_button = QPushButton('Clear Chart', self)
self.clear_data_button = QPushButton('Clear Data', self)
# 将按钮添加到布局中
self.input_layout.addWidget(self.start_button)
self.input_layout.addWidget(self.stop_button)
self.input_layout.addWidget(self.clear_chart_button)
self.input_layout.addWidget(self.clear_data_button)
# 将输入布局添加到主布局
self.layout.addLayout(self.input_layout)
# 创建图表布局
self.plot_layout = QVBoxLayout()
self.plot_widget = pg.PlotWidget()
self.plot_layout.addWidget(self.plot_widget)
self.plot_widget.setXRange(-200, 2000) # 设置y轴范围
self.plot_widget.showGrid(x=True, y=True)
# 创建滚动区域
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.text_edit = QTextEdit()
self.scroll_area.setWidget(self.text_edit)
self.text_edit.setFixedWidth(1760)
self.text_edit.setFixedHeight(500)
self.scrollBar = self.scroll_area.verticalScrollBar()
self.scrollBar.setValue(25)
# 将滚动区域添加到图表布局
self.plot_layout.addWidget(self.scroll_area)
# 将图表布局添加到主布局
self.layout.addLayout(self.plot_layout)
# 创建一个水平布局
self.labels_layout = QHBoxLayout()
# 创建两个 QLabel 控件
self.label1 = QLabel(self)
self.label1.setText("Label 1")
self.labels_layout.addWidget(self.label1)
self.original_font = self.label1.font() # 保存 label 的原始字体
new_font = self.label1.font()
new_font.setBold(True)
new_font.setPointSize(new_font.pointSize() + 6)
self.label1.setFont(new_font)
self.label2 = QLabel(self)
self.label2.setText("Label 2")
self.labels_layout.addWidget(self.label2)
new_font = self.label2.font()
new_font.setBold(True)
new_font.setPointSize(new_font.pointSize() + 6)
self.label2.setFont(new_font)
# 将 labels_layout 添加到主布局中
self.layout.addLayout(self.labels_layout)
# 创建曲线图
self.curve_dict = {}
self.curve_data = {}
colors = ['#e6194B', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#42d4f4', '#f032e6']
channel_name = ['TCD', 'TCD_PS', 'AUX_VOLT', 'PID', 'DID', 'PID_PS', 'VBUS', 'AUX_CURR']
widths = [3, 3, 3, 3, 3, 3, 3, 3]
# 添加图例
self.plot_widget.addLegend(labelTextColor="black", offset=(2, 2), labelTextSize='12pt',
brush=pg.mkBrush(color='#E8f0f0'))
for i in range(8):
name = channel_name[i % len(channel_name)]
pen = pg.mkPen(cosmetic=True, color=colors[i % len(colors)], width=widths[i % len(widths)])
self.curve_dict[i] = self.plot_widget.plot(pen=pen, name=name, labelTextSize='16pt')
self.curve_data[i] = np.array([])
self.data_receiver = DataReceiver('', 0)
self.plot_updater = PlotUpdater(self.curve_dict, self.text_edit)
self.data_receiver.data_received.connect(self.plot_updater.update_plot)
self.plot_updater.update_plot_signal.connect(self.update_plot)
self.plot_updater.update_text_signal.connect(self.update_text)
self.data_thread = QThread()
self.plot_thread = QThread()
self.start_dash_thread = QThread()
self.open_broswer_thread = QThread()
self.data_receiver.moveToThread(self.data_thread)
self.plot_updater.moveToThread(self.plot_thread)
self.data_thread.started.connect(self.data_receiver.start_server)
self.plot_thread.started.connect(self.start_plotting)
self.start_dash_thread.started.connect(self.run_dash)
self.open_broswer_thread.started.connect(self.open_broswer)
self.start_button.clicked.connect(self.start_server)
self.stop_button.clicked.connect(self.stop_server)
self.stop_button.clicked.connect(self.run_dash)
self.stop_button.clicked.connect(self.open_broswer)
self.clear_chart_button.clicked.connect(self.clear_chart)
self.clear_data_button.clicked.connect(self.clear_data)
self.timer = QTimer(self)
self.timer.timeout.connect(self.update_plot)
self.timer.start(100) # 设置定时器,每100毫秒更新一次绘图
def start_plotting(self):
for i in range(8):
self.curve_dict[i].setDownsampling(auto=True, method='peak')
self.curve_dict[i].setClipToView(True)
def start_server(self):
if not self.data_thread.isRunning():
ip, port = self.ip_port_input.text().split(':')
self.data_receiver.host = ip
self.data_receiver.port = int(port)
self.data_receiver.start_csv_file()
self.data_thread.start()
self.start_button.setStyleSheet("background-color: green")
self.start_button.setEnabled(False)
def stop_server(self):
if self.data_thread.isRunning():
self.data_receiver.stop_server()
self.data_thread.quit()
self.data_thread.wait()
self.start_button.setStyleSheet("")
self.start_button.setEnabled(True)
def run_dash(self):
print("run dash")
# Start spectrum analysis in a separate thread
app.run_server(debug=True, threaded=True)
def open_broswer(self):
# 等待应用启动完成,可以根据实际情况调整等待时间
time.sleep(5)
print("open broswer")
# 你的Dash应用启动的URL
url = "http://127.0.0.1:8050/"
webbrowser.open(url, new=2)
def clear_chart(self):
for i in range(8):
self.curve_data[i] = np.array([])
self.curve_dict[i].setData(y=self.curve_data[i])
def clear_data(self):
self.text_edit.clear()
def update_plot(self, data_values=None):
try:
if data_values is not None:
if not isinstance(data_values, list) or len(data_values) != 8:
raise ValueError("Invalid data format")
for i, value in enumerate(data_values):
if i >= 8:
break
if not isinstance(value, (int, float)):
raise ValueError(f"Invalid data type for value {i}: {type(value)}")
if len(self.curve_data[i]) >= 20000:
self.curve_data[i] = np.roll(self.curve_data[i], -1)
self.curve_data[i][-1] = value
else:
self.curve_data[i] = np.append(self.curve_data[i], value)
self.curve_dict[i].setData(y=self.curve_data[i])
self.label1.setText("VOLT:" + str(data_values[2]) + "V")
self.label2.setText("CURR:" + str(data_values[7]) + "A")
# 滚动显示最新40行文本数据
current_text = self.text_edit.toPlainText()
new_text = '\n'.join([current_text] + [', '.join(map(str, data_values))])
self.text_edit.setPlainText('\n'.join(new_text.splitlines()[-40:]))
# self.scroll_area.verticalScrollBar().setValue(30)
self.scrollBar.setValue(30)
# print(self.scrollBar.maximum())
except Exception as e:
print(f"Error in update_plot: {e}")
def update_text(self, text):
# 滚动显示最新40行文本数据
current_text = self.text_edit.toPlainText()
new_text = '\n'.join([current_text, text])
self.text_edit.setPlainText('\n'.join(new_text.splitlines()[-40:]))
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec_())
5. Demonstration video of the work’s functions
6. Project Summary
For me, the time for this competition was very short (I registered on November 14th and received the AD7606C evaluation board from DigiKey on November 30th). I completed the design of the hardware, embedded software, and host computer software of the entire system in less than 45 days after work. The hardware ranged from system design to device selection to circuit design, PCB outsourcing, manual patch and welding, and system debugging. The development of embedded software, especially the debugging of AD7606C, also took a lot of time, but with the help of open source libraries and projects such as U8g2, my development was greatly accelerated. For the host computer software, with the help of the powerful ChatGPT, I was able to provide different implementation ideas, jump out of the blind spot of choice, and greatly simplify the development of the entire software part. Only 300+ lines of code can realize the UI that could only be realized by countless codes in the past.
This project is a multi-channel gas chromatography acquisition unit, but its purpose is far more than that. It can be easily expanded into a data acquisition platform (the purpose of auxiliary voltage measurement and current measurement is for this purpose). With the upper computer software, it can provide an open and free platform for engineers and enthusiasts, which can help everyone realize more creativity more easily and easily. The PD power supply part included in the project does not use devices with integrated protocols, but can implement different protocols through user programming, such as PPS, 240w private protocol fast charging, etc., which greatly improves scalability. The Bluetooth thread is also reserved in the embedded software, and the code can be easily controlled through the mobile phone BLE connection by slightly modifying the code later.
The embedded software and host computer software in this competition all use open source tools and platforms, which can be used and developed very conveniently by friends in the future. With the help of the open source platform library, the development efficiency can be greatly improved. The complete set of software and hardware design materials of the entire system will be open to the forum and Github, hoping to help everyone's subsequent learning and creativity.
Finally, I would like to thank DigiKey and EEWORLD for organizing this grand competition, which provides a platform and opportunity for many electronics enthusiasts and professionals to showcase their ideas and continuously innovate and create.
VII. Others
github hardware design:
github embedded software source code:
GitHub host software source code:
|