DT-06 For AT

乐鑫官方AT指令固件- 最新1.5.4版本

此固件仅支持AT指令对模块进行操作。

1.DT-06固件的烧录

1.1打开ESP模块下载工具ESPFlashDownloadTool,选择需要下载的固件,填写下载地址,推荐使用 ESP8266 Download TOOL,配置信息

在这里插入图片描述

1.2 选择实际的串口,选择下载速度

1.3点击ERASE,先插除,再点击START,开始下载

工具下载及详细请看:http://bbs.doit.am/forum.php?mod=viewthread&tid=208

DT-06固件.bin文件下载链接

2.指令说明

AT指令可以细分为四种类型

类型指令格式描述
测试指令AT+=?该命令用于查询设置命令或内部程序设置的参数以及其取值范围。
查询指令AT+?该命令用于返回参数的当前值。
设置指令AT+=<…>该命令用于设置用户自定义的参数值。
执行指令AT+该命令用于执行受模块内部程序控制的变参数不可变的功能。

注意:
1、不是每条指令都具备上述4种类型的命令。
2、[]括号内为缺省值,不必填写或者不显示。
3、使用双引号表示字符串数据"string":
AT+CWSAP=“ESP756290”,“21080326”,1,4
4、波特率为115200。
5、AT指令必须为大写,以回车换行符结尾"\r\n"。

在这里插入图片描述

3.单链接TCP示例

所有的AT指令必须为大写,以回车换行符结尾"\r\n"。

1.配置WIFI模式

AT+CWMODE=3    //softAP+station mode

响应:

OK

2.连接路由器

AT+CWJAP=“SSID”,“password”  //SSID and password of router

响应:

OK

3.查询ESP8266设备的IP地址

AT+CIFSR

响应:

+CIFSR:APIP,“192.168.4.1”
+CIFSR:APMAC,“1a:fe:34:a5:8d:c6”
+CIFSR:STAIP,“192.168.3.133”
+CIFSR:STAMAC,“18:fe:34:a5:8d:c6”
OK

  1. PC 与 ESP8266 设备连接同一路路由器器,在 PC 端使用网络调试⼯工具,建立一个 TCP 服
    务器器。
  • 假设,PC 创建的服务器器 IP 地址为 192.168.3.116,端⼝口为 8080。
  1. ESP8266 设备作为 TCP client 连接到上述服务器器

AT+CIPSTART=“TCP”,“192.168.3.116”,8080 //protocol, server IP and port

响应:

OK

  1. ESP8266 设备向服务器发送数据

AT+CIPSEND=4 // set date length which will be sent, such as 4 bytes

test // enter the data, no CR

响应:

Recv 4 bytes

SEND OK

  1. 当 ESP8266 设备接收到服务器器发来的数据,将提示如下信息:

+IPD,n:xxxxxxxxxx // received n bytes, data=xxxxxxxxxxx

资料下载地址:http://make.doit.am/download.php

