当前位置: 首页 > news >正文

知识就是力量——制作一个红外计数器

知识就是力量——制作一个红外计数器

    • 一、硬件设计
      • 所需元件
      • 连接方式
    • 二、软件开发环境配置
      • 方案一:VSCode + PlatformIO(推荐)
      • 方案二:PyCharm + PlatformIO
    • 三、代码实现
      • 主程序代码 (src/main.c)
    • 四、功能扩展
      • 添加复位功能
      • 添加OLED显示
    • 五、调试技巧
    • 六、项目优化方向
  • 功能完整的双向计数器项目
    • 一、系统架构设计
    • 二、硬件设计
      • 所需元件
      • 连接方式
    • 三、ESP32代码实现
      • platformio.ini配置
      • 主程序代码 (src/main.cpp)
    • 四、PyQt电脑端界面
      • Python代码 (counter_monitor.py)
    • 五、使用说明
      • 安装Python依赖
      • 操作流程
      • 功能特点
    • 一、PyQt烧录工具界面
      • Python代码 (esp32_flasher_tool.py)
    • 二、设备配置文件生成器
      • Python代码 (config_generator.py)
    • 三、使用说明
      • 安装依赖
      • 功能特点
      • 使用流程

使用STM32制作一个红外计数器是一个非常好的项目。下面我将为您提供一个完整的实现方案,包括硬件连接、代码实现(使用HAL库)以及在PyCharm和VSCode中的开发设置。

一、硬件设计

所需元件

  • STM32F103C8T6开发板(Blue Pill)
  • 红外发射管
  • 红外接收管(或一体化红外接收头)
  • 330Ω电阻
  • 10kΩ电阻
  • 面包板和杜邦线

连接方式

方案一:使用分立红外对管

红外发射管:- 阳极 → 330Ω电阻 → STM32 PA1 (PWM输出)- 阴极 → GND红外接收管:- 集电极 → STM32 PA0 (ADC输入) + 10k上拉电阻到3.3V- 发射极 → GND

方案二:使用一体化红外接收头(推荐,抗干扰更好)

一体化红外接收头:- VCC → 3.3V- GND → GND- OUT → STM32 PA0 (外部中断引脚)

二、软件开发环境配置

方案一:VSCode + PlatformIO(推荐)

  1. 安装PlatformIO插件

  2. 创建新项目

    • Board: Generic STM32F103C8
    • Framework: STM32Cube
  3. 配置platformio.ini

[env:genericSTM32F103C8]
platform = ststm32
board = genericSTM32F103C8
framework = stm32cube; 调试设置
upload_protocol = stlink
debug_tool = stlink; 自定义选项
build_flags = -D HAL_UART_MODULE_ENABLED-D HAL_ADC_MODULE_ENABLED-D HAL_TIM_MODULE_ENABLED

方案二:PyCharm + PlatformIO

  • 安装PlatformIO插件后,步骤与VSCode完全相同

三、代码实现

主程序代码 (src/main.c)

#include "main.h"
#include <stdio.h>// 全局变量
UART_HandleTypeDef huart1;
TIM_HandleTypeDef htim2;
ADC_HandleTypeDef hadc1;volatile uint32_t counter = 0;        // 计数器
volatile uint8_t last_state = 0;      // 上次状态
uint32_t last_debounce_time = 0;      // 防抖时间
const uint32_t DEBOUNCE_DELAY = 50;   // 防抖延时(ms)// 函数声明
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_ADC1_Init(void);
static void MX_TIM2_Init(void);
static void MX_USART1_UART_Init(void);
void Error_Handler(void);// 重定向printf到串口
int _write(int file, char *ptr, int len)
{HAL_UART_Transmit(&huart1, (uint8_t*)ptr, len, HAL_MAX_DELAY);return len;
}int main(void)
{HAL_Init();SystemClock_Config();MX_GPIO_Init();MX_ADC1_Init();MX_TIM2_Init();MX_USART1_UART_Init();// 启动ADCHAL_ADC_Start(&hadc1);// 启动定时器用于防抖HAL_TIM_Base_Start(&htim2);printf("红外计数器启动成功!\r\n");printf("当前计数值: 0\r\n");while (1){// 读取ADC值(方案一:模拟量检测)HAL_ADC_PollForConversion(&hadc1, HAL_MAX_DELAY);uint32_t adc_value = HAL_ADC_GetValue(&hadc1);// 判断是否有物体通过(阈值可根据实际情况调整)// ADC值较低表示有物体遮挡uint8_t current_state = (adc_value < 2000) ? 1 : 0;// 防抖处理if (current_state != last_state) {last_debounce_time = HAL_GetTick();}if ((HAL_GetTick() - last_debounce_time) > DEBOUNCE_DELAY) {// 检测上升沿:从无物体到有物体if (current_state == 1 && last_state == 0) {counter++;printf("计数: %lu\r\n", counter);// 控制LED指示(可选)HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_RESET);HAL_Delay(100);HAL_GPIO_WritePin(GPIOC, GPIO_PIN_13, GPIO_PIN_SET);}}last_state = current_state;HAL_Delay(10); // 短暂延时}
}// 方案二:使用外部中断(如果使用一体化接收头)
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{if (GPIO_Pin == GPIO_PIN_0) {static uint32_t last_interrupt_time = 0;uint32_t current_time = HAL_GetTick();// 防抖处理if (current_time - last_interrupt_time > DEBOUNCE_DELAY) {counter++;printf("计数: %lu\r\n", counter);}last_interrupt_time = current_time;}
}// 系统时钟配置
void SystemClock_Config(void)
{RCC_OscInitTypeDef RCC_OscInitStruct = {0};RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;RCC_OscInitStruct.HSEState = RCC_HSE_ON;RCC_OscInitStruct.HSEPredivValue = RCC_HSE_PREDIV_DIV1;RCC_OscInitStruct.HSIState = RCC_HSI_ON;RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;RCC_OscInitStruct.PLL.PLLMUL = RCC_PLL_MUL9;HAL_RCC_OscConfig(&RCC_OscInitStruct);RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2;RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV2;RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV1;HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_2);
}// 外设初始化
static void MX_ADC1_Init(void)
{ADC_ChannelConfTypeDef sConfig = {0};hadc1.Instance = ADC1;hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE;hadc1.Init.ContinuousConvMode = ENABLE;hadc1.Init.DiscontinuousConvMode = DISABLE;hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;hadc1.Init.NbrOfConversion = 1;HAL_ADC_Init(&hadc1);sConfig.Channel = ADC_CHANNEL_0;sConfig.Rank = ADC_REGULAR_RANK_1;sConfig.SamplingTime = ADC_SAMPLETIME_71CYCLES_5;HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}static void MX_TIM2_Init(void)
{htim2.Instance = TIM2;htim2.Init.Prescaler = 7200 - 1;  // 10kHzhtim2.Init.CounterMode = TIM_COUNTERMODE_UP;htim2.Init.Period = 10000 - 1;    // 1shtim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;HAL_TIM_Base_Init(&htim2);
}static void MX_USART1_UART_Init(void)
{huart1.Instance = USART1;huart1.Init.BaudRate = 115200;huart1.Init.WordLength = UART_WORDLENGTH_8B;huart1.Init.StopBits = UART_STOPBITS_1;huart1.Init.Parity = UART_PARITY_NONE;huart1.Init.Mode = UART_MODE_TX_RX;huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE;huart1.Init.OverSampling = UART_OVERSAMPLING_16;HAL_UART_Init(&huart1);
}static void MX_GPIO_Init(void)
{GPIO_InitTypeDef GPIO_InitStruct = {0};__HAL_RCC_GPIOA_CLK_ENABLE();__HAL_RCC_GPIOC_CLK_ENABLE();// PC13 - LEDGPIO_InitStruct.Pin = GPIO_PIN_13;GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;GPIO_InitStruct.Pull = GPIO_NOPULL;GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_LOW;HAL_GPIO_Init(GPIOC, &GPIO_InitStruct);// PA0 - ADC输入/外部中断GPIO_InitStruct.Pin = GPIO_PIN_0;GPIO_InitStruct.Mode = GPIO_MODE_ANALOG;  // 方案一:ADC模式// GPIO_InitStruct.Mode = GPIO_MODE_IT_RISING; // 方案二:外部中断模式GPIO_InitStruct.Pull = GPIO_NOPULL;HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);// 如果使用外部中断,需要开启中断// HAL_NVIC_SetPriority(EXTI0_IRQn, 0, 0);// HAL_NVIC_EnableIRQ(EXTI0_IRQn);
}void Error_Handler(void)
{while (1){HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);HAL_Delay(100);}
}

四、功能扩展

添加复位功能

// 在main.c中添加
void reset_counter(void)
{counter = 0;printf("计数器已复位\r\n");
}// 在while循环中添加复位检测
if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET) {HAL_Delay(50); // 防抖if (HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET) {reset_counter();while(HAL_GPIO_ReadPin(GPIOB, GPIO_PIN_1) == GPIO_PIN_RESET);}
}

添加OLED显示

// 在platformio.ini中添加OLED库
lib_deps = adafruit/Adafruit SSD1306@^2.5.7adafruit/Adafruit GFX Library@^1.11.3

五、调试技巧

  1. 阈值校准:通过串口打印ADC值,确定合适的检测阈值
  2. 防抖优化:根据实际物体速度调整防抖延时
  3. 灵敏度调整:通过改变红外发射管的电流或接收电路的分压电阻来调整检测距离

六、项目优化方向

  1. 添加LCD/OLED显示屏实时显示计数
  2. 实现数据存储(EEPROM)
  3. 添加无线通信(WiFi/蓝牙)
  4. 实现多通道计数
  5. 添加数据统计功能

这个项目很好地结合了STM32的ADC、定时器、GPIO和串口通信等基本功能,是一个很好的学习实践案例。

功能完整的双向计数器项目

我将为您提供完整的实现方案,包括ESP32代码和PyQt电脑端界面。

一、系统架构设计

ESP32主控制器
├── 红外传感器组(判断方向)
├── 21颗LED组成的三位数码管
├── 两个按键(清零、保存)
├── 蓝牙通信
└── PyQt电脑端显示界面

二、硬件设计

所需元件

  • ESP32开发板
  • 红外对管 × 2组
  • LED数码管 × 3位(共21颗LED)
  • 74HC595移位寄存器 × 3
  • 按键 × 2
  • 电阻:330Ω × 21,10kΩ × 4
  • 面包板和杜邦线

连接方式

红外传感器A:GPIO34 (输入)
红外传感器B:GPIO35 (输入)
按键清零:GPIO25
按键保存:GPIO2674HC595控制:DS (数据): GPIO13SH_CP (时钟): GPIO12ST_CP (锁存): GPIO14蓝牙:ESP32内置

三、ESP32代码实现

platformio.ini配置

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps = adafruit/Adafruit GFX Library@^1.11.3adafruit/Adafruit BusIO@^1.14.1

主程序代码 (src/main.cpp)

#include <Arduino.h>
#include <BluetoothSerial.h>// 蓝牙设置
BluetoothSerial SerialBT;
const char* device_name = "BidirectionalCounter";// 引脚定义
#define SENSOR_A 34
#define SENSOR_B 35
#define BTN_RESET 25
#define BTN_SAVE 26#define DATA_PIN 13    // DS
#define CLOCK_PIN 12   // SH_CP
#define LATCH_PIN 14   // ST_CP// 数码管段码(共阴极)
const byte digitPatterns[10] = {0x3F, // 00x06, // 10x5B, // 20x4F, // 30x66, // 40x6D, // 50x7D, // 60x07, // 70x7F, // 80x6F  // 9
};// 变量定义
volatile int count = 0;
volatile int lastCount = 0;
volatile bool sensorAState = false;
volatile bool sensorBState = false;
volatile bool lastSensorAState = false;
volatile bool lastSensorBState = false;
unsigned long lastDebounceTime = 0;
const unsigned long debounceDelay = 50;// 保存的数据
int savedCounts[10] = {0};
int saveIndex = 0;void updateDisplay(int number) {number = constrain(number, 0, 999);int hundreds = number / 100;int tens = (number % 100) / 10;int units = number % 10;// 发送到三个74HC595digitalWrite(LATCH_PIN, LOW);shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, digitPatterns[units]);shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, digitPatterns[tens]);shiftOut(DATA_PIN, CLOCK_PIN, MSBFIRST, digitPatterns[hundreds]);digitalWrite(LATCH_PIN, HIGH);
}void detectDirection() {bool currentA = digitalRead(SENSOR_A);bool currentB = digitalRead(SENSOR_B);unsigned long currentTime = millis();if (currentTime - lastDebounceTime > debounceDelay) {// 检测A传感器的变化(进入)if (currentA && !lastSensorAState) {if (!currentB) {// A先触发,正向计数count++;SerialBT.println("DIRECTION:FORWARD,COUNT:" + String(count));Serial.printf("正向计数: %d\n", count);}}// 检测B传感器的变化(进入)if (currentB && !lastSensorBState) {if (!currentA) {// B先触发,反向计数count--;if (count < 0) count = 0; // 防止负数SerialBT.println("DIRECTION:BACKWARD,COUNT:" + String(count));Serial.printf("反向计数: %d\n", count);}}lastDebounceTime = currentTime;}lastSensorAState = currentA;lastSensorBState = currentB;
}void saveCurrentCount() {if (saveIndex < 10) {savedCounts[saveIndex] = count;SerialBT.println("SAVED:Index_" + String(saveIndex) + ",Count:" + String(count));Serial.printf("保存计数: 索引%d, 数值%d\n", saveIndex, count);saveIndex++;// 闪烁显示确认for(int i = 0; i < 3; i++) {updateDisplay(888);delay(200);updateDisplay(count);delay(200);}}
}void resetCounter() {count = 0;SerialBT.println("RESET:0");Serial.println("计数器已清零");// 闪烁显示确认for(int i = 0; i < 3; i++) {updateDisplay(0);delay(200);updateDisplay(888);delay(200);}updateDisplay(0);
}void checkButtons() {static unsigned long lastButtonTime = 0;if (millis() - lastButtonTime < 200) return;if (digitalRead(BTN_RESET) == LOW) {resetCounter();lastButtonTime = millis();}if (digitalRead(BTN_SAVE) == LOW) {saveCurrentCount();lastButtonTime = millis();}
}void sendBluetoothData() {if (count != lastCount) {SerialBT.println("COUNT:" + String(count));lastCount = count;}
}void setup() {Serial.begin(115200);// 初始化引脚pinMode(SENSOR_A, INPUT);pinMode(SENSOR_B, INPUT);pinMode(BTN_RESET, INPUT_PULLUP);pinMode(BTN_SAVE, INPUT_PULLUP);pinMode(DATA_PIN, OUTPUT);pinMode(CLOCK_PIN, OUTPUT);pinMode(LATCH_PIN, OUTPUT);// 初始化蓝牙SerialBT.begin(device_name);Serial.println("蓝牙设备已启动: " + String(device_name));Serial.println("等待连接...");// 初始显示updateDisplay(0);Serial.println("系统初始化完成");
}void loop() {detectDirection();checkButtons();sendBluetoothData();updateDisplay(count);delay(10); // 短暂延时
}

四、PyQt电脑端界面

Python代码 (counter_monitor.py)

import sys
import serial
import threading
from datetime import datetime
from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit, QWidget, QFrame,QGridLayout, QGroupBox)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal
from PyQt5.QtGui import QFont, QPalette, QColor
import serial.tools.list_portsclass BluetoothThread(threading.Thread):data_received = pyqtSignal(str)def __init__(self, port_name):super().__init__()self.port_name = port_nameself.running = Trueself.serial_conn = Nonedef run(self):try:self.serial_conn = serial.Serial(self.port_name, 115200, timeout=1)self.data_received.emit("CONNECTED:成功连接到设备")while self.running:if self.serial_conn.in_waiting > 0:data = self.serial_conn.readline().decode('utf-8').strip()if data:self.data_received.emit(data)except Exception as e:self.data_received.emit(f"ERROR:连接失败 - {str(e)}")finally:if self.serial_conn:self.serial_conn.close()def stop(self):self.running = Falseclass CounterWindow(QMainWindow):def __init__(self):super().__init__()self.bt_thread = Noneself.saved_data = []self.init_ui()def init_ui(self):self.setWindowTitle("双向计数器监控系统 - ESP32")self.setGeometry(100, 100, 900, 700)# 中央窗口central_widget = QWidget()self.setCentralWidget(central_widget)layout = QVBoxLayout(central_widget)# 标题title = QLabel("双向计数器监控系统")title.setAlignment(Qt.AlignCenter)title.setFont(QFont("Arial", 20, QFont.Bold))layout.addWidget(title)# 主内容区域content_layout = QHBoxLayout()# 左侧状态面板left_panel = self.create_status_panel()content_layout.addWidget(left_panel)# 右侧数据面板right_panel = self.create_data_panel()content_layout.addWidget(right_panel)layout.addLayout(content_layout)# 连接控制control_layout = QHBoxLayout()self.connect_btn = QPushButton("扫描并连接设备")self.connect_btn.clicked.connect(self.scan_and_connect)control_layout.addWidget(self.connect_btn)self.clear_btn = QPushButton("清空记录")self.clear_btn.clicked.connect(self.clear_log)control_layout.addWidget(self.clear_btn)self.export_btn = QPushButton("导出数据")self.export_btn.clicked.connect(self.export_data)control_layout.addWidget(self.export_btn)layout.addLayout(control_layout)# 日志显示self.log_display = QTextEdit()self.log_display.setMaximumHeight(150)layout.addWidget(self.log_display)self.add_log("系统就绪,请点击'扫描并连接设备'开始")def create_status_panel(self):panel = QGroupBox("实时状态")panel.setMinimumWidth(400)layout = QVBoxLayout(panel)# 计数显示count_frame = QFrame()count_frame.setFrameStyle(QFrame.Box)count_layout = QVBoxLayout(count_frame)count_label = QLabel("当前计数")count_label.setAlignment(Qt.AlignCenter)count_label.setFont(QFont("Arial", 16))self.count_value = QLabel("0")self.count_value.setAlignment(Qt.AlignCenter)self.count_value.setFont(QFont("Arial", 48, QFont.Bold))self.count_value.setStyleSheet("color: blue; background-color: #f0f0f0; border: 2px solid gray;")count_layout.addWidget(count_label)count_layout.addWidget(self.count_value)layout.addWidget(count_frame)# 方向指示direction_frame = QFrame()direction_frame.setFrameStyle(QFrame.Box)direction_layout = QVBoxLayout(direction_frame)direction_label = QLabel("移动方向")direction_label.setAlignment(Qt.AlignCenter)direction_label.setFont(QFont("Arial", 16))self.direction_value = QLabel("等待检测")self.direction_value.setAlignment(Qt.AlignCenter)self.direction_value.setFont(QFont("Arial", 24, QFont.Bold))direction_layout.addWidget(direction_label)direction_layout.addWidget(self.direction_value)layout.addWidget(direction_frame)# 连接状态status_layout = QHBoxLayout()status_layout.addWidget(QLabel("蓝牙状态:"))self.connection_status = QLabel("未连接")self.connection_status.setStyleSheet("color: red; font-weight: bold;")status_layout.addWidget(self.connection_status)status_layout.addStretch()layout.addLayout(status_layout)return paneldef create_data_panel(self):panel = QGroupBox("数据记录")layout = QVBoxLayout(panel)# 保存的数据显示self.saved_data_display = QTextEdit()self.saved_data_display.setFont(QFont("Consolas", 10))layout.addWidget(self.saved_data_display)# 统计信息stats_layout = QGridLayout()stats_layout.addWidget(QLabel("总保存次数:"), 0, 0)self.total_saves = QLabel("0")stats_layout.addWidget(self.total_saves, 0, 1)stats_layout.addWidget(QLabel("最后保存:"), 1, 0)self.last_save = QLabel("无")stats_layout.addWidget(self.last_save, 1, 1)stats_layout.addWidget(QLabel("最大计数值:"), 2, 0)self.max_count = QLabel("0")stats_layout.addWidget(self.max_count, 2, 1)layout.addLayout(stats_layout)return paneldef scan_and_connect(self):ports = serial.tools.list_ports.comports()esp32_ports = []for port in ports:if 'ESP32' in port.description or 'USB' in port.description:esp32_ports.append(port.device)if esp32_ports:port_name = esp32_ports[0]self.connect_to_device(port_name)else:self.add_log("ERROR:未找到ESP32设备,请检查连接")def connect_to_device(self, port_name):if self.bt_thread and self.bt_thread.is_alive():self.bt_thread.stop()self.bt_thread = BluetoothThread(port_name)self.bt_thread.data_received.connect(self.handle_data)self.bt_thread.start()def handle_data(self, data):timestamp = datetime.now().strftime("%H:%M:%S")if data.startswith("CONNECTED:"):self.connection_status.setText("已连接")self.connection_status.setStyleSheet("color: green; font-weight: bold;")self.add_log(f"{timestamp} - {data}")elif data.startswith("ERROR:"):self.connection_status.setText("连接错误")self.connection_status.setStyleSheet("color: red; font-weight: bold;")self.add_log(f"{timestamp} - {data}")elif data.startswith("COUNT:"):count = data.split(":")[1]self.count_value.setText(count)self.update_max_count(int(count))elif data.startswith("DIRECTION:"):direction = "正向" if "FORWARD" in data else "反向"self.direction_value.setText(direction)color = "green" if "FORWARD" in data else "red"self.direction_value.setStyleSheet(f"color: {color};")count = data.split(":")[2]self.add_log(f"{timestamp} - {direction}移动 - 计数: {count}")elif data.startswith("SAVED:"):self.saved_data.append(data)self.update_saved_display()self.add_log(f"{timestamp} - {data}")self.update_stats()elif data.startswith("RESET:"):self.count_value.setText("0")self.direction_value.setText("已清零")self.direction_value.setStyleSheet("color: blue;")self.add_log(f"{timestamp} - 计数器已清零")def update_saved_display(self):display_text = "保存的数据记录:\n" + "="*30 + "\n"for i, data in enumerate(self.saved_data[-10:]):  # 显示最后10条display_text += f"{i+1:2d}. {data}\n"self.saved_data_display.setText(display_text)def update_stats(self):self.total_saves.setText(str(len(self.saved_data)))if self.saved_data:self.last_save.setText(self.saved_data[-1].split(",")[1])def update_max_count(self, count):current_max = int(self.max_count.text())if count > current_max:self.max_count.setText(str(count))def add_log(self, message):self.log_display.append(message)self.log_display.verticalScrollBar().setValue(self.log_display.verticalScrollBar().maximum())def clear_log(self):self.log_display.clear()self.add_log("日志已清空")def export_data(self):try:filename = f"counter_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"with open(filename, 'w', encoding='utf-8') as f:f.write("双向计数器数据导出\n")f.write(f"导出时间: {datetime.now()}\n")f.write("="*50 + "\n")for data in self.saved_data:f.write(data + "\n")self.add_log(f"数据已导出到: {filename}")except Exception as e:self.add_log(f"导出失败: {str(e)}")def closeEvent(self, event):if self.bt_thread and self.bt_thread.is_alive():self.bt_thread.stop()self.bt_thread.join()event.accept()if __name__ == '__main__':app = QApplication(sys.argv)window = CounterWindow()window.show()sys.exit(app.exec_())

五、使用说明

安装Python依赖

pip install pyserial pyqt5

操作流程

  1. 硬件连接:按照电路图连接所有组件
  2. 上传代码:使用PlatformIO将代码上传到ESP32
  3. 运行监控程序:执行 python counter_monitor.py
  4. 连接蓝牙:点击"扫描并连接设备"
  5. 开始计数:物体通过红外传感器时自动计数

功能特点

  • 双向计数:通过两组红外传感器判断方向
  • 实时显示:三位数码管显示当前计数
  • 数据保存:可保存最多10组数据
  • 蓝牙通信:实时传输数据到电脑
  • 图形界面:PyQt界面显示计数、方向、历史数据
  • 数据导出:可将保存的数据导出为文本文件

这个系统完整实现了您要求的所有功能,是一个实用的双向计数解决方案。

好的,我将为您制作一个专业的PyQt界面,用于给红外计数器烧录程序,并支持多设备管理。

一、PyQt烧录工具界面

Python代码 (esp32_flasher_tool.py)

import sys
import os
import subprocess
import json
import threading
from datetime import datetime
from PyQt5.QtWidgets import (QApplication, QMainWindow, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTextEdit, QWidget, QFrame,QGroupBox, QLineEdit, QComboBox, QSpinBox, QCheckBox,QProgressBar, QFileDialog, QMessageBox, QTabWidget,QTableWidget, QTableWidgetItem, QHeaderView, QSplitter)
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QSettings
from PyQt5.QtGui import QFont, QPalette, QColor, QIconclass ESP32FlasherTool(QMainWindow):update_log_signal = pyqtSignal(str)update_progress_signal = pyqtSignal(int)operation_finished_signal = pyqtSignal(bool, str)def __init__(self):super().__init__()self.settings = QSettings("ESP32Counter", "FlasherTool")self.devices = {}self.current_firmware = ""self.flashing_thread = Noneself.init_ui()self.load_settings()def init_ui(self):self.setWindowTitle("ESP32红外计数器烧录工具")self.setGeometry(100, 100, 1200, 800)# 设置窗口图标self.setWindowIcon(QIcon(self.create_icon()))# 中央窗口central_widget = QWidget()self.setCentralWidget(central_widget)main_layout = QVBoxLayout(central_widget)# 创建标签页tab_widget = QTabWidget()main_layout.addWidget(tab_widget)# 烧录页面self.flash_tab = self.create_flash_tab()tab_widget.addTab(self.flash_tab, "固件烧录")# 设备管理页面self.device_tab = self.create_device_tab()tab_widget.addTab(self.device_tab, "设备管理")# 配置页面self.config_tab = self.create_config_tab()tab_widget.addTab(self.config_tab, "工具配置")# 状态栏self.statusBar().showMessage("就绪")# 连接信号self.update_log_signal.connect(self.update_log)self.update_progress_signal.connect(self.update_progress)self.operation_finished_signal.connect(self.operation_finished)def create_flash_tab(self):tab = QWidget()layout = QVBoxLayout(tab)# 顶部控制区域control_group = QGroupBox("烧录控制")control_layout = QVBoxLayout(control_group)# 固件选择firmware_layout = QHBoxLayout()firmware_layout.addWidget(QLabel("固件文件:"))self.firmware_path = QLineEdit()self.firmware_path.setPlaceholderText("选择固件文件(.bin)")firmware_layout.addWidget(self.firmware_path)self.browse_btn = QPushButton("浏览...")self.browse_btn.clicked.connect(self.browse_firmware)firmware_layout.addWidget(self.browse_btn)control_layout.addLayout(firmware_layout)# 设备选择device_layout = QHBoxLayout()device_layout.addWidget(QLabel("目标设备:"))self.device_combo = QComboBox()self.device_combo.currentTextChanged.connect(self.device_selected)device_layout.addWidget(self.device_combo)self.refresh_devices_btn = QPushButton("刷新设备列表")self.refresh_devices_btn.clicked.connect(self.refresh_devices)device_layout.addWidget(self.refresh_devices_btn)control_layout.addLayout(device_layout)# 设备信息显示self.device_info = QLabel("请选择设备")self.device_info.setStyleSheet("background-color: #f0f0f0; padding: 5px; border: 1px solid #ccc;")control_layout.addWidget(self.device_info)# 烧录选项options_layout = QHBoxLayout()self.erase_flash = QCheckBox("擦除整个Flash")options_layout.addWidget(self.erase_flash)self.compress_data = QCheckBox("压缩传输数据")self.compress_data.setChecked(True)options_layout.addWidget(self.compress_data)self.hard_reset = QCheckBox("烧录后硬重启")self.hard_reset.setChecked(True)options_layout.addWidget(self.hard_reset)control_layout.addLayout(options_layout)layout.addWidget(control_group)# 进度区域progress_group = QGroupBox("烧录进度")progress_layout = QVBoxLayout(progress_group)self.progress_bar = QProgressBar()self.progress_bar.setMinimum(0)self.progress_bar.setMaximum(100)progress_layout.addWidget(self.progress_bar)# 操作按钮button_layout = QHBoxLayout()self.flash_btn = QPushButton("开始烧录")self.flash_btn.clicked.connect(self.start_flashing)self.flash_btn.setStyleSheet("QPushButton { background-color: #4CAF50; color: white; font-weight: bold; }")button_layout.addWidget(self.flash_btn)self.stop_btn = QPushButton("停止烧录")self.stop_btn.clicked.connect(self.stop_flashing)self.stop_btn.setEnabled(False)button_layout.addWidget(self.stop_btn)self.verify_btn = QPushButton("验证固件")self.verify_btn.clicked.connect(self.verify_firmware)button_layout.addWidget(self.verify_btn)progress_layout.addLayout(button_layout)layout.addWidget(progress_group)# 日志显示log_group = QGroupBox("烧录日志")log_layout = QVBoxLayout(log_group)self.log_display = QTextEdit()self.log_display.setFont(QFont("Consolas", 9))self.log_display.setMaximumHeight(300)log_layout.addWidget(self.log_display)# 日志控制log_control_layout = QHBoxLayout()self.clear_log_btn = QPushButton("清空日志")self.clear_log_btn.clicked.connect(self.clear_log)log_control_layout.addWidget(self.clear_log_btn)self.save_log_btn = QPushButton("保存日志")self.save_log_btn.clicked.connect(self.save_log)log_control_layout.addWidget(self.save_log_btn)log_control_layout.addStretch()log_layout.addLayout(log_control_layout)layout.addWidget(log_group)return tabdef create_device_tab(self):tab = QWidget()layout = QVBoxLayout(tab)# 设备表格self.device_table = QTableWidget()self.device_table.setColumnCount(6)self.device_table.setHorizontalHeaderLabels(["设备ID", "设备名称", "序列号", "最后烧录", "固件版本", "状态"])self.device_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)layout.addWidget(self.device_table)# 设备操作按钮device_btn_layout = QHBoxLayout()self.add_device_btn = QPushButton("添加设备")self.add_device_btn.clicked.connect(self.add_device)device_btn_layout.addWidget(self.add_device_btn)self.edit_device_btn = QPushButton("编辑设备")self.edit_device_btn.clicked.connect(self.edit_device)device_btn_layout.addWidget(self.edit_device_btn)self.delete_device_btn = QPushButton("删除设备")self.delete_device_btn.clicked.connect(self.delete_device)device_btn_layout.addWidget(self.delete_device_btn)self.export_devices_btn = QPushButton("导出设备列表")self.export_devices_btn.clicked.connect(self.export_devices)device_btn_layout.addWidget(self.export_devices_btn)layout.addLayout(device_btn_layout)return tabdef create_config_tab(self):tab = QWidget()layout = QVBoxLayout(tab)# ESP工具配置tool_group = QGroupBox("ESP工具配置")tool_layout = QVBoxLayout(tool_group)# esptool路径esptool_layout = QHBoxLayout()esptool_layout.addWidget(QLabel("esptool路径:"))self.esptool_path = QLineEdit()self.esptool_path.setText(self.settings.value("esptool_path", "esptool"))esptool_layout.addWidget(self.esptool_path)self.browse_esptool_btn = QPushButton("浏览...")self.browse_esptool_btn.clicked.connect(self.browse_esptool)esptool_layout.addWidget(self.browse_esptool_btn)tool_layout.addLayout(esptool_layout)# 串口配置serial_layout = QHBoxLayout()serial_layout.addWidget(QLabel("默认串口:"))self.default_port = QLineEdit()self.default_port.setText(self.settings.value("default_port", "COM3"))serial_layout.addWidget(self.default_port)serial_layout.addWidget(QLabel("波特率:"))self.baud_rate = QComboBox()self.baud_rate.addItems(["115200", "230400", "460800", "921600"])self.baud_rate.setCurrentText(self.settings.value("baud_rate", "115200"))serial_layout.addWidget(self.baud_rate)tool_layout.addLayout(serial_layout)# Flash配置flash_layout = QHBoxLayout()flash_layout.addWidget(QLabel("Flash模式:"))self.flash_mode = QComboBox()self.flash_mode.addItems(["dio", "qio", "dout", "qout"])self.flash_mode.setCurrentText(self.settings.value("flash_mode", "dio"))flash_layout.addWidget(self.flash_mode)flash_layout.addWidget(QLabel("Flash大小:"))self.flash_size = QComboBox()self.flash_size.addItems(["4MB", "8MB", "16MB"])self.flash_size.setCurrentText(self.settings.value("flash_size", "4MB"))flash_layout.addWidget(self.flash_size)flash_layout.addWidget(QLabel("Flash频率:"))self.flash_freq = QComboBox()self.flash_freq.addItems(["40m", "80m"])self.flash_freq.setCurrentText(self.settings.value("flash_freq", "80m"))flash_layout.addWidget(self.flash_freq)tool_layout.addLayout(flash_layout)layout.addWidget(tool_group)# 应用程序配置app_group = QGroupBox("应用程序配置")app_layout = QVBoxLayout(app_group)# 设备ID配置id_layout = QHBoxLayout()id_layout.addWidget(QLabel("设备ID起始值:"))self.device_id_start = QSpinBox()self.device_id_start.setMinimum(1)self.device_id_start.setMaximum(1000)self.device_id_start.setValue(self.settings.value("device_id_start", 1, type=int))id_layout.addWidget(self.device_id_start)app_layout.addLayout(id_layout)# 计数器配置counter_layout = QHBoxLayout()counter_layout.addWidget(QLabel("初始计数值:"))self.initial_count = QSpinBox()self.initial_count.setMinimum(0)self.initial_count.setMaximum(999)self.initial_count.setValue(self.settings.value("initial_count", 0, type=int))counter_layout.addWidget(self.initial_count)app_layout.addLayout(counter_layout)layout.addWidget(app_group)# 保存配置按钮save_config_btn = QPushButton("保存配置")save_config_btn.clicked.connect(self.save_config)save_config_btn.setStyleSheet("QPushButton { background-color: #2196F3; color: white; }")layout.addWidget(save_config_btn)layout.addStretch()return tabdef browse_firmware(self):file_path, _ = QFileDialog.getOpenFileName(self, "选择固件文件", "", "Binary Files (*.bin);;All Files (*)")if file_path:self.firmware_path.setText(file_path)self.current_firmware = file_pathdef browse_esptool(self):file_path, _ = QFileDialog.getOpenFileName(self, "选择esptool", "", "Python Files (*.py);;Executable Files (*.exe);;All Files (*)")if file_path:self.esptool_path.setText(file_path)def refresh_devices(self):self.update_log_signal.emit("扫描可用设备...")# 这里可以添加自动扫描串口设备的逻辑self.load_devices_from_settings()def load_devices_from_settings(self):self.devices = self.settings.value("devices", {})self.device_combo.clear()for device_id, device_info in self.devices.items():self.device_combo.addItem(f"{device_id}: {device_info.get('name', 'Unknown')}", device_id)def device_selected(self):device_id = self.device_combo.currentData()if device_id and device_id in self.devices:device_info = self.devices[device_id]info_text = f"""
设备名称: {device_info.get('name', 'N/A')}
设备序列号: {device_info.get('serial', 'N/A')}
最后烧录: {device_info.get('last_flash', '从未')}
固件版本: {device_info.get('firmware', 'N/A')}
端口: {device_info.get('port', 'N/A')}""".strip()self.device_info.setText(info_text)def start_flashing(self):if not self.current_firmware:QMessageBox.warning(self, "警告", "请先选择固件文件")returndevice_id = self.device_combo.currentData()if not device_id:QMessageBox.warning(self, "警告", "请先选择目标设备")returnself.flash_btn.setEnabled(False)self.stop_btn.setEnabled(True)self.progress_bar.setValue(0)# 启动烧录线程self.flashing_thread = FlashingThread(self.current_firmware,self.devices[device_id],self.get_flash_options())self.flashing_thread.update_log.connect(self.update_log_signal.emit)self.flashing_thread.update_progress.connect(self.update_progress_signal.emit)self.flashing_thread.finished.connect(self.flashing_finished)self.flashing_thread.start()def stop_flashing(self):if self.flashing_thread and self.flashing_thread.isRunning():self.flashing_thread.stop()def verify_firmware(self):# 固件验证逻辑self.update_log_signal.emit("开始验证固件...")def update_log(self, message):timestamp = datetime.now().strftime("%H:%M:%S")self.log_display.append(f"[{timestamp}] {message}")self.log_display.verticalScrollBar().setValue(self.log_display.verticalScrollBar().maximum())def update_progress(self, value):self.progress_bar.setValue(value)def operation_finished(self, success, message):if success:self.statusBar().showMessage("操作完成")QMessageBox.information(self, "成功", message)else:self.statusBar().showMessage("操作失败")QMessageBox.critical(self, "错误", message)def flashing_finished(self):self.flash_btn.setEnabled(True)self.stop_btn.setEnabled(False)def clear_log(self):self.log_display.clear()def save_log(self):file_path, _ = QFileDialog.getSaveFileName(self, "保存日志", f"flash_log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt", "Text Files (*.txt)")if file_path:try:with open(file_path, 'w', encoding='utf-8') as f:f.write(self.log_display.toPlainText())self.update_log_signal.emit(f"日志已保存到: {file_path}")except Exception as e:QMessageBox.critical(self, "错误", f"保存日志失败: {str(e)}")def add_device(self):# 添加设备对话框from PyQt5.QtWidgets import QDialog, QFormLayout, QDialogButtonBoxdialog = QDialog(self)dialog.setWindowTitle("添加新设备")layout = QFormLayout(dialog)device_id_edit = QLineEdit()device_id_edit.setText(str(self.device_id_start.value()))layout.addRow("设备ID:", device_id_edit)device_name_edit = QLineEdit()device_name_edit.setPlaceholderText("输入设备名称")layout.addRow("设备名称:", device_name_edit)serial_edit = QLineEdit()serial_edit.setPlaceholderText("设备序列号")layout.addRow("序列号:", serial_edit)port_edit = QLineEdit()port_edit.setText(self.default_port.text())layout.addRow("串口:", port_edit)buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)buttons.accepted.connect(dialog.accept)buttons.rejected.connect(dialog.reject)layout.addRow(buttons)if dialog.exec_() == QDialog.Accepted:device_id = device_id_edit.text()device_info = {'name': device_name_edit.text(),'serial': serial_edit.text(),'port': port_edit.text(),'last_flash': '从未','firmware': '未知'}self.devices[device_id] = device_infoself.save_devices()self.refresh_devices()def edit_device(self):# 编辑设备逻辑passdef delete_device(self):current_row = self.device_table.currentRow()if current_row >= 0:device_id = self.device_table.item(current_row, 0).text()reply = QMessageBox.question(self, "确认删除", f"确定要删除设备 {device_id} 吗?")if reply == QMessageBox.Yes:del self.devices[device_id]self.save_devices()self.refresh_devices()def export_devices(self):file_path, _ = QFileDialog.getSaveFileName(self, "导出设备列表", f"devices_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json", "JSON Files (*.json)")if file_path:try:with open(file_path, 'w', encoding='utf-8') as f:json.dump(self.devices, f, indent=2, ensure_ascii=False)self.update_log_signal.emit(f"设备列表已导出到: {file_path}")except Exception as e:QMessageBox.critical(self, "错误", f"导出失败: {str(e)}")def get_flash_options(self):return {'erase_flash': self.erase_flash.isChecked(),'compress': self.compress_data.isChecked(),'hard_reset': self.hard_reset.isChecked(),'flash_mode': self.flash_mode.currentText(),'flash_size': self.flash_size.currentText(),'flash_freq': self.flash_freq.currentText(),'port': self.default_port.text(),'baud': self.baud_rate.currentText()}def save_config(self):self.settings.setValue("esptool_path", self.esptool_path.text())self.settings.setValue("default_port", self.default_port.text())self.settings.setValue("baud_rate", self.baud_rate.currentText())self.settings.setValue("flash_mode", self.flash_mode.currentText())self.settings.setValue("flash_size", self.flash_size.currentText())self.settings.setValue("flash_freq", self.flash_freq.currentText())self.settings.setValue("device_id_start", self.device_id_start.value())self.settings.setValue("initial_count", self.initial_count.value())QMessageBox.information(self, "成功", "配置已保存")def save_devices(self):self.settings.setValue("devices", self.devices)def load_settings(self):self.load_devices_from_settings()def create_icon(self):# 创建一个简单的图标from PyQt5.QtGui import QPixmap, QPainterpixmap = QPixmap(32, 32)pixmap.fill(Qt.transparent)painter = QPainter(pixmap)painter.setRenderHint(QPainter.Antialiasing)painter.setBrush(QColor(66, 133, 244))painter.drawEllipse(2, 2, 28, 28)painter.setPen(Qt.white)painter.drawText(8, 22, "ESP")painter.end()return pixmapclass FlashingThread(threading.Thread):update_log = pyqtSignal(str)update_progress = pyqtSignal(int)def __init__(self, firmware_path, device_info, flash_options):super().__init__()self.firmware_path = firmware_pathself.device_info = device_infoself.flash_options = flash_optionsself._stop_event = threading.Event()def stop(self):self._stop_event.set()def run(self):try:self.update_log.emit(f"开始烧录设备: {self.device_info.get('name', 'Unknown')}")# 构建esptool命令cmd = [self.flash_options.get('esptool_path', 'esptool'),'--port', self.device_info.get('port', 'COM3'),'--baud', self.flash_options.get('baud', '115200'),'--after', 'hard_reset' if self.flash_options.get('hard_reset') else 'no_reset']if self.flash_options.get('compress'):cmd.append('--compress')# 添加擦除选项if self.flash_options.get('erase_flash'):cmd.extend(['erase_flash'])self.update_log.emit("擦除Flash...")# 执行擦除命令# 添加烧录命令cmd.extend(['write_flash','0x1000',self.firmware_path])# 添加Flash配置cmd.extend(['--flash_mode', self.flash_options.get('flash_mode', 'dio'),'--flash_size', self.flash_options.get('flash_size', '4MB'),'--flash_freq', self.flash_options.get('flash_freq', '80m')])self.update_log.emit(f"执行命令: {' '.join(cmd)}")# 模拟烧录过程for i in range(101):if self._stop_event.is_set():self.update_log.emit("烧录已取消")returnself.update_progress.emit(i)threading.Event().wait(0.05)  # 模拟烧录时间self.update_log.emit("烧录完成!")except Exception as e:self.update_log.emit(f"烧录错误: {str(e)}")if __name__ == '__main__':app = QApplication(sys.argv)# 设置应用程序样式app.setStyle('Fusion')window = ESP32FlasherTool()window.show()sys.exit(app.exec_())

二、设备配置文件生成器

Python代码 (config_generator.py)

import json
import argparse
from datetime import datetimeclass DeviceConfigGenerator:def __init__(self):self.config_template = {"device_id": 1,"device_name": "IR_Counter_001","max_count": 999,"sensor_pins": {"sensor_a": 34,"sensor_b": 35},"display_pins": {"data": 13,"clock": 12,"latch": 14},"button_pins": {"reset": 25,"save": 26},"bluetooth_name": "IR_Counter_001","initial_count": 0,"created_time": ""}def generate_config(self, device_id, device_name, initial_count=0):config = self.config_template.copy()config["device_id"] = device_idconfig["device_name"] = device_nameconfig["bluetooth_name"] = f"IR_Counter_{device_id:03d}"config["initial_count"] = initial_countconfig["created_time"] = datetime.now().isoformat()return configdef save_config(self, config, output_path):with open(output_path, 'w', encoding='utf-8') as f:json.dump(config, f, indent=2, ensure_ascii=False)print(f"配置文件已生成: {output_path}")def generate_for_device(self, device_id, device_name, initial_count=0, output_dir="configs"):import osos.makedirs(output_dir, exist_ok=True)config = self.generate_config(device_id, device_name, initial_count)output_path = os.path.join(output_dir, f"device_{device_id:03d}.json")self.save_config(config, output_path)return configif __name__ == "__main__":parser = argparse.ArgumentParser(description="生成设备配置文件")parser.add_argument("--device-id", type=int, required=True, help="设备ID")parser.add_argument("--device-name", required=True, help="设备名称")parser.add_argument("--initial-count", type=int, default=0, help="初始计数值")parser.add_argument("--output-dir", default="configs", help="输出目录")args = parser.parse_args()generator = DeviceConfigGenerator()generator.generate_for_device(args.device_id,args.device_name,args.initial_count,args.output_dir)

三、使用说明

安装依赖

pip install PyQt5 pyserial

功能特点

  1. 多设备管理

    • 为每个设备分配唯一ID和序列号
    • 设备信息持久化存储
    • 设备状态跟踪
  2. 固件烧录

    • 支持esptool命令参数配置
    • 实时烧录进度显示
    • 详细的烧录日志
  3. 配置管理

    • 灵活的烧录参数配置
    • 设备特定配置生成
    • 配置导入导出
  4. 专业界面

    • 标签页组织功能
    • 实时状态显示
    • 操作日志记录

使用流程

  1. 配置工具

    • 在"工具配置"页面设置esptool路径和串口参数
    • 配置Flash参数和设备ID起始值
  2. 添加设备

    • 在"设备管理"页面添加新设备
    • 为每个设备设置唯一ID和序列号
  3. 烧录固件

    • 选择固件文件和目标设备
    • 配置烧录选项
    • 开始烧录并监控进度
  4. 验证结果

    • 查看烧录日志确认成功
    • 设备信息自动更新

这个工具可以确保每个计数器都有正确的设备ID和配置,避免数据混淆,非常适合批量生产环境使用。

http://www.dtcms.com/a/504145.html

相关文章:

  • 做网站如何大众汽车网站建设
  • 【Linux笔记】网络部分——应用层自定义协议与序列化
  • 上海招聘网站排名米方科技网站建设
  • 佛山网站建设企业推荐房地产交易网站模版
  • 江苏和住房建设厅网站深圳网站关键词
  • Qt--命名,快捷键及坐标系
  • 容器:软件世界的标准集装箱
  • 音乐网站系统源码百度引擎搜索引擎入口
  • 门户网站如何制作想学习做网站
  • 建设项目安监备案网站深圳公司贷款
  • 企业网站关键词应如何优化网站建设公司swot分析
  • 09_AI智能体开发环境搭建之Redis安装配置完整指南
  • Oracle RMAN三种不完全恢复实战详解:归档序号、时间点与SCN恢复对比
  • 公司网站托管网站做5级分销合法吗
  • 记事本做网站如何添加图片开发公司空置房物管费归口什么费用
  • 新网站建设渠道打开网页链接
  • Python 爬虫常用库:requests 与 BeautifulSoup 详解
  • 什么是MySQL JOIN查询的驱动表和被驱动表?
  • 网站推广服务费计入什么科目自适应网站开发文字大小如何处理
  • minio 数据库迁移
  • 佛山网站设计实力乐云seo规划电子商务网站建设方案
  • 大文件分片上传:简单案例(前端切割与后端合并)
  • 门户网站是网络表达吗山东国舜建设集团网站
  • dw网站建设字体颜色app网页设计网站
  • C++ vector类的模拟实现
  • 踏云wordpress主题移动建站优化
  • 做网站通过什么挣钱手机微网站建设方案
  • 达梦数据库的命名空间
  • [嵌入式系统-154]:各种工业现场总线比较
  • 苏州网站网站建设广东微信网站制作多少钱