430 lines
14 KiB
Python
430 lines
14 KiB
Python
# Device routes
|
|
from fastapi import FastAPI, HTTPException
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from typing import Optional
|
|
from motor.motor_asyncio import AsyncIOMotorClient
|
|
from bson import ObjectId
|
|
from pydantic import BaseModel, Field
|
|
from contextlib import asynccontextmanager
|
|
from datetime import datetime, timedelta
|
|
import uuid
|
|
from redis import asyncio as aioredis
|
|
import asyncio
|
|
# Database Configuration
|
|
MONGODB_URL = "mongodb://lab:[email protected]:27017/lab"
|
|
|
|
# 更新 FastAPI 实例化
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
# 启动时执行
|
|
await connect_to_mongo()
|
|
await init_redis()
|
|
yield
|
|
# 关闭时执行
|
|
await close_mongo_connection()
|
|
|
|
app = FastAPI(lifespan=lifespan)
|
|
|
|
# CORS configuration
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
expose_headers=["*"]
|
|
)
|
|
|
|
# MongoDB setup
|
|
class PyObjectId(ObjectId):
|
|
"""
|
|
自定义ObjectId类,用于在Pydantic模型中处理MongoDB的ObjectId
|
|
"""
|
|
@classmethod
|
|
def __get_validators__(cls):
|
|
yield cls.validate
|
|
|
|
@classmethod
|
|
def validate(cls, v, handler):
|
|
if not ObjectId.is_valid(v):
|
|
raise ValueError("Invalid ObjectId")
|
|
return ObjectId(v)
|
|
|
|
@classmethod
|
|
def __get_pydantic_json_schema__(cls, _schema_cache, **_kwargs):
|
|
return {
|
|
'type': 'string',
|
|
'description': 'ObjectId',
|
|
'pattern': r'^[0-9a-fA-F]{24}$'
|
|
}
|
|
|
|
@classmethod
|
|
def __modify_schema__(cls, field_schema):
|
|
field_schema.update(
|
|
type='string',
|
|
description='ObjectId',
|
|
pattern=r'^[0-9a-fA-F]{24}$'
|
|
)
|
|
|
|
class SensorModel(BaseModel):
|
|
"""传感器模型"""
|
|
index: str
|
|
sensor_name: str
|
|
sensor_type: str
|
|
unit: str
|
|
|
|
class DeviceModel(BaseModel):
|
|
id: Optional[PyObjectId] = Field(alias="_id", default=None)
|
|
device_name: str
|
|
device_type: str
|
|
device_number: int
|
|
serial_numbers: list = Field(default_factory=list)
|
|
sensors: list[SensorModel] = Field(default_factory=list)
|
|
|
|
class Config:
|
|
populate_by_name = True
|
|
arbitrary_types_allowed = True
|
|
json_encoders = {ObjectId: str}
|
|
|
|
def dict(self, *args, **kwargs):
|
|
"""确保返回的字典包含 _id 字段"""
|
|
kwargs["by_alias"] = True
|
|
return super().dict(*args, **kwargs)
|
|
|
|
# Database connection
|
|
class Database:
|
|
"""数据库连接管理类"""
|
|
client: AsyncIOMotorClient = None
|
|
|
|
db = Database()
|
|
|
|
async def get_database():
|
|
"""获取数据库连接"""
|
|
if db.client is None:
|
|
await connect_to_mongo()
|
|
return db.client["lab"]
|
|
|
|
async def connect_to_mongo():
|
|
try:
|
|
db.client = AsyncIOMotorClient(MONGODB_URL)
|
|
# 验证连接
|
|
await db.client.admin.command('ping')
|
|
print("Successfully connected to MongoDB")
|
|
except Exception as e:
|
|
print(f"Could not connect to MongoDB: {e}")
|
|
raise
|
|
|
|
async def close_mongo_connection():
|
|
db.client.close()
|
|
|
|
@app.post("/devices/devices")
|
|
async def create_device(device: DeviceModel):
|
|
"""
|
|
创建新的设备
|
|
|
|
参数:
|
|
device: 设备模型,包含设备信息和传感器列表
|
|
|
|
返回:
|
|
dict: 包含创建成功消息和新设备ID
|
|
"""
|
|
db = await get_database()
|
|
|
|
# 验证传感器数量是否与通道数匹配
|
|
if len(device.sensors) != device.device_number:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"传感器数量({len(device.sensors)})与通道数({device.device_number})不匹配"
|
|
)
|
|
|
|
device_dict = device.model_dump(exclude={"id"})
|
|
result = await db.devices.insert_one(device_dict)
|
|
|
|
return {
|
|
"message": "设备创建成功",
|
|
"id": str(result.inserted_id)
|
|
}
|
|
|
|
@app.get("/devices/devices")
|
|
async def get_devices():
|
|
"""
|
|
获取所有设备列表
|
|
|
|
返回:
|
|
List[dict]: 设备列表,包含所有设备的信息
|
|
"""
|
|
db = await get_database()
|
|
|
|
devices = []
|
|
async for device in db.devices.find():
|
|
# 确保 _id 被正确序列化
|
|
device["_id"] = str(device["_id"])
|
|
devices.append(device)
|
|
|
|
return devices
|
|
|
|
# 添加删除路由
|
|
@app.delete("/devices/devices/{device_id}")
|
|
async def delete_device(device_id: str):
|
|
"""
|
|
删除指定的设备
|
|
|
|
参数:
|
|
device_id: 设备的ObjectId字符串
|
|
|
|
返回:
|
|
dict: 包含删除操作的结果信息
|
|
"""
|
|
try:
|
|
db = await get_database()
|
|
result = await db.devices.delete_one({"_id": ObjectId(device_id)})
|
|
|
|
if result.deleted_count == 0:
|
|
return {"message": "未找到指定设备"}
|
|
|
|
return {"message": "设备删除成功"}
|
|
except Exception as e:
|
|
return {"message": f"删除设备失败: {str(e)}"}
|
|
# 添加生成序列号的函数
|
|
def generate_serial_number(device_type, batch_number, device_id):
|
|
"""生成序列号
|
|
格式: XX00-UUID(10位)-设备ID后6位
|
|
"""
|
|
# 获取前缀(设备类型前两位大写)
|
|
prefix = f"{device_type[:2].upper()}{batch_number:02d}"
|
|
|
|
# 生成UUID并取前10位
|
|
uuid_part = str(uuid.uuid4()).replace('-', '')[:10]
|
|
|
|
# 获取设备ID的后6位作为校验码
|
|
checksum = str(device_id)[-6:]
|
|
|
|
return f"{prefix}-{uuid_part}-{checksum}"
|
|
|
|
# 添加生成序列号的路由
|
|
@app.post("/devices/devices/{device_id}/serials")
|
|
async def generate_device_serials(device_id: str):
|
|
"""为设备生成10个新的序列号"""
|
|
try:
|
|
db = await get_database()
|
|
|
|
# 获取设备信息
|
|
device = await db.devices.find_one({"_id": ObjectId(device_id)})
|
|
if not device:
|
|
return {"message": "设备不存在"}
|
|
|
|
# 获取当前序列号数量作为批次号
|
|
current_batch = len(device.get("serial_numbers", [])) // 10 + 1
|
|
|
|
# 生成10个新序列号
|
|
new_serials = []
|
|
for _ in range(10):
|
|
serial = generate_serial_number(
|
|
device["device_type"],
|
|
current_batch,
|
|
device_id # 传入设备ID
|
|
)
|
|
new_serials.append({
|
|
"serial": serial,
|
|
"status": "available",
|
|
"created_at": datetime.now().isoformat()
|
|
})
|
|
|
|
# 更新设备记录
|
|
result = await db.devices.update_one(
|
|
{"_id": ObjectId(device_id)},
|
|
{"$push": {"serial_numbers": {"$each": new_serials}}}
|
|
)
|
|
|
|
return {
|
|
"message": "序列号生成成功",
|
|
"serials": new_serials
|
|
}
|
|
except Exception as e:
|
|
return {"message": f"生成序列号失败: {str(e)}"}
|
|
|
|
# 添加获取序列号的路由
|
|
@app.get("/devices/devices/{device_id}/serials")
|
|
async def get_device_serials(device_id: str):
|
|
"""获取设备的所有序列号"""
|
|
try:
|
|
db = await get_database()
|
|
device = await db.devices.find_one({"_id": ObjectId(device_id)})
|
|
|
|
if not device:
|
|
return {"message": "设备不存在"}
|
|
|
|
return {
|
|
"device_id": str(device["_id"]),
|
|
"device_name": device["device_name"],
|
|
"serial_numbers": device.get("serial_numbers", [])
|
|
}
|
|
except Exception as e:
|
|
return {"message": f"获取序列号失败: {str(e)}"}
|
|
|
|
# 添加更新设备的路由
|
|
@app.put("/devices/devices/{device_id}")
|
|
async def update_device(device_id: str, device: DeviceModel):
|
|
"""更新设备信息"""
|
|
try:
|
|
db = await get_database()
|
|
|
|
# 首先获取现有设备数据,特别是serial_numbers
|
|
existing_device = await db.devices.find_one({"_id": ObjectId(device_id)})
|
|
if not existing_device:
|
|
raise HTTPException(status_code=404, detail="设备不存在")
|
|
|
|
# 验证传感器数量
|
|
if len(device.sensors) != device.device_number:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"传感器数量({len(device.sensors)})与通道数({device.device_number})不匹配"
|
|
)
|
|
|
|
# 只更新允许修改的字段,保留原有的serial_numbers
|
|
update_data = {
|
|
"device_name": device.device_name,
|
|
"device_type": device.device_type,
|
|
"device_number": device.device_number,
|
|
"sensors": [sensor.dict() for sensor in device.sensors],
|
|
"serial_numbers": existing_device.get("serial_numbers", []) # 保留原有的序列号
|
|
}
|
|
|
|
result = await db.devices.update_one(
|
|
{"_id": ObjectId(device_id)},
|
|
{"$set": update_data}
|
|
)
|
|
|
|
return {"message": "设备更新成功"}
|
|
except Exception as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
# 添加Redis连接配置
|
|
REDIS_URL = "redis://:Obscura@[email protected]:6379"
|
|
redis_client = None
|
|
|
|
# 用于存储上一次检查时的stream长度
|
|
last_stream_lengths = {}
|
|
|
|
# 在全局变量中添加设备活跃时间记录
|
|
device_last_active = {}
|
|
|
|
# 初始化Redis连接
|
|
async def init_redis():
|
|
global redis_client
|
|
if redis_client is None:
|
|
redis_client = await aioredis.from_url(REDIS_URL, db=200)
|
|
return redis_client
|
|
|
|
@app.get("/devices/devices/online")
|
|
async def get_online_devices():
|
|
"""获取在线设备列表"""
|
|
try:
|
|
redis = await init_redis()
|
|
db = await get_database()
|
|
|
|
keys = [key.decode('utf-8') for key in await redis.keys("experiment:*")]
|
|
online_devices = {}
|
|
current_time = datetime.now()
|
|
|
|
# 用于临时存储序列号的活跃状态
|
|
serial_active_status = {}
|
|
stream_lengths = {} # 添加存储stream长度的字典
|
|
|
|
print("\n=== 检查 Stream 活跃状态 ===")
|
|
# 检查所有streams
|
|
for key in keys:
|
|
try:
|
|
serial_number = key.split(":")[-1]
|
|
current_length = await redis.xlen(key)
|
|
last_length = last_stream_lengths.get(key, 0)
|
|
|
|
# 记录当前stream长度
|
|
stream_lengths[key] = {
|
|
'current': current_length,
|
|
'last': last_length
|
|
}
|
|
|
|
# 如果该序列号还未被标记为活跃,且当前stream有更新
|
|
if not serial_active_status.get(serial_number, False) and current_length != last_length:
|
|
serial_active_status[serial_number] = True
|
|
device_last_active[serial_number] = current_time
|
|
last_stream_lengths[key] = current_length
|
|
|
|
except Exception as e:
|
|
print(f"处理key {key} 时出错: {str(e)}")
|
|
continue
|
|
|
|
# 处理在线设备
|
|
for serial_number, is_active in serial_active_status.items():
|
|
last_active_time = device_last_active.get(serial_number)
|
|
|
|
# 如果设备在过去5分钟内有活动,则认为它在线
|
|
if last_active_time and (current_time - last_active_time) <= timedelta(minutes=5):
|
|
device = await db.devices.find_one({
|
|
"serial_numbers.serial": serial_number
|
|
})
|
|
|
|
if device:
|
|
online_devices[serial_number] = {
|
|
"device_id": str(device["_id"]),
|
|
"device_name": device["device_name"],
|
|
"device_type": device["device_type"],
|
|
"device_number": device["device_number"],
|
|
"serial_number": serial_number,
|
|
"last_active": last_active_time.isoformat(),
|
|
"sensors": device["sensors"],
|
|
"status": "active" if is_active else "idle",
|
|
"stream_info": { # 添加stream信息
|
|
k: v for k, v in stream_lengths.items()
|
|
if k.endswith(serial_number)
|
|
}
|
|
}
|
|
|
|
print("\n=== 在线设备统计 ===")
|
|
print(f"在线设备数量: {len(online_devices)}")
|
|
for device in online_devices.values():
|
|
print(f"\n设备: {device['device_name']} ({device['serial_number']})")
|
|
print(f" - 状态: {device['status']}")
|
|
print(f" - 最后活跃: {device['last_active']}")
|
|
print(" - Stream信息:")
|
|
for stream_key, info in device['stream_info'].items():
|
|
print(f" * {stream_key}: 当前={info['current']}, 上次={info['last']}")
|
|
|
|
response_data = {
|
|
"online_count": len(online_devices),
|
|
"devices": list(online_devices.values())
|
|
}
|
|
return response_data
|
|
|
|
except Exception as e:
|
|
print(f"获取在线设备出错: {str(e)}")
|
|
raise HTTPException(status_code=500, detail=f"获取在线设备失败: {str(e)}")
|
|
|
|
# 修改清理函数
|
|
async def cleanup_old_records():
|
|
while True:
|
|
current_time = datetime.now()
|
|
# 清理stream长度记录
|
|
keys_to_remove = []
|
|
for key in last_stream_lengths.keys():
|
|
if current_time - last_stream_lengths.get(key, {}).get('last_check', current_time) > timedelta(minutes=10):
|
|
keys_to_remove.append(key)
|
|
|
|
for key in keys_to_remove:
|
|
last_stream_lengths.pop(key, None)
|
|
|
|
# 清理过期的设备活跃记录
|
|
serials_to_remove = []
|
|
for serial, last_active in device_last_active.items():
|
|
if current_time - last_active > timedelta(minutes=10):
|
|
serials_to_remove.append(serial)
|
|
|
|
for serial in serials_to_remove:
|
|
device_last_active.pop(serial, None)
|
|
|
|
await asyncio.sleep(300) # 每5分钟清理一次
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=6001) |