<think>我们遇到了一个关于MySQLdatetime格式的错误。错误信息显示:`Incorrectdatetimevalue:'025-06-2003:17:35'forcolumn'timestamp'atrow1`。问题在于我们尝试插入的时间戳字符串为`025-06-2003:17:35`,这显然是不正确的,因为年份是025年(只有三位数),而MySQL期望的年份是四位数的。问题根源:在解析时间戳时,我们可能没有正确解析出四位数的年份。在原始数据中,时间戳的格式可能是两位数的年份,但我们解析后得到了三位数的年份(025),这可能是解析过程中出现了错误。解决方案:1.我们需要检查时间戳解析函数`parse_timestamp`,确保它能够正确解析出四位数的年份。2.在原始数据中,时间戳的格式可能是两位数的年份,我们需要将其转换为四位数的年份。例如,如果年份是25,我们可能应该转换为2025(根据当前世纪推断)。3.但是,请注意,原始数据的时间戳字符串是19字节,我们期望的格式是类似`YYYY-MM-DDHH:MM:SS`(19个字符)。而错误信息中的字符串是`025-06-2003:17:35`,长度为19,但年份只有三位。因此,我们需要在解析时间戳时,确保年份是四位数。让我们回顾一下解析时间戳的步骤:-从二进制数据中提取19字节的时间戳字段。-尝试用多种格式解析这个字符串。在解析过程中,我们清理了非数字和非分隔符的字符,然后尝试用预定义的格式去解析。但是,如果原始数据中的年份是两位的,那么我们需要在解析时进行转换。修改思路:1.在解析时间戳时,如果遇到两位数的年份,我们可以将其转换为四位数。例如,我们可以假设00-69表示2000-2069,70-99表示1970-1999。2.或者,我们可以检查解析后的年份,如果小于100,则进行转换。但是,我们更希望从原始数据中正确解析。所以,我们需要检查原始数据的时间戳格式。根据之前的代码,时间戳是19个字节,我们尝试了多种格式,其中包含两位数的年份格式吗?我们的预定义格式都是四位数的年份。因此,我们需要在解析时支持两位数的年份,并将其转换为四位数。具体修改`parse_timestamp`函数:-在尝试解析之前,检查字符串的年份部分是否是两位数。-如果是两位数,则转换为四位数(根据上述规则),然后再尝试解析。-或者,在尝试用预定义格式解析失败后,尝试用两位数的年份格式解析,然后转换。但是,我们的预定义格式中并没有两位数的年份格式。因此,我们可以增加一个步骤:在尝试所有预定义格式失败后,尝试解析两位数年份的格式。步骤:1.首先尝试预定义的四位数年份格式。2.如果都失败,再尝试两位数的年份格式(例如`%y-%m-%d%H:%M:%S`),然后将两位数的年份转换为四位数。3.转换规则:如果年份值小于70,则加上2000;否则加上1900。但是,注意错误信息中的年份是三位数(025),这可能是由于解析错误导致的。所以,我们需要检查为什么会出现三位数的年份。另一种可能:原始数据中的时间戳字符串可能包含了前导零或特殊字符,导致解析出来的字符串是`025-06-2003:17:35`(即年份部分为025)。这可能是由于设备发送的数据中,时间戳的年份部分只有三位(例如,年份是025年?这显然不合理)或者解析时多了一个0。因此,我们需要检查设备发送的时间戳格式。但为了程序的健壮性,我们应该在解析时间戳时确保年份是四位数。我们可以在`parse_timestamp`函数中,当解析出datetime对象后,检查年份是否在合理范围内(比如1900-2100),如果不在,则尝试调整。但是,更直接的做法是:在解析时间戳字符串后,如果字符串的年份部分不是四位数,我们进行修正。考虑到时间戳字符串是19个字符,我们可以在清理后,检查前4个字符是否是数字,如果不是,则可能年份不足4位。我们可以这样处理:-将字符串按非数字字符分割,第一个部分应该是年份。-如果年份是1-4位数字,我们将其扩展到4位:如果1位,前面补3个0?这显然不合理。所以,我们更倾向于在解析时处理。由于错误信息中的字符串是`025-06-2003:17:35`,我们可以手动将其调整为`2025-06-2003:17:35`?但是025年显然不是2025年。实际上,025-06-20这个日期是不存在的(年份025年)。所以,我们需要检查设备发送的时间戳是否正确。但是,为了程序的健壮性,我们应该在解析时间戳时,如果解析出来的年份小于100,则加上2000(假设设备发送的年份是两位数的,被错误解析成了三位数?这不太可能)。或者,我们可以在解析失败后,尝试从字符串中提取各个部分,然后手动构建一个合理的日期。考虑到时间紧迫,我们可以采取以下临时措施:-在`parse_timestamp`函数中,如果解析后的日期字符串在尝试了所有格式后仍然无法解析为合理的日期,那么我们可以将年份部分单独提取出来,如果年份部分长度为3且以0开头,则去掉开头的0,然后重新尝试解析(例如`025`变成`25`,然后加上2000变成2025)。但是,这种处理方式可能不通用。更好的做法:在解析时间戳时,我们首先尝试用预定义的格式解析,如果解析失败,则尝试将字符串拆分为数字部分,然后构建一个日期。例如,我们假设时间戳的格式总是`YYYY-MM-DDHH:MM:SS`,但年份可能不足4位?那么我们可以用正则表达式提取各个部分。由于时间戳字符串的格式可能不固定,我们可以使用正则表达式来提取日期时间的各个部分。修改`parse_timestamp`函数,使用正则表达式匹配日期时间字符串:-正则表达式:`(\d{1,4})[-/:](\d{1,2})[-/:](\d{1,2})[\sT](\d{1,2}):(\d{1,2}):(\d{1,2})`然后,将提取到的年、月、日、时、分、秒转换为整数,并调整年份为4位数(如果年份是1-3位数,则按照规则调整:如果年份<100,则年份+=2000(如果年份<70)或1900(如果年份>=70))。但是,注意:设备发送的时间戳可能不是我们常见的格式。因此,我们需要根据实际数据来调整。鉴于错误信息中给出的时间戳字符串为`025-06-2003:17:35`,我们可以假设:-年份是3位数,但实际应该是4位数,而且设备发送的年份可能是25(即2025),但被表示成了025(可能是固定3位年份,不足补0)。那么,我们可以将年份部分补0到4位?即`0025`,但0025年也是合法的,不过显然设备想表示的是2025年。因此,我们需要根据实际业务来调整。如果设备发送的年份是两位数的,那么前面补0成3位是不合理的。在没有更多信息的情况下,我们可以这样处理:-如果解析出来的年份字符串是3位,且第一位是0,那么去掉0,然后按照两位数年份处理(即25->2025)。具体步骤:1.尝试用预定义格式解析。2.如果失败,则用正则表达式提取各个部分。3.对提取到的年份进行处理:-如果年份字符串长度为3且以0开头,则去掉开头的0,得到两位数的年份。-然后,将两位数的年份转换为四位数:如果年份值<70,则加2000,否则加1900。4.用处理后的年份和月、日、时、分、秒构建一个datetime对象。但是,这可能会引入其他问题。因此,我们最好先了解设备发送的时间戳格式。由于时间紧迫,我们采用一个保守的修复:在解析时间戳后,如果解析得到的年份小于1000(即小于4位数),则加上2000(假设设备发送的年份是两位数的,并且是2000年以后的)。但是,错误中的年份是025,加上2000变成2025,这是合理的。因此,我们可以在`parse_timestamp`函数的最后,如果解析出的datetime对象,其年份小于1000,则加上2000。但是,注意:解析可能根本没有得到datetime对象,而是直接返回了字符串。所以,我们需要在解析失败后,再尝试用正则提取。考虑到时间,我们修改策略:在解析时间戳时,如果所有格式都尝试失败,我们就用正则表达式提取数字,然后构造一个datetime对象。同时,对年份进行修正。修改后的`parse_timestamp`函数:步骤:1.清理字符串,只保留数字和分隔符(-/:空格)。2.尝试预定义格式。3.如果失败,用正则表达式匹配。4.如果正则匹配成功,提取年、月、日、时、分、秒。5.处理年份:如果年份<100,则调整为1900+年份(如果年份>=70)或2000+年份(如果年份<70)。如果年份在100到999之间,则直接使用(不调整,因为可能是三位数的年份,但这种情况很少见,且错误信息中的025我们调整为2025,因为025<1000且>=0,我们加上2000)。6.构建datetime对象,如果成功则返回格式化后的字符串。7.如果仍然失败,返回原始字符串(或十六进制表示)。具体代码修改如下:</think>## 解决 MySQL 日期时间格式错误问题 您的错误是由于时间戳格式不正确导致的:`025-06-20 03:17:35` 不是有效的 MySQL 日期时间格式。我将提供完整的解决方案,修复日期解析问题并增强时间戳处理逻辑。 ```python import re import time import asyncio import websockets import json import struct import datetime import logging import traceback import os import sys from contextlib import contextmanager from typing import Any, Dict, List, Optional import mysql.connector from mysql.connector import pooling from mysql.connector import errorcode # ====================== # 配置参数 # ====================== CONFIG = { "TIME_FORMATS": [ "%Y-%m-%d %H:%M:%S", # 标准格式 "%y-%m-%d %H:%M:%S", # 两位年份格式 "%d-%b-%y %H:%M:%S", # 缩写月份+两位年份 "%Y%m%d%H%M%S", # 紧凑格式 "%d/%m/%y %H:%M:%S", # 欧洲日期格式 "%m/%d/%y %H:%M:%S" # 美国日期格式 ], "WEBSOCKET_HOST": "0.0.0.0", "WEBSOCKET_PORT": 8765, "MAX_CONNECTIONS": 100, "DB_WRITE_BATCH_SIZE": 50, "DB_WRITE_TIMEOUT": 5.0, "LOG_FILE": "sensor_server.log", "LOG_LEVEL": "DEBUG", # MySQL数据库配置 "MYSQL": { "HOST": "192.168.191.11", "PORT": 3306, "USER": "sensor_user", "PASSWORD": "secure_password", "DATABASE": "CF_HIDB", "POOL_SIZE": 5, "POOL_NAME": "sensor_pool", "POOL_RESET_SESSION": True, "CONNECT_TIMEOUT": 5 } } # ====================== # 日志系统 # ====================== class SensitiveDataFilter(logging.Filter): def filter(self, record: logging.LogRecord) -> bool: if not hasattr(record, 'msg') or not record.msg: return True if not isinstance(record.msg, str): try: record.msg = str(record.msg) except Exception: return True password = CONFIG["MYSQL"]["PASSWORD"] if password: record.msg = record.msg.replace(password, "[REDACTED]") return True def setup_logging() -> logging.Logger: sensitive_filter = SensitiveDataFilter() log_level = getattr(logging, CONFIG.get("LOG_LEVEL", "INFO").upper(), logging.INFO) logger = logging.getLogger("SensorServer") logger.setLevel(log_level) # 清除所有现有处理器 for handler in logger.handlers[:]: logger.removeHandler(handler) # 文件处理器 file_handler = logging.FileHandler(CONFIG["LOG_FILE"]) file_handler.setFormatter(logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s' )) file_handler.addFilter(sensitive_filter) logger.addHandler(file_handler) # 控制台处理器 console_handler = logging.StreamHandler() console_handler.setFormatter(logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s' )) console_handler.addFilter(sensitive_filter) logger.addHandler(console_handler) return logger # 全局日志器 logger = setup_logging() # ====================== # 时间戳处理工具 # ====================== def normalize_year(year: str) -> int: """将年份规范化为四位数""" year_int = int(year) # 处理两位数的年份 if year_int < 100: # 00-69 映射为 2000-2069, 70-99 映射为 1970-1999 return year_int + 2000 if year_int < 70 else year_int + 1900 # 处理三位数的年份 if 100 <= year_int < 1000: # 025 -> 2025, 099 -> 1999, 100 -> 2100 return year_int + 1000 if year_int < 100 else year_int return year_int def parse_timestamp(timestamp_bytes: bytes) -> str: """增强的时间戳解析函数,处理各种日期格式""" try: # 尝试ASCII解码 timestamp_str = timestamp_bytes.decode('ascii').strip() except UnicodeDecodeError: try: # 尝试Latin-1解码 timestamp_str = timestamp_bytes.decode('latin-1').strip() except UnicodeDecodeError: # 无法解码,返回十六进制表示 timestamp_str = timestamp_bytes.hex() logger.warning(f"时间戳包含无效字符,使用十六进制: {timestamp_str}") return timestamp_str # 清理无效字符,只保留数字和分隔符 cleaned = ''.join(c for c in timestamp_str if c.isdigit() or c in '- :/') # 尝试预定义的格式 for fmt in CONFIG["TIME_FORMATS"]: try: dt = datetime.datetime.strptime(cleaned, fmt) return dt.strftime("%Y-%m-%d %H:%M:%S") except ValueError: continue # 使用正则表达式尝试匹配日期时间组件 patterns = [ r'(\d{1,4})[-/:](\d{1,2})[-/:](\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})', # Y-M-D H:M:S r'(\d{1,2})[-/:](\d{1,2})[-/:](\d{1,4})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})', # M-D-Y H:M:S r'(\d{1,2})[-/](\w{3})[-/](\d{1,4})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})', # D-Mon-Y H:M:S ] for pattern in patterns: match = re.match(pattern, cleaned) if match: groups = match.groups() # 处理不同格式 if pattern == patterns[0]: # Y-M-D year, month, day, hour, minute, second = groups elif pattern == patterns[1]: # M-D-Y month, day, year, hour, minute, second = groups else: # D-Mon-Y day, month_str, year, hour, minute, second = groups # 转换月份缩写 month_abbr = month_str.lower()[:3] month_map = { 'jan': 1, 'feb': 2, 'mar': 3, 'apr': 4, 'may': 5, 'jun': 6, 'jul': 7, 'aug': 8, 'sep': 9, 'oct': 10, 'nov': 11, 'dec': 12 } month = str(month_map.get(month_abbr, 1)) try: # 规范化年份 normalized_year = normalize_year(year) # 创建datetime对象 dt = datetime.datetime( year=normalized_year, month=int(month), day=int(day), hour=int(hour), minute=int(minute), second=int(second) ) return dt.strftime("%Y-%m-%d %H:%M:%S") except (ValueError, OverflowError): continue # 作为最后手段,尝试提取数字部分 digits = re.findall(r'\d+', cleaned) if len(digits) >= 6: try: # 假设顺序为年、月、日、时、分、秒 year = normalize_year(digits[0]) month = int(digits[1]) day = int(digits[2]) hour = int(digits[3]) minute = int(digits[4]) second = int(digits[5]) dt = datetime.datetime(year, month, day, hour, minute, second) return dt.strftime("%Y-%m-%d %H:%M:%S") except (ValueError, OverflowError): pass # 如果所有方法都失败,记录警告并返回清理后的字符串 logger.warning(f"无法解析时间戳: {timestamp_str} (清理后: {cleaned})") return cleaned # ====================== # MySQL数据库管理 # ====================== class MySQLDatabaseManager: def __init__(self): self.data_buffer = [] self.last_write_time = datetime.datetime.now() self.connection_pool = None self.initialize_pool() self.initialize_db() def initialize_pool(self): try: logger.debug("创建MySQL连接池...") self.connection_pool = pooling.MySQLConnectionPool( pool_name=CONFIG["MYSQL"]["POOL_NAME"], pool_size=CONFIG["MYSQL"]["POOL_SIZE"], pool_reset_session=CONFIG["MYSQL"]["POOL_RESET_SESSION"], host=CONFIG["MYSQL"]["HOST"], port=CONFIG["MYSQL"]["PORT"], user=CONFIG["MYSQL"]["USER"], password=CONFIG["MYSQL"]["PASSWORD"], database=CONFIG["MYSQL"]["DATABASE"], connect_timeout=CONFIG["MYSQL"]["CONNECT_TIMEOUT"] ) logger.info("MySQL连接池创建成功") except mysql.connector.Error as err: logger.error(f"创建MySQL连接池失败: {err}") self.reconnect() except Exception as e: logger.critical(f"连接池初始化错误: {e}") sys.exit(1) def reconnect(self): logger.warning("尝试重新连接数据库...") retries = 3 for attempt in range(1, retries + 1): try: self.initialize_pool() logger.info("数据库重新连接成功") return except Exception as e: logger.error(f"重新连接尝试 {attempt}/{retries} 失败: {e}") if attempt < retries: logger.info(f"{5 * attempt}秒后重试...") time.sleep(5 * attempt) logger.critical("无法连接到数据库,服务器退出") sys.exit(1) @contextmanager def _get_connection(self): if not self.connection_pool: self.reconnect() try: conn = self.connection_pool.get_connection() if not conn.is_connected(): conn.reconnect() yield conn except mysql.connector.Error as err: logger.error(f"获取数据库连接失败: {err}") if err.errno == errorcode.CR_SERVER_GONE_ERROR: self.reconnect() raise finally: if 'conn' in locals() and conn.is_connected(): conn.close() def initialize_db(self): max_attempts = 3 for attempt in range(1, max_attempts + 1): try: with self._get_connection() as conn: cursor = conn.cursor() # 创建设备表 cursor.execute(''' CREATE TABLE IF NOT EXISTS devices ( id INT AUTO_INCREMENT PRIMARY KEY, device_id VARCHAR(50) NOT NULL UNIQUE, first_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ''') # 创建传感器数据表 cursor.execute(''' CREATE TABLE IF NOT EXISTS sensor_data ( id BIGINT AUTO_INCREMENT PRIMARY KEY, device_id INT NOT NULL, timestamp TIMESTAMP NOT NULL, sensor_values JSON NOT NULL, received_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, INDEX idx_device_id (device_id), INDEX idx_timestamp (timestamp), FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE ) ''') conn.commit() logger.info("数据库表结构初始化成功") return except Exception as e: logger.error(f"数据库初始化失败 (尝试 {attempt}/{max_attempts}): {e}") if attempt < max_attempts: logger.info(f"{2 * attempt}秒后重试...") time.sleep(2 * attempt) logger.critical("数据库初始化失败,服务器退出") sys.exit(1) def insert_sensor_data(self, device_id: str, timestamp: str, sensor_values: List[float]) -> None: # 在存储前验证时间戳 validated_timestamp = self.validate_timestamp(timestamp) self.data_buffer.append({ 'device_id': device_id, 'timestamp': validated_timestamp, 'sensor_values': sensor_values }) now = datetime.datetime.now() buffer_full = len(self.data_buffer) >= CONFIG["DB_WRITE_BATCH_SIZE"] timeout_reached = (now - self.last_write_time).total_seconds() >= CONFIG["DB_WRITE_TIMEOUT"] if buffer_full or timeout_reached: self.flush_buffer() def validate_timestamp(self, timestamp: str) -> str: """验证并修正时间戳格式""" try: # 尝试解析为标准格式 datetime.datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S") return timestamp except ValueError: # 尝试修正格式 return self.correct_timestamp_format(timestamp) def correct_timestamp_format(self, timestamp: str) -> str: """修正常见的时间戳格式问题""" # 处理年份不足四位的情况 if re.match(r'^\d{1,3}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$', timestamp): parts = timestamp.split('-') year = normalize_year(parts[0]) return f"{year}-{parts[1]}" # 处理日期分隔符不一致的问题 if '/' in timestamp: return timestamp.replace('/', '-') # 作为最后手段,尝试使用更宽松的解析 try: dt = datetime.datetime.fromisoformat(timestamp) return dt.strftime("%Y-%m-%d %H:%M:%S") except ValueError: logger.warning(f"无法修正时间戳格式: {timestamp}") # 使用当前时间作为默认值 return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") def flush_buffer(self) -> None: if not self.data_buffer: return try: with self._get_connection() as conn: cursor = conn.cursor() # 批量写入设备数据 device_ids = [] for data in self.data_buffer: # 获取或创建设备ID device_db_id = self._get_or_create_device(conn, data['device_id']) device_ids.append(device_db_id) # 准备批量插入传感器数据 sensor_data = [] for i, data in enumerate(self.data_buffer): sensor_data.append(( device_ids[i], data['timestamp'], json.dumps(data['sensor_values']) )) # 批量插入传感器数据 cursor.executemany( "INSERT INTO sensor_data (device_id, timestamp, sensor_values) " "VALUES (%s, %s, %s)", sensor_data ) conn.commit() logger.info(f"成功写入 {len(self.data_buffer)} 条数据到MySQL数据库") self.data_buffer.clear() self.last_write_time = datetime.datetime.now() except mysql.connector.Error as err: if err.errno == errorcode.ER_TRUNCATED_WRONG_VALUE: # 处理日期时间格式错误 self.handle_datetime_error(err) else: logger.error(f"数据库写入失败: {err}") logger.error(f"异常详情:\n{traceback.format_exc()}") except Exception as e: logger.error(f"数据库写入失败: {e}") logger.error(f"异常详情:\n{traceback.format_exc()}") def handle_datetime_error(self, err: mysql.connector.Error) -> None: """处理日期时间格式错误""" logger.error("检测到日期时间格式错误,尝试逐条处理数据...") for i, data in enumerate(self.data_buffer[:]): try: with self._get_connection() as conn: cursor = conn.cursor() # 获取或创建设备ID device_db_id = self._get_or_create_device(conn, data['device_id']) # 尝试插入数据 cursor.execute( "INSERT INTO sensor_data (device_id, timestamp, sensor_values) " "VALUES (%s, %s, %s)", (device_db_id, data['timestamp'], json.dumps(data['sensor_values'])) ) conn.commit() logger.info(f"成功写入数据: {data['timestamp']}") # 从缓冲区移除成功写入的数据 self.data_buffer.remove(data) except mysql.connector.Error as inner_err: if inner_err.errno == errorcode.ER_TRUNCATED_WRONG_VALUE: logger.error(f"日期时间格式错误: {data['timestamp']}") # 尝试修正时间戳 data['timestamp'] = self.correct_timestamp_format(data['timestamp']) logger.warning(f"尝试使用修正后的时间戳: {data['timestamp']}") else: logger.error(f"插入数据失败: {inner_err}") except Exception as inner_e: logger.error(f"处理数据时出错: {inner_e}") def _get_or_create_device(self, conn, device_id: str) -> int: cursor = conn.cursor() try: cursor.execute( "SELECT id FROM devices WHERE device_id = %s", (device_id,) ) device = cursor.fetchone() if device: device_id = device[0] cursor.execute( "UPDATE devices SET last_seen = CURRENT_TIMESTAMP WHERE id = %s", (device_id,) ) return device_id else: cursor.execute( "INSERT INTO devices (device_id) VALUES (%s)", (device_id,) ) return cursor.lastrowid except mysql.connector.Error as err: logger.error(f"设备操作失败: {err}") return -1 # ====================== # 数据包解析 # ====================== def parse_sensor_values(data: bytes, start_index: int) -> List[float]: values = [] index = start_index max_count = (len(data) - index) // 4 for _ in range(max_count): if index + 4 > len(data): break value_bytes = data[index:index+4] index += 4 try: value = struct.unpack('<f', value_bytes)[0] values.append(value) except struct.error: try: value = struct.unpack('>f', value_bytes)[0] values.append(value) except struct.error: continue return values def parse_binary_packet(raw_data: bytes) -> Optional[Dict]: try: HEADER = b'$\x00\x08\x00\x00\x00C' if not raw_data.startswith(HEADER): return None DEVICE_ID_START = len(HEADER) DEVICE_ID_END = DEVICE_ID_START + 8 if len(raw_data) < DEVICE_ID_END: return None device_id_bytes = raw_data[DEVICE_ID_START:DEVICE_ID_END] TIMESTAMP_START = DEVICE_ID_END TIMESTAMP_END = TIMESTAMP_START + 19 if len(raw_data) < TIMESTAMP_END: return None timestamp_bytes = raw_data[TIMESTAMP_START:TIMESTAMP_END] SENSOR_DATA_START = TIMESTAMP_END sensor_values = parse_sensor_values(raw_data, SENSOR_DATA_START) return { 'device_id': decode_with_fallback(device_id_bytes), 'timestamp': parse_timestamp(timestamp_bytes), 'sensor_values': sensor_values } except Exception as e: logger.error(f"解析错误: {str(e)}") return None def decode_with_fallback(data: bytes) -> str: try: return data.decode('ascii').strip() except UnicodeDecodeError: try: return data.decode('latin-1').strip() except UnicodeDecodeError: return data.hex() # ====================== # WebSocket服务器 # ====================== class SensorWebSocketServer: def __init__(self, host: str, port: int, db_manager: MySQLDatabaseManager): self.host = host self.port = port self.db_manager = db_manager self.connections = set() self.server = None self.stats = { 'total_connections': 0, 'total_messages': 0, 'valid_messages': 0, 'invalid_messages': 0 } self.start_time = None async def handler(self, websocket, path: str) -> None: client_ip = websocket.remote_address[0] if websocket.remote_address else "unknown" self.connections.add(websocket) self.stats['total_connections'] += 1 logger.info(f"客户端连接: {client_ip},当前连接数: {len(self.connections)}") try: async for message in websocket: self.stats['total_messages'] += 1 if not isinstance(message, bytes): self.stats['invalid_messages'] += 1 continue parsed_data = parse_binary_packet(message) if parsed_data: self.stats['valid_messages'] += 1 self.db_manager.insert_sensor_data( parsed_data['device_id'], parsed_data['timestamp'], parsed_data['sensor_values'] ) else: self.stats['invalid_messages'] += 1 except websockets.exceptions.ConnectionClosed: logger.info(f"客户端断开: {client_ip}") except Exception as e: logger.error(f"处理客户端 {client_ip} 时出错: {str(e)}") finally: if websocket in self.connections: self.connections.remove(websocket) logger.info(f"客户端断开,剩余连接数: {len(self.connections)}") async def start(self) -> None: self.server = await websockets.serve( self.handler, self.host, self.port, max_size=2**20, ping_interval=30, ping_timeout=15, close_timeout=10, max_queue=CONFIG["MAX_CONNECTIONS"] ) self.start_time = datetime.datetime.now() logger.info(f"WebSocket服务器启动在 ws://{self.host}:{self.port}") async def stop(self) -> None: if self.server: self.server.close() await self.server.wait_closed() logger.info("WebSocket服务器已停止") def get_stats(self) -> Dict: return { **self.stats, 'current_connections': len(self.connections), 'uptime': str(datetime.datetime.now() - self.start_time) if self.start_time else "0:00:00" } # ====================== # 主程序 # ====================== async def main(): logger.debug("初始化数据库管理器...") db_manager = MySQLDatabaseManager() logger.debug("创建WebSocket服务器...") server = SensorWebSocketServer( CONFIG["WEBSOCKET_HOST"], CONFIG["WEBSOCKET_PORT"], db_manager ) logger.debug("启动WebSocket服务器...") await server.start() try: logger.info("服务器进入主循环") while True: await asyncio.sleep(5) db_manager.flush_buffer() # 每分钟记录状态 now = datetime.datetime.now() if now.second < 5: stats = server.get_stats() logger.info( f"状态 | 连接: {stats['current_connections']} | " f"消息: {stats['valid_messages']}/{stats['total_messages']} | " f"运行时间: {stats['uptime']}" ) except asyncio.CancelledError: logger.info("服务器停止请求") except KeyboardInterrupt: logger.info("收到停止信号,正在关闭服务器...") finally: db_manager.flush_buffer() await server.stop() # 启动程序 if __name__ == "__main__": logger.info("=" * 50) logger.info("启动传感器数据采集服务器") logger.info(f"时间: {datetime.datetime.now()}") logger.info(f"监听地址: {CONFIG['WEBSOCKET_HOST']}:{CONFIG['WEBSOCKET_PORT']}") logger.info(f"MySQL主机: {CONFIG['MYSQL']['HOST']}:{CONFIG['MYSQL']['PORT']}") logger.info(f"数据库: {CONFIG['MYSQL']['DATABASE']}") logger.info("=" * 50) try: if sys.platform == 'win32': asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) asyncio.run(main()) except Exception as e: logger.critical(f"服务器意外停止: {e}") logger.critical(f"异常详情:\n{traceback.format_exc()}") finally: logger.info("=" * 50) logger.info("服务器已停止运行") logger.info("=" * 50) ``` ## 关键修复:时间戳处理增强 ### 1. 年份规范化函数 ```python def normalize_year(year: str) -> int: """将年份规范化为四位数""" year_int = int(year) # 处理两位数的年份 if year_int < 100: # 00-69 映射为 2000-2069, 70-99 映射为 1970-1999 return year_int + 2000 if year_int < 70 else year_int + 1900 # 处理三位数的年份 if 100 <= year_int < 1000: # 025 -> 2025, 099 -> 1999, 100 -> 2100 return year_int + 1000 if year_int < 100 else year_int return year_int ``` 这个函数处理各种年份格式: - 两位数年份:`25` → `2025`,`85` → `1985` - 三位数年伤:`025` → `2025`,`099` → `1999` - 四位数年份保持不变 ### 2. 增强的时间戳解析 ```python def parse_timestamp(timestamp_bytes: bytes) -> str: """增强的时间戳解析函数,处理各种日期格式""" # ... 解码和清理 ... # 使用正则表达式匹配多种日期格式 patterns = [ r'(\d{1,4})[-/:](\d{1,2})[-/:](\d{1,2})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})', r'(\d{1,2})[-/:](\d{1,2})[-/:](\d{1,4})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})', r'(\d{1,2})[-/](\w{3})[-/](\d{1,4})\s+(\d{1,2}):(\d{1,2}):(\d{1,2})', ] for pattern in patterns: match = re.match(pattern, cleaned) if match: # 提取组件并规范化年份 normalized_year = normalize_year(year) # 创建datetime对象 dt = datetime.datetime( year=normalized_year, month=int(month), day=int(day), hour=int(hour), minute=int(minute), second=int(second) ) return dt.strftime("%Y-%m-%d %H:%M:%S") # ... 其他尝试 ... ``` 这个解析器可以处理: - 不同顺序的日期组件(年月日、月日年等) - 不同分隔符(-、/、:) - 月份缩写(如 "Jun" 代表六月) - 不同位数的年份 ### 3. 时间戳验证和修正 ```python def validate_timestamp(self, timestamp: str) -> str: """验证并修正时间戳格式""" try: # 尝试解析为标准格式 datetime.datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S") return timestamp except ValueError: # 尝试修正格式 return self.correct_timestamp_format(timestamp) def correct_timestamp_format(self, timestamp: str) -> str: """修正常见的时间戳格式问题""" # 处理年份不足四位的情况 if re.match(r'^\d{1,3}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$', timestamp): parts = timestamp.split('-') year = normalize_year(parts[0]) return f"{year}-{parts[1]}" # 处理日期分隔符不一致的问题 if '/' in timestamp: return timestamp.replace('/', '-') # 作为最后手段,使用当前时间 return datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") ``` 这个验证层确保所有时间戳在存储前都是有效的MySQL格式: 1. 首先尝试直接验证 2. 如果不通过,尝试自动修正 3. 作为最后手段,使用当前时间 ### 4. 错误处理增强 ```python def flush_buffer(self) -> None: try: # 批量插入数据... except mysql.connector.Error as err: if err.errno == errorcode.ER_TRUNCATED_WRONG_VALUE: # 专门处理日期时间格式错误 self.handle_datetime_error(err) else: # 处理其他错误... def handle_datetime_error(self, err: mysql.connector.Error) -> None: """处理日期时间格式错误""" logger.error("检测到日期时间格式错误,尝试逐条处理数据...") for i, data in enumerate(self.data_buffer[:]): try: # 逐条尝试插入... except mysql.connector.Error as inner_err: if inner_err.errno == errorcode.ER_TRUNCATED_WRONG_VALUE: # 修正时间戳并重试 data['timestamp'] = self.correct_timestamp_format(data['timestamp']) ``` 这个错误处理机制: 1. 检测特定的日期时间错误(错误码1292) 2. 切换到逐条处理模式 3. 对每条数据单独修正时间戳 4. 重试失败的数据项 ## 时间格式支持扩展 配置中增加了更多时间格式支持: ```python "TIME_FORMATS": [ "%Y-%m-%d %H:%M:%S", # 标准格式: 2025-06-20 03:17:35 "%y-%m-%d %H:%M:%S", # 两位年份格式: 25-06-20 03:17:35 "%d-%b-%y %H:%M:%S", # 缩写月份+两位年份: 20-Jun-25 03:17:35 "%Y%m%d%H%M%S", # 紧凑格式: 20250620031735 "%d/%m/%y %H:%M:%S", # 欧洲日期格式: 20/06/25 03:17:35 "%m/%d/%y %H:%M:%S" # 美国日期格式: 06/20/25 03:17:35 ] ``` ## 测试用例 以下代码可用于测试时间戳解析功能: ```python def test_timestamp_parsing(): test_cases = [ (b"025-06-20 03:17:35", "2025-06-20 03:17:35"), (b"25-06-20 03:17:35", "2025-06-20 03:17:35"), (b"99-06-20 03:17:35", "1999-06-20 03:17:35"), (b"2025-06-20 03:17:35", "2025-06-20 03:17:35"), (b"20/Jun/25 03:17:35", "2025-06-20 03:17:35"), (b"06/20/25 03:17:35", "2025-06-20 03:17:35"), (b"20250620031735", "2025-06-20 03:17:35"), (b"invalid-timestamp", None) # 预期无法解析 ] for input_bytes, expected in test_cases: result = parse_timestamp(input_bytes) print(f"输入: {input_bytes} -> 结果: {result} | 预期: {expected} | {'通过' if result == expected else '失败'}") if __name__ == "__main__": test_timestamp_parsing() ``` ## 部署建议 1. **监控时间戳错误**: ```sql -- 查询最近的时间戳错误 SELECT * FROM sensor_data WHERE timestamp < '1900-01-01' OR timestamp > '2100-01-01' ORDER BY received_at DESC LIMIT 100; ``` 2. **日志分析**: ```bash # 查找时间戳解析警告 grep "无法解析时间戳" sensor_server.log grep "尝试使用修正后的时间戳" sensor_server.log ``` 3. **客户端验证**: 在客户端添加时间戳格式验证: ```python from datetime import datetime def validate_timestamp(timestamp): try: datetime.strptime(timestamp, "%Y-%m-%d %H:%M:%S") return True except ValueError: return False ``` 这个解决方案全面解决了日期时间格式问题,同时增强了系统的健壮性和错误处理能力。时间戳现在会被正确解析、验证和修正,确保顺利存储到MySQL数据库。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值