<think>我们已知有859个标签(Tag),每个标签包含TagId,TagName,DataType等信息。根据提供的示例数据,我们注意到:1.数据以二进制格式传输。2.示例数据:b'$\x00\x08\x00\x00\x00CREC10082025-06-1802:28:00\\\x8fB?...'(后面很长)根据之前的信息,数据包结构可能如下(但需要根据实际数据调整):-头部:可能包含数据包长度等信息(示例中前6个字节:b'$\x00\x08\x00\x00\x00')-设备ID:固定8字节(示例中为'CREC1008')-时间戳:固定19字节(示例中为'2025-06-1802:28:00')-后续为各个标签的值,按照标签的顺序排列,每个标签的值根据数据类型占据不同字节数:Float:4字节Short:2字节Boolean:1字节但是,我们注意到标签表中大部分是Float,有少量Short和Boolean。特别是第326号标签(环号)和288号标签(导向掘进环数)是Short类型,以及从651号标签开始是Boolean类型(共859-650=209个Boolean)。因此,我们需要一个解析函数,能够根据标签的序号和数据类型来解析二进制数据。步骤:1.初始化数据库,创建表来存储原始数据和解析后的数据(按标签存储)。2.在WebSocket服务器中接收二进制消息。3.解析消息头(前6字节),然后读取设备ID(8字节)和时间戳(19字节)。4.之后的数据部分,按照标签表的顺序和数据类型进行解析。注意:标签表总共有859个标签,我们需要一个包含所有标签定义的列表(或从文件/数据库加载)。由于标签数量较多,我们在代码中不会全部列出,但会给出一个示例结构,实际部署时需要将完整的标签表导入数据库或配置文件。调整解析函数:-我们假设数据包结构为:头部(6字节)+设备ID(8字节)+时间戳(19字节)+数据部分(每个标签的值按顺序排列)-数据部分:先按顺序解析前326个标签(前325个是Float,第326个是Short),然后继续解析后面的标签(直到第650个都是Float),然后从651到859是Boolean(每个1字节)。但是,请注意标签表中有两个Short类型(288和326)和209个Boolean类型(从651到859)。其他都是Float。因此,数据部分的结构可能是:-前325个标签:每个4字节(Float)-第326个标签(环号):2字节(Short)-然后第327到650个标签:每个4字节(Float)(共650-326=324个,但注意第288个标签是Short,但它在前面已经按Float解析了?这似乎有矛盾)我们需要重新审视:标签的序号(TagId)是从1到859,但是它们在数据包中出现的顺序是按照TagId顺序排列的吗?根据之前提供的信息,数据包中的标签值顺序是按照TagIndex(即TagId)的顺序排列的。因此,我们需要按照TagId的顺序来解析,并且根据每个TagId对应的数据类型来解析。所以,我们必须在解析时遍历标签表(按TagId从1到859),根据每个标签的数据类型读取相应的字节数。因此,解析步骤:position=0读取6字节头部->position=6读取8字节设备ID->position=14读取19字节时间戳(字符串)->position=33然后,对于每个标签(1到859):如果标签的数据类型是Float:读取4字节,转换为float如果标签的数据类型是Short:读取2字节,转换为short如果标签的数据类型是Boolean:读取1字节,转换为boolean(非0为True)但是,我们注意到标签表中第288号标签(导向掘进环数)是Short,第326号标签(环号)也是Short,其余Float和Boolean(从651开始)。因此,我们需要一个标签定义列表,按TagId排序(从1到859),并且每个元素包含数据类型。由于标签数量多,我们可以在数据库中建立标签元数据表,然后在解析时从数据库读取标签定义(按TagId排序)。但这样每次解析都要查数据库,效率低。所以,我们可以在程序启动时加载到内存中。我们也可以在代码中定义一个列表,但这样代码会很长。折中方案:将标签表存储在JSON文件中,程序启动时加载。为简化,我们在代码中创建一个tags列表,包含859个元素,每个元素是一个字典,包含id,name,data_type。但实际部署时,建议从外部文件或数据库加载。由于标签表很长,我们这里只写框架,实际使用需要填充完整的标签表。调整存储:-原始数据表:存储整个二进制消息(可选)-解析数据表:按时间戳、设备ID、每个标签存储一条记录(这样每条消息会产生859条记录)?或者按消息存储为一条记录,包含多个字段?但是859个字段不太现实。考虑到查询灵活性,我们采用第一种方式:每条消息的每个标签值存储为单独记录。这样便于按标签查询历史数据。但是,这样存储量会很大(每秒一条消息就有859条记录)。因此,我们需要权衡。另一种方式是每条消息存储为一行,有859个列。但这在关系型数据库中可能超过列数限制(SQLite最多2000列,但859列是允许的),不过查询时如果想查某个标签的历史,需要遍历所有行,效率也不高。因此,我们采用第一种(长表格式),表结构如下:id(自增主键),timestamp,device_id,tag_id,value这样,每收到一条消息,我们就插入859行。但是,这样每秒有859条记录,一天就有859*86400≈7420万条记录。需要考虑分区或使用时序数据库。作为初始方案,我们先使用SQLite,但长期应考虑更强大的数据库。代码调整:1.初始化数据库时,创建表parsed_data,包含字段:idINTEGERPRIMARYKEYAUTOINCREMENT,timestampDATETIME,--从消息中解析的时间戳device_idTEXT,tag_idINTEGER,valueREAL,--对于Boolean,我们存储为0.0或1.0,这样可以用REAL(SQLite没有布尔类型,但可以用整数,不过我们统一用REAL)tag_nameTEXT--可选,方便查询2.解析函数将按照标签定义列表,依次解析每个标签的值,然后插入到parsed_data表中。3.标签定义列表:由于数量大,我们从一个文件加载(比如tags.json),格式如下:[{"id":1,"name":"1#注浆A液压力","data_type":"Float"},...]4.在程序启动时,加载标签定义。由于标签表是固定的,我们可以将其放在一个Python字典中,但为了代码简洁,我们使用外部JSON文件。步骤:-创建tags.json文件,包含所有标签的定义(按id从1到859)-程序启动时读取这个文件,得到一个列表`tags`,按id排序(确保顺序)注意:标签的id是连续的,所以我们可以按索引访问(索引0对应id1,索引1对应id2,...)代码实现:由于标签数量多,我们不会在代码中硬编码,而是从文件加载。我们修改之前的代码:1.添加一个函数`load_tags_from_json(filename)`,返回标签列表。2.在解析函数中使用这个标签列表。考虑到数据包中时间戳是字符串,我们直接存储为字符串(或者转换为datetime对象,但原始消息中已经是字符串)。我们假设时间戳字符串格式为:'%Y-%m-%d%H:%M:%S'修改后的解析函数:注意:在解析二进制数据时,使用struct模块。对于浮点数,使用'<f'(小端),对于整数(short)使用'<h',对于布尔值,读取一个字节,如果为0则为False,否则为True。但是,注意:布尔值在数据库中存储为0.0或1.0(浮点数),这样统一。由于标签数量多,解析时循环859次。我们还需要注意:布尔值在消息中占1个字节,但我们的标签表中布尔值有209个,所以消息中数据部分的总长度应该是:325个Float*4=13002个Short*2=4(但注意有两个Short标签,分别在第288和326位置,但它们在两个不同的位置,所以不能简单相加)实际上,我们需要按顺序计算:前287个标签:Float->287*4第288个标签:Short->2第289到325个标签:Float->(325-288)*4=37*4=148第326个标签:Short->2第327到650个标签:Float->(650-326)*4=324*4=1296(注意:这里包括第288和326之间的其他Float?实际上,我们按顺序来)然后第651到859:Boolean->209*1总长度=287*4+2+37*4+2+324*4+209*1=287*4+37*4+324*4+2+2+209=(287+37+324)*4+213=648*4+213=2592+213=2805再加上头部6+8+19=33,总长度=33+2805=2838字节。但示例数据长度是多少?示例数据开头是b'$\x00\x08\x00\x00\x00CREC10082025-06-1802:28:00\\\x8fB?...',后面很长。我们计算示例数据的长度:示例数据:b'$\x00\x08\x00\x00\x00CREC10082025-06-1802:28:00\\\x8fB?...'(后面还有很长,但具体长度未知)我们可以打印示例数据长度:len(data)=33+2805=2838?但实际示例数据长度是:len(b'...')我们不知道,但我们可以用len(收到的数据)来验证。如果长度不一致,说明我们的假设有误。因此,我们需要根据实际数据包调整解析。另一种方法:不假设长度,而是按照标签定义依次解析,如果数据不够,则报错。我们按顺序解析,如果解析完859个标签后,数据还有剩余或不足,则记录错误。现在,我们修改代码:由于标签表中有859个标签,我们按顺序解析。在解析函数中,我们使用一个位置指针,从33开始(头部6+设备ID8+时间戳19=33),然后循环标签列表,根据每个标签的类型解析。代码框架:position=33fortagintags:#tags是按id排序的标签列表iftag['data_type']=='Float':value=struct.unpack_from('<f',data,position)[0]position+=4eliftag['data_type']=='Short':value=struct.unpack_from('<h',data,position)[0]position+=2eliftag['data_type']=='Boolean':#读取1个字节,转换为布尔值value=bool(data[position])position+=1#存储这个标签的值最后,检查position是否等于len(data),如果不相等,则记录错误。由于标签数量大,我们最好将标签定义存储在外部文件中。我们创建tags.json文件,格式如下:[{"id":1,"name":"CREC1008.CREC1008.1","description":"1#注浆A液压力","data_type":"Float"},...]注意:我们只需要id,name,data_type。description可选。在程序中,我们按id排序(因为id是连续的,但为了确保顺序,我们按id升序排列)。由于标签表有859行,我们可以从提供的tag表生成JSON文件。生成方法:将tag表保存为CSV,然后写一个脚本转换为JSON。或者,我们直接在代码中构造(但代码会很长)。为了简便,我们在代码中不包含完整的标签表,而是假设有一个tags.json文件。现在,我们修改代码:1.添加load_tags函数2.修改解析函数3.修改数据库存储(每条消息存储859条记录)考虑到性能,我们可以使用批量插入,但SQLite的批量插入每次最多500条,我们可以分两次插入。或者,使用事务,将859条插入放在一个事务中。代码中,我们使用一个事务插入所有解析结果。修改后的代码:由于代码较长,我们只给出关键部分。注意:在Docker中,我们需要将tags.json放在容器内,可以在Dockerfile中复制。步骤:-准备tags.json文件,包含所有标签定义(按id顺序)-修改Dockerfile,复制tags.json-修改代码,在启动时加载tags.json由于标签定义可能变化,使用外部文件更灵活。下面,我们给出修改后的代码框架。</think>根据您提供的完整 tag 表(共 859 条),我将优化数据解析和存储方案,并提供完整的 Docker 部署方案。以下是改进后的解决方案:
### 完整解决方案
#### 1. 主程序 (app.py)
```python
import asyncio
import websockets
import sqlite3
import struct
import json
from datetime import datetime
from fastapi import FastAPI, HTTPException
import uvicorn
import os
# 加载标签定义
def load_tags():
# 从 JSON 文件加载标签定义
if os.path.exists('tags.json'):
with open('tags.json', 'r', encoding='utf-8') as f:
return json.load(f)
# 如果文件不存在,使用提供的标签表创建
tags = [
{"id": 1, "name": "CREC1008.CREC1008.1", "description": "1#注浆A液压力", "data_type": "Float"},
{"id": 2, "name": "CREC1008.CREC1008.2", "description": "2#注浆A液压力", "data_type": "Float"},
# ... 完整 859 个标签定义
{"id": 859, "name": "CREC1008.CREC1008.859", "description": "渣车满", "data_type": "Boolean"}
]
# 保存为 JSON 文件
with open('tags.json', 'w', encoding='utf-8') as f:
json.dump(tags, f, ensure_ascii=False, indent=2)
return tags
TAGS = load_tags()
TAG_MAP = {tag['id']: tag for tag in TAGS}
# 数据库初始化
def init_db():
conn = sqlite3.connect('data.db')
c = conn.cursor()
# 创建原始数据表
c.execute('''CREATE TABLE IF NOT EXISTS raw_data (
id INTEGER PRIMARY KEY,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
device_id TEXT,
raw_data BLOB)''')
# 创建解析数据表
c.execute('''CREATE TABLE IF NOT EXISTS parsed_data (
id INTEGER PRIMARY KEY,
timestamp DATETIME,
device_id TEXT,
tag_id INTEGER,
tag_name TEXT,
value REAL)''')
# 创建元数据表
c.execute('''CREATE TABLE IF NOT EXISTS tag_metadata (
tag_id INTEGER PRIMARY KEY,
tag_name TEXT,
description TEXT,
data_type TEXT)''')
# 插入或更新tag元数据
for tag in TAGS:
c.execute('''INSERT OR REPLACE INTO tag_metadata
(tag_id, tag_name, description, data_type)
VALUES (?, ?, ?, ?)''',
(tag['id'], tag['name'], tag.get('description', ''), tag['data_type']))
# 创建索引
c.execute('''CREATE INDEX IF NOT EXISTS idx_parsed_data_timestamp ON parsed_data(timestamp)''')
c.execute('''CREATE INDEX IF NOT EXISTS idx_parsed_data_tag_id ON parsed_data(tag_id)''')
c.execute('''CREATE INDEX IF NOT EXISTS idx_parsed_data_device_id ON parsed_data(device_id)''')
conn.commit()
conn.close()
# 解析二进制数据(优化版)
def parse_data(device_id, timestamp, data):
results = []
position = 0
try:
# 解析头部 (6字节)
header = struct.unpack_from('<cH3s', data, position)
position += 6
# 设备ID (8字节)
device_id_str = data[position:position+8].decode('ascii')
position += 8
# 时间戳 (19字节)
timestamp_str = data[position:position+19].decode('ascii')
position += 19
# 验证设备ID
if device_id != device_id_str:
print(f"设备ID不匹配: 路径 {device_id} vs 数据 {device_id_str}")
return []
# 解析各个tag值
for tag_id in range(1, 860): # 标签ID从1到859
tag = TAG_MAP.get(tag_id)
if not tag:
continue
if tag['data_type'] == 'Float':
value = struct.unpack_from('<f', data, position)[0]
position += 4
elif tag['data_type'] == 'Short':
value = struct.unpack_from('<h', data, position)[0]
position += 2
elif tag['data_type'] == 'Boolean':
value = bool(data[position])
position += 1
# 布尔值转换为1.0/0.0便于存储
value = 1.0 if value else 0.0
else:
# 默认处理为float
value = struct.unpack_from('<f', data, position)[0]
position += 4
results.append({
'timestamp': timestamp_str,
'device_id': device_id,
'tag_id': tag_id,
'tag_name': tag['name'],
'value': value
})
# 验证解析长度
if position != len(data):
print(f"警告: 数据长度不匹配 解析位置: {position}, 实际长度: {len(data)}")
except Exception as e:
print(f"解析错误: {e}")
return results
# WebSocket服务器
async def handle_client(websocket, path):
device_id = path.strip('/')
print(f"设备 {device_id} 已连接")
try:
async for message in websocket:
timestamp = datetime.now().isoformat()
# 存储原始数据
conn = sqlite3.connect('data.db')
c = conn.cursor()
c.execute('''INSERT INTO raw_data
(device_id, raw_data)
VALUES (?, ?)''',
(device_id, message))
conn.commit()
# 解析数据
parsed = parse_data(device_id, timestamp, message)
if not parsed:
print(f"从 {device_id} 收到数据但解析失败")
conn.close()
continue
# 批量插入解析数据
batch = [(item['timestamp'], item['device_id'],
item['tag_id'], item['tag_name'], item['value'])
for item in parsed]
c.executemany('''INSERT INTO parsed_data
(timestamp, device_id, tag_id, tag_name, value)
VALUES (?, ?, ?, ?, ?)''', batch)
conn.commit()
conn.close()
print(f"从 {device_id} 收到 {len(parsed)} 条数据, 时间: {parsed[0]['timestamp']}")
except Exception as e:
print(f"处理错误: {e}")
finally:
print(f"设备 {device_id} 断开连接")
# 启动WebSocket服务器
async def start_websocket_server():
async with websockets.serve(handle_client, "0.0.0.0", 8765):
print("WebSocket 服务器启动,端口 8765")
await asyncio.Future() # 永久运行
# REST API
app = FastAPI()
@app.get("/data/latest")
async def get_latest_data(device_id: str, tag_id: int = None):
conn = sqlite3.connect('data.db')
c = conn.cursor()
try:
if tag_id:
c.execute('''SELECT * FROM parsed_data
WHERE device_id = ? AND tag_id = ?
ORDER BY timestamp DESC LIMIT 1''',
(device_id, tag_id))
else:
c.execute('''SELECT * FROM parsed_data
WHERE device_id = ?
ORDER BY timestamp DESC LIMIT 1''',
(device_id,))
result = c.fetchone()
if not result:
raise HTTPException(status_code=404, detail="未找到数据")
return {
"id": result[0],
"timestamp": result[1],
"device_id": result[2],
"tag_id": result[3],
"tag_name": result[4],
"value": result[5]
}
finally:
conn.close()
@app.get("/data/history")
async def get_history_data(device_id: str, tag_id: int, hours: int = 24, limit: int = 1000):
conn = sqlite3.connect('data.db')
c = conn.cursor()
try:
c.execute('''SELECT timestamp, value FROM parsed_data
WHERE device_id = ? AND tag_id = ?
AND timestamp >= datetime('now', ?)
ORDER BY timestamp DESC
LIMIT ?''',
(device_id, tag_id, f"-{hours} hours", limit))
results = c.fetchall()
if not results:
raise HTTPException(status_code=404, detail="未找到数据")
return [{"timestamp": r[0], "value": r[1]} for r in results]
finally:
conn.close()
@app.get("/tags")
async def get_all_tags():
conn = sqlite3.connect('data.db')
c = conn.cursor()
try:
c.execute("SELECT * FROM tag_metadata ORDER BY tag_id")
return [{
"tag_id": r[0],
"tag_name": r[1],
"description": r[2],
"data_type": r[3]
} for r in c.fetchall()]
finally:
conn.close()
@app.get("/tags/{tag_id}")
async def get_tag_detail(tag_id: int):
conn = sqlite3.connect('data.db')
c = conn.cursor()
try:
c.execute("SELECT * FROM tag_metadata WHERE tag_id = ?", (tag_id,))
result = c.fetchone()
if not result:
raise HTTPException(status_code=404, detail="未找到标签")
return {
"tag_id": result[0],
"tag_name": result[1],
"description": result[2],
"data_type": result[3]
}
finally:
conn.close()
# 主函数
if __name__ == "__main__":
# 初始化数据库
init_db()
# 启动服务
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
# 启动WebSocket服务器
websocket_task = loop.create_task(start_websocket_server())
# 启动REST API
config = uvicorn.Config(
app,
host="0.0.0.0",
port=8000,
loop=loop
)
server = uvicorn.Server(config)
rest_task = loop.create_task(server.serve())
# 同时运行两个服务
loop.run_until_complete(asyncio.gather(websocket_task, rest_task))
except KeyboardInterrupt:
print("服务关闭")
finally:
loop.close()
```
#### 2. Dockerfile
```dockerfile
# 使用官方Python镜像
FROM python:3.11-slim
# 设置工作目录
WORKDIR /app
# 安装系统依赖
RUN apt-get update && apt-get install -y \
gcc \
libsqlite3-dev \
&& rm -rf /var/lib/apt/lists/*
# 安装Python依赖
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 复制代码和标签定义
COPY . .
# 初始化数据库
RUN python -c "from app import init_db; init_db()"
# 暴露端口
EXPOSE 8765 8000
# 启动命令
CMD ["python", "app.py"]
```
#### 3. requirements.txt
```
websockets==12.0
fastapi==0.109.0
uvicorn==0.25.0
python-multipart==0.0.9
```
#### 4. 标签定义文件 (tags.json)
```json
[
{
"id": 1,
"name": "CREC1008.CREC1008.1",
"description": "1#注浆A液压力",
"data_type": "Float"
},
{
"id": 2,
"name": "CREC1008.CREC1008.2",
"description": "2#注浆A液压力",
"data_type": "Float"
},
// ... 完整859个标签定义
{
"id": 859,
"name": "CREC1008.CREC1008.859",
"description": "渣车满",
"data_type": "Boolean"
}
]
```
### 系统优化说明
1. **标签管理优化**:
- 使用JSON文件存储标签定义,便于维护和更新
- 自动创建标签定义文件(如果不存在)
- 在数据库中存储标签元数据,便于查询
2. **数据解析优化**:
- 精确处理三种数据类型:Float(4字节)、Short(2字节)、Boolean(1字节)
- 添加数据长度验证,确保解析完整性
- 使用TAG_MAP快速查找标签定义
3. **数据库优化**:
- 添加索引提高查询性能
- 使用批量插入(executemany)提高数据存储效率
- 添加详细的错误处理
4. **API增强**:
- 添加分页和限制参数防止大数据量查询
- 添加单个标签详情接口
- 返回标签描述信息
5. **部署优化**:
- 使用Python 3.11提高性能
- 添加必要的系统依赖
- 优化Docker构建过程
### 构建和运行命令
```bash
# 1. 准备文件
# app.py, Dockerfile, requirements.txt, tags.json
# 2. 构建Docker镜像
docker build -t tunnel-monitor -f Dockerfile .
# 3. 运行容器(持久化数据存储)
docker run -d \
-p 8765:8765 \
-p 8000:8000 \
-v tunnel-data:/app/data \
--name monitor \
tunnel-monitor
# 4. 测试WebSocket连接
python -c "import asyncio, websockets, os; asyncio.get_event_loop().run_until_complete(websockets.connect('ws://localhost:8765/CREC1008'))"
# 5. 测试API
curl "http://localhost:8000/tags"
curl "http://localhost:8000/data/latest?device_id=CREC1008&tag_id=1"
```
### 性能优化补充
1. **数据分片存储**:
```python
# 在数据库中按时间分表
def get_table_name(timestamp):
dt = datetime.fromisoformat(timestamp)
return f"data_{dt.year}_{dt.month}"
```
2. **数据压缩**:
```python
# 存储前压缩数据
import zlib
compressed = zlib.compress(message)
```
3. **缓存机制**:
```python
# 使用LRU缓存最近查询
from functools import lru_cache
@lru_cache(maxsize=1000)
def get_tag_definition(tag_id):
# 数据库查询
```
4. **监控端点**:
```python
@app.get("/system/status")
async def system_status():
return {
"websocket_connections": len(websockets.connections),
"db_size": os.path.getsize("data.db")
}
```
### 使用场景示例
1. **实时监控仪表盘**:
```javascript
// 前端使用WebSocket实时更新数据
const ws = new WebSocket('ws://server:8765/CREC1008');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
updateDashboard(data);
}
```
2. **历史数据分析**:
```python
# Python分析历史数据
import pandas as pd
import sqlite3
conn = sqlite3.connect('data.db')
df = pd.read_sql(
"SELECT * FROM parsed_data WHERE tag_id=129 AND timestamp > '2023-01-01'",
conn
)
df.plot(x='timestamp', y='value')
```
3. **报警系统集成**:
```python
# 检查刀盘转速异常
def check_alert(data):
if data['tag_id'] == 129 and data['value'] > 10.0: # 刀盘转速
send_alert("刀盘转速过高!")
```