web_app_2006.py 65 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651
  1. # C:\Users\chx_s\AppData\Local\Programs\Python\Python310\Scripts\pip.exe
  2. # C:\Users\chx_s\AppData\Local\Programs\Python\Python310\python.exe
  3. from flask import Flask, render_template, request, redirect, url_for, session, jsonify, send_from_directory
  4. from datetime import datetime, timedelta
  5. import os
  6. import json
  7. import threading
  8. import paho.mqtt.client as mqtt
  9. import logging
  10. from functools import wraps
  11. import random
  12. import math
  13. import time
  14. import subprocess
  15. import platform as sys_platform
  16. import shutil
  17. import traceback
  18. # 自定义
  19. from pos_manager import frame
  20. # 应用版本信息
  21. APP_VERSION = '1.0.18'
  22. # 应用日志文件路径
  23. LOG_FILE = 'app.log'
  24. # 配置日志
  25. # 自定义日志格式化器,统一应用日志和Flask内部日志的时间格式
  26. class CustomFormatter(logging.Formatter):
  27. def format(self, record):
  28. if not record.name.startswith('werkzeug'):
  29. # 应用日志格式
  30. record.timestamp = f'APP:'+datetime.now().strftime('%Y-%m-%d %H:%M:%S')
  31. return '{0.timestamp} - {0.levelname} - {0.funcName}[line:{0.lineno}]: {0.msg}'.format(record)
  32. else:
  33. # Flask内部日志(werkzeug)格式
  34. return super().format(record)
  35. # 创建处理器
  36. file_handler = logging.FileHandler(LOG_FILE, encoding='utf-8')
  37. stream_handler = logging.StreamHandler()
  38. # 设置格式器
  39. formatter = CustomFormatter('%(message)s')
  40. file_handler.setFormatter(formatter)
  41. stream_handler.setFormatter(formatter)
  42. # 配置根日志器
  43. logging.basicConfig(
  44. level=logging.INFO,
  45. handlers=[file_handler, stream_handler]
  46. )
  47. # 配置Flask内部日志级别
  48. werkzeug_logger = logging.getLogger('werkzeug')
  49. werkzeug_logger.setLevel(logging.INFO)
  50. logger = logging.getLogger(__name__)
  51. # 记录应用启动时间
  52. app_start_time = time.time()
  53. app = Flask(__name__)
  54. app.secret_key = 'supersecretkey'
  55. app.config['UPLOAD_FOLDER'] = 'static/uploads'
  56. app.config['ALLOWED_EXTENSIONS'] = {'bin', 'zip', 'tar', 'rar'}
  57. app.config['DATA_FOLDER'] = 'data'
  58. app.config['APP_UPGREADE_FOLDER_TEMP'] = 'app_upgrade_temp'
  59. # 关键配置:禁用 ASCII 转义,确保中文正常显示
  60. app.config['JSON_AS_ASCII'] = False
  61. # 确保目录存在
  62. os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
  63. os.makedirs(app.config['DATA_FOLDER'], exist_ok=True)
  64. # 数据文件路径
  65. USERS_PATH = os.path.join(app.config['DATA_FOLDER'], 'users.json')
  66. FIRMWARES_PATH = os.path.join(app.config['DATA_FOLDER'], 'firmwares.json')
  67. DEVICES_PATH = os.path.join(app.config['DATA_FOLDER'], 'devices.json')
  68. PLATFORMS_PATH = os.path.join(app.config['DATA_FOLDER'], 'platforms.json')
  69. # 全局变量
  70. # MQTT客户端管理
  71. mqtt_clients = {}
  72. mqtt_threads = {}
  73. mqtt_lock = threading.Lock()
  74. mqtt_sub_topic = [("cpyypt/up/2006/#", 0)]
  75. mqtt_pub_topic = "cpyypt/down/2006/0000000101"
  76. # 定时器
  77. threading_timer = {
  78. 'mqtt_status_check_timer': None,
  79. 'mqtt_status_check_timeout': 2,
  80. 'dev_online_check_timer': None,
  81. 'dev_online_check_timeout': 5,
  82. 'per_second_timer': None,
  83. 'per_second_timeout': 2,
  84. }
  85. device_speed = {}
  86. wr_devices_lock = threading.Lock()
  87. @app.route('/favicon.ico')
  88. def favicon():
  89. return send_from_directory("static", "favicon.ico", mimetype='image/vnd.microsoft.icon')
  90. @app.before_request
  91. def update_last_activity():
  92. mqtt_last_activity['last_time'] = time.time()
  93. # logger.info(f"mqtt_last_activity: {mqtt_last_activity}")
  94. # 输出例如 "1天8小时37分钟36秒"
  95. def format_timedelta(seconds):
  96. # 使用timedelta来创建一个时间差对象
  97. delta = timedelta(seconds=seconds)
  98. # 获取总天数
  99. total_days = delta.days
  100. # 获取剩余的小时数、分钟数和秒数
  101. hours, remainder = divmod(delta.seconds, 3600)
  102. minutes, seconds = divmod(remainder, 60)
  103. # 构建格式化字符串
  104. parts = []
  105. if total_days > 0:
  106. parts.append(f"{total_days}天")
  107. if hours > 0:
  108. parts.append(f"{hours}小时")
  109. if minutes > 0:
  110. parts.append(f"{minutes}分钟")
  111. if seconds > 0:
  112. parts.append(f"{seconds}秒")
  113. # 如果所有的部分都没有被添加,至少显示一秒
  114. if not parts:
  115. parts.append("0秒")
  116. return ' '.join(parts)
  117. # 获取应用运行时长
  118. @app.route('/api/run_time')
  119. def get_run_time():
  120. global app_start_time
  121. run_time_str = format_timedelta(int(time.time() - app_start_time))
  122. # logger.info(f"run_time_str: {run_time_str}")
  123. return jsonify({'run_time': run_time_str})
  124. # 上下文处理器
  125. @app.context_processor
  126. def inject_common_data():
  127. return {
  128. 'datetime': datetime,
  129. 'app_version': APP_VERSION
  130. }
  131. # 初始化数据
  132. def init_data():
  133. # 初始化用户数据
  134. if not os.path.exists(USERS_PATH):
  135. with open(USERS_PATH, 'w', encoding='utf-8') as f:
  136. json.dump({'admin': 'admin'}, f, ensure_ascii=False)
  137. # 初始化固件数据
  138. if not os.path.exists(FIRMWARES_PATH):
  139. init_firmwares = [
  140. {'id': 1, 'filename': 'pos.bin', 'upload_time': '2025-08-01 13:08:30', 'remark': '初始版本'},
  141. {'id': 2, 'filename': 'ITSF-POS V2.3.bin', 'upload_time': '2025-08-01 14:10:09', 'remark': '基础功能版'}
  142. ]
  143. with open(FIRMWARES_PATH, 'w', encoding='utf-8') as f:
  144. json.dump(init_firmwares, f, ensure_ascii=False, indent=2)
  145. # 初始化设备数据
  146. if not os.path.exists(DEVICES_PATH):
  147. init_devices()
  148. with open(DEVICES_PATH, 'w', encoding='utf-8') as f:
  149. json.dump(init_devices, f, ensure_ascii=False, indent=2)
  150. # 初始化云平台数据
  151. if not os.path.exists(PLATFORMS_PATH):
  152. platform_d = {
  153. "current_platform": "test_env",
  154. "platform_l": {
  155. "aliyun_prod_env": {
  156. "id": 1,
  157. "name": "阿里云生产",
  158. "status": "connected",
  159. "ip": "mqtt.cpyypt.cn",
  160. "port": 9000,
  161. "sub_topic ": mqtt_sub_topic,
  162. "pub_topic ": mqtt_pub_topic,
  163. "client_id": f"pos_mng_{random.randint(0, 100)}",
  164. "username": "cpyypt",
  165. "password": "1SvTlvm1VCawSzS"
  166. },
  167. "test_env": {
  168. "id": 2,
  169. "name": "测试环境",
  170. "status": "disconnected",
  171. "ip": "test-mqtt.cpyypt.cn",
  172. "port": 9000,
  173. "sub_topic ": mqtt_sub_topic,
  174. "pub_topic ": mqtt_pub_topic,
  175. "client_id": f"pos_mng_{random.randint(0, 100)}",
  176. "username": "admin",
  177. "password": "houjianwei"
  178. },
  179. "localize_a_env": {
  180. "id": 3,
  181. "name": "本地化A云平台",
  182. "status": "disconnected",
  183. "ip": "mqtt.localize_a_env.com",
  184. "port": 1883,
  185. "sub_topic ": mqtt_sub_topic,
  186. "pub_topic ": mqtt_pub_topic,
  187. "client_id": f"pos_mng_{random.randint(0, 100)}",
  188. "username": "localize_a_env_user",
  189. "password": "localize_a_env_pass"
  190. }
  191. }
  192. }
  193. # platform_d["platform_l"]["aliyun_prod_env"]["sub_topic"] = [("cpyypt/up/2006/#", 0)]
  194. # platform_d["platform_l"]["aliyun_prod_env"]["pub_topic"] = [("cpyypt/down/2006/#", 0)]
  195. # platform_d["platform_l"]["aliyun_prod_env"]["client_id"] = f"pos_mng_{random.randint(0, 100)}"
  196. with open(PLATFORMS_PATH, 'w', encoding='utf-8') as f:
  197. json.dump(platform_d, f, ensure_ascii=False, indent=2)
  198. # 数据加载函数
  199. def load_users():
  200. with open(USERS_PATH, 'r', encoding='utf-8') as f:
  201. return json.load(f)
  202. def load_firmwares():
  203. with open(FIRMWARES_PATH, 'r', encoding='utf-8') as f:
  204. return json.load(f)
  205. load_devices_first = True # 标记第1次加载
  206. # 创建线程锁对象用于保护load_devices函数
  207. def load_devices():
  208. # 使用线程锁确保函数的互斥访问
  209. with wr_devices_lock:
  210. devices = []
  211. if not os.path.exists(DEVICES_PATH):
  212. logger.info(f"设备配置文件不存在,初始化数据: {DEVICES_PATH}")
  213. init_data()
  214. with open(DEVICES_PATH, 'r', encoding='utf-8') as f:
  215. f_r = f.read()
  216. # logger.info(f"加载设备配置文件: {DEVICES_PATH}, 文件内容长度: {len(f_r)}")
  217. try:
  218. devices = json.loads(f_r)
  219. except json.JSONDecodeError as e:
  220. logger.error(f"解析设备配置文件失败: e:{e},f_r:{f_r}")
  221. stack = traceback.extract_stack()
  222. if stack:
  223. logger.info(f"调用信息 {stack[-3][2]} , {stack[-2][2]} , {stack[-1][2]} ")
  224. # 尝试重新初始化
  225. devices = init_devices()
  226. # 程序启动时,强制将所有所有统计信息为0
  227. global load_devices_first
  228. if load_devices_first :
  229. load_devices_first = False # 第1次加载完成,标记为False
  230. for device in devices:
  231. device['online'] = '离线'
  232. device['realtime'] = 0
  233. device['run_time'] = 0
  234. device['receive_sum'] = 0
  235. device['send_sum'] = 0
  236. device['abnormal_sum'] = 0
  237. device['success_rate'] = 0
  238. device['speed'] = 0
  239. # 保存强制更新后的状态
  240. save_devices(devices)
  241. return devices
  242. load_platforms_first = True # 标记第1次加载
  243. def load_platforms():
  244. try:
  245. if not os.path.exists(PLATFORMS_PATH):
  246. logger.info(f"云平台配置文件不存在,初始化数据: {PLATFORMS_PATH}")
  247. init_data()
  248. # logger.info(f"加载云平台配置文件: {PLATFORMS_PATH}")
  249. with open(PLATFORMS_PATH, 'r', encoding='utf-8') as f:
  250. file_content = f.read().strip()
  251. if not file_content:
  252. logger.error("云平台配置文件为空")
  253. # 如果文件为空,重新初始化
  254. init_data()
  255. with open(PLATFORMS_PATH, 'r', encoding='utf-8') as f_new:
  256. platform_d = json.load(f_new)
  257. else:
  258. try:
  259. platform_d = json.loads(file_content)
  260. except json.JSONDecodeError as e:
  261. logger.error(f"解析云平台配置文件失败: {e}")
  262. # 尝试重新初始化
  263. init_data()
  264. with open(PLATFORMS_PATH, 'r', encoding='utf-8') as f_new:
  265. platform_d = json.load(f_new)
  266. # 程序启动时,强制将所有云平台状态设置为已断开
  267. global load_platforms_first
  268. if load_platforms_first and 'platform_l' in platform_d:
  269. load_platforms_first = False # 第1次加载完成,标记为False
  270. for platform_id in platform_d['platform_l']:
  271. platform_d['platform_l'][platform_id]['status'] = 'disconnected'
  272. platform_d['platform_l'][platform_id]['client_id'] = "pos_mng_%s_%d" % (os.environ['COMPUTERNAME'], random.randint(0, 100))
  273. # 保存强制更新后的状态
  274. save_platforms(platform_d)
  275. return platform_d
  276. except Exception as e:
  277. logger.error(f"加载云平台配置时发生异常: {e}")
  278. # 尝试返回一个默认值或重新初始化
  279. init_data()
  280. with open(PLATFORMS_PATH, 'r', encoding='utf-8') as f:
  281. return json.load(f)
  282. return platform_d
  283. def load_platforms_c():
  284. platform_d = load_platforms()
  285. platform_c = {}
  286. for platform_id in platform_d['platform_l']:
  287. platform_c[platform_id] = {}
  288. platform_c[platform_id]['name'] = platform_d['platform_l'][platform_id]['name']
  289. platform_c[platform_id]['status'] = platform_d['platform_l'][platform_id]['status']
  290. platform_c[platform_id]['ip'] = platform_d['platform_l'][platform_id]['ip']
  291. return platform_c
  292. # 数据保存函数
  293. def save_users(users):
  294. with open(USERS_PATH, 'w', encoding='utf-8') as f:
  295. json.dump(users, f, ensure_ascii=False, indent=2)
  296. def save_firmwares(firmwares):
  297. with open(FIRMWARES_PATH, 'w', encoding='utf-8') as f:
  298. json.dump(firmwares, f, ensure_ascii=False, indent=2)
  299. def save_devices(devices):
  300. # with wr_devices_lock:
  301. with open(DEVICES_PATH, 'w', encoding='utf-8') as f:
  302. json.dump(devices, f, ensure_ascii=False, indent=2)
  303. def save_platforms(platform_d):
  304. with open(PLATFORMS_PATH, 'w', encoding='utf-8') as f:
  305. json.dump(platform_d, f, ensure_ascii=False, indent=2)
  306. def add_device(devices,dev_type,dev_sn):
  307. # 检查设备是否已存在
  308. for device in devices:
  309. if device['dev_type'] == dev_type and device['dev_sn'] == dev_sn:
  310. logger.info(f"设备 {dev_type} {dev_sn} 已存在,无需添加")
  311. return
  312. device = {
  313. "dev_type": dev_type,
  314. "dev_sn": dev_sn,
  315. "online": "离线",
  316. "realtime": 0,
  317. "realtime": 0,
  318. "run_time": 0,
  319. "receive_sum": 0,
  320. "send_sum": 0,
  321. "abnormal_sum": 0,
  322. "success_rate": 0,
  323. "speed": 0,
  324. "cloud_platform": "阿里云生产",
  325. "master_type": dev_type,
  326. "master_sn": dev_sn,
  327. "app_version": "未知",
  328. "upgrade_status": "未知",
  329. "reset_times": 0,
  330. "last_reset_type": "00",
  331. "uuid": "未知",
  332. "network_type": "未知",
  333. "wifi_ssid": "未知",
  334. "wifi_password": "未知",
  335. "sim_status": 1,
  336. "free_fifo": 0,
  337. "ip": "x.x.x.x"
  338. }
  339. devices.append(device)
  340. # 先排序,再保存
  341. devices.sort(key=lambda x: x['dev_sn'])
  342. save_devices(devices)
  343. return devices
  344. def init_devices():
  345. devices = []
  346. dev_type = '2006'
  347. dev_sns = ['101', '102', '103', '104', '105', '106']
  348. for dev_sn in dev_sns:
  349. device = {
  350. "dev_type": dev_type,
  351. "dev_sn": dev_sn,
  352. "online": "离线",
  353. "realtime": 0,
  354. "realtime": 0,
  355. "run_time": 0,
  356. "receive_sum": 0,
  357. "send_sum": 0,
  358. "abnormal_sum": 0,
  359. "success_rate": 0,
  360. "speed": 0,
  361. "cloud_platform": "阿里云生产",
  362. "master_type": dev_type,
  363. "master_sn": dev_sn,
  364. "app_version": "未知",
  365. "upgrade_status": "未知",
  366. "reset_times": 0,
  367. "last_reset_type": "00",
  368. "uuid": "未知",
  369. "network_type": "未知",
  370. "wifi_ssid": "未知",
  371. "wifi_password": "未知",
  372. "sim_status": 1,
  373. "free_fifo": 0,
  374. "ip": "x.x.x.x"
  375. }
  376. devices.append(device)
  377. # 先排序,再保存
  378. devices.sort(key=lambda x: x['dev_sn'])
  379. save_devices(devices)
  380. return devices
  381. # MQTT回调函数
  382. def on_connect(client, userdata, flags, rc):
  383. platform_id = userdata['platform_id']
  384. logger.info(f"MQTT连接成功 - 云平台: {platform_id}, 结果代码: {rc}")
  385. pub_topic = userdata['pub_topic']
  386. client.subscribe(mqtt_sub_topic)
  387. logger.info(f"已订阅主题: {mqtt_sub_topic}")
  388. with mqtt_lock:
  389. platform_d = load_platforms()
  390. if platform_id in platform_d['platform_l']:
  391. platform_d['platform_l'][platform_id]['status'] = 'connected'
  392. save_platforms(platform_d)
  393. def on_disconnect(client, userdata, rc):
  394. platform_id = userdata['platform_id']
  395. logger.info(f"MQTT断开连接 - 云平台: {platform_id}, 结果代码: {rc}")
  396. with mqtt_lock:
  397. platform_d = load_platforms()
  398. if platform_id in platform_d['platform_l']:
  399. platform_d['platform_l'][platform_id]['status'] = 'disconnected'
  400. save_platforms(platform_d)
  401. def on_message(client, userdata, msg):
  402. platform_id = userdata['platform_id']
  403. try:
  404. payload_str = msg.payload.decode('gb2312')
  405. except UnicodeDecodeError:
  406. try:
  407. payload_str = msg.payload.decode('utf-8')
  408. except UnicodeDecodeError:
  409. payload_str = msg.payload.hex().upper()
  410. # logger.info(f"收到消息 - 云平台: {platform_id}, 主题: {msg.topic}, 内容: {payload_str}")
  411. # 如果长时间未操作,自动断开MQTT连接
  412. platform_d = load_platforms()
  413. # """
  414. get_platform_status = next((value['status'] for key, value in platform_d['platform_l'].items() if key == platform_id), None)
  415. if get_platform_status == 'connected':
  416. mqtt_disconnect_time_out = 5*60
  417. if time.time() - mqtt_last_activity['last_time'] > mqtt_disconnect_time_out: # 5分钟无操作
  418. logger.info(f"云平台 {platform_id} 长时间未操作,超过{mqtt_disconnect_time_out}秒,自动断开连接")
  419. platform_d['platform_l'][platform_id]['status'] = 'disconnected'
  420. stop_mqtt_client(platform_id)
  421. logger.info(f"云平台 {platform_id} 自动断开连接,已完成")
  422. return
  423. # """
  424. dev_type = msg.topic.split("/")[-2]
  425. dev_sn = msg.topic.split("/")[-1].lstrip('0')
  426. devices = load_devices()
  427. device = None if devices == None else next((item for item in devices if item['dev_sn'] == dev_sn), None)
  428. if device == None:
  429. devices = init_devices()
  430. device = next((item for item in devices if item['dev_sn'] == dev_sn), None)
  431. if device:
  432. if(device.get('online',None) != '在线'):
  433. logger.info(f"云平台:{platform_id}, 设备 {dev_sn} 上线")
  434. device['online'] = '在线'
  435. device['realtime'] = int(time.time())
  436. platform_name = next((value['name'] for key, value in platform_d['platform_l'].items() if key == platform_id), None)
  437. device['cloud_platform'] = platform_name
  438. save_devices(devices)
  439. if device and type(msg.payload)== bytes and len(msg.payload) > 4 and payload_str[0:4] == "FEFE":
  440. # if type(msg.payload)== bytes and len(msg.payload) > 4 and msg.payload[0:2] == 0xFEFE:
  441. try:
  442. header, body, info_msg = frame.parse_data(client, msg.payload, msg.topic)
  443. if len(header) > 0 and len(body) > 0:
  444. pro_ver = header.get('pro_ver', None)
  445. msg_type1 = header.get('msg_type1', None)
  446. msg_type2 = header.get('msg_type2', None)
  447. if pro_ver == 0x01:
  448. if msg_type1 == 0x02:
  449. if msg_type2 == 0x2003:
  450. r_body = {}
  451. r_body['master_type'] = body.get('master_type', None)
  452. r_body['master_sn'] = body.get('master_sn', None)
  453. r_body['app_version'] = body.get('app_version', None)
  454. r_body['reset_times'] = body.get('reset_times', None)
  455. r_body['last_reset_type'] = body.get('last_reset_type', None)
  456. r_body['uuid'] = body.get('uuid', None)
  457. r_body['run_time'] = body.get('run_time', None)
  458. r_body['receive_sum'] = body.get('receive_sum', None)
  459. r_body['send_sum'] = body.get('send_sum', None)
  460. r_body['network_type'] = body.get('network_type', None)
  461. r_body['cloud_platform'] = body.get('cloud_platform', None)
  462. r_body['sim_status'] = body.get('sim_status', None)
  463. r_body['free_fifo'] = body.get('free_fifo', None)
  464. r_body['ip'] = body.get('ip', None)
  465. # 发送计数器
  466. device_send_sum = device.get('send_sum', None)
  467. if r_body['send_sum'] and device_send_sum:
  468. if r_body['send_sum'] < device_send_sum:
  469. os.makedirs('dev_log', exist_ok=True)
  470. with open(f"dev_log/{dev_sn.zfill(10)}.log", 'a', encoding='utf-8') as f:
  471. w_msg = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] + "-->:"
  472. w_msg = w_msg + f"%s,%10u,%10u." % (dev_sn.zfill(10), device_send_sum, r_body['send_sum'])
  473. f.write(w_msg+ "\n")
  474. # 至少有1个不为None,则继续
  475. if any(r_body.values()):
  476. # 先整理一下字段
  477. if r_body['network_type'] is not None:
  478. if r_body['network_type'] == 1:
  479. r_body['network_type'] = 'WiFi'
  480. elif r_body['network_type'] == 2:
  481. r_body['network_type'] = '有线网'
  482. else:
  483. r_body['network_type'] = '未知'
  484. if r_body['cloud_platform'] is not None:
  485. if r_body['cloud_platform'] == 1:
  486. r_body['cloud_platform'] = '阿里云生产'
  487. elif r_body['cloud_platform'] == 2:
  488. r_body['cloud_platform'] = '测试环境'
  489. else:
  490. r_body['cloud_platform'] = '未知'
  491. # 有变化,才更新
  492. is_changed = False
  493. for k, v in r_body.items():
  494. if v is not None:
  495. if k not in device or device[k] != v:
  496. is_changed = True
  497. device[k] = v
  498. if is_changed:
  499. logger.info(f"云平台:{platform_id}, 设备 {dev_sn} 接收到MQTT消息,参数有更新")
  500. if dev_sn not in device_speed:
  501. device_speed[dev_sn] = []
  502. device_speed[dev_sn].append({
  503. 'svr_run_time':time.time(),
  504. 'dev_run_time':r_body['run_time'],
  505. 'send_sum':r_body['send_sum'],
  506. 'receive_sum':r_body['receive_sum'],
  507. })
  508. while len(device_speed[dev_sn]) > 11:
  509. device_speed[dev_sn].pop(0)
  510. if len(device_speed[dev_sn]) > 1:
  511. run_time_delta = device_speed[dev_sn][-1]['svr_run_time'] - device_speed[dev_sn][0]['svr_run_time']
  512. send_sum_delta = device_speed[dev_sn][-1]['send_sum'] - device_speed[dev_sn][0]['send_sum']
  513. speed = 0 if run_time_delta == 0 else (send_sum_delta / run_time_delta)
  514. else:
  515. speed = 0
  516. device['speed'] = speed
  517. save_devices(devices)
  518. if msg_type1 == 0x04:
  519. if msg_type2 == 0x2001:
  520. upgrade_status = body.get('upgrade_status', None)
  521. device['upgrade_status'] = f"0x%02X" % (upgrade_status) if upgrade_status != None else '未知'
  522. save_devices(devices)
  523. logger.info(f"info_msg:{info_msg}")
  524. except Exception as e:
  525. logger.error(f"解析数据失败 - 云平台: {platform_id}, 错误: {str(e)}")
  526. logger.error(f"解析数据:{' '.join([payload_str[i:i+2] for i in range(0, len(payload_str), 2)])}")
  527. # MQTT客户端线程
  528. def mqtt_client_thread(platform_id):
  529. platform_d = load_platforms()
  530. if platform_id not in platform_d['platform_l']:
  531. logger.error(f"云平台 {platform_id} 不存在")
  532. return
  533. platform = platform_d['platform_l'][platform_id]
  534. client = mqtt.Client(client_id=platform['client_id'])
  535. client.username_pw_set(platform['username'], platform['password'])
  536. # platform['pub_topic'],
  537. # platform['sub_topic']
  538. client.user_data_set({
  539. 'platform_id': platform_id,
  540. 'pub_topic': mqtt_pub_topic,
  541. 'sub_topic': mqtt_sub_topic
  542. })
  543. client.on_connect = on_connect
  544. client.on_disconnect = on_disconnect
  545. client.on_message = on_message
  546. with mqtt_lock:
  547. mqtt_clients[platform_id] = client
  548. try:
  549. client.connect(platform['ip'], platform['port'], 60)
  550. client.loop_forever()
  551. except Exception as e:
  552. logger.error(f"MQTT线程错误 - 云平台: {platform_id}, 错误: {str(e)}")
  553. finally:
  554. # 确保客户端已断开连接
  555. try:
  556. client.disconnect()
  557. except Exception as e:
  558. logger.error(f"MQTT线程结束时断开连接失败 - 云平台: {platform_id}, 错误: {str(e)}")
  559. with mqtt_lock:
  560. if platform_id in mqtt_clients:
  561. del mqtt_clients[platform_id]
  562. if platform_id in mqtt_threads:
  563. del mqtt_threads[platform_id]
  564. try:
  565. client.disconnect()
  566. except:
  567. pass
  568. # MQTT控制函数
  569. def start_mqtt_client(platform_id):
  570. with mqtt_lock:
  571. if platform_id in mqtt_threads and mqtt_threads[platform_id].is_alive():
  572. logger.info(f"MQTT客户端已运行 - 云平台: {platform_id}")
  573. return True
  574. thread = threading.Thread(target=mqtt_client_thread, args=(platform_id,), daemon=True)
  575. with mqtt_lock:
  576. mqtt_threads[platform_id] = thread
  577. thread.start()
  578. logger.info(f"启动MQTT客户端线程 - 云平台: {platform_id}")
  579. return True
  580. mqtt_last_activity = {'last_time':time.time()}
  581. # 定期检查所有MQTT连接状态
  582. def check_all_mqtt_connections():
  583. try:
  584. with mqtt_lock:
  585. platform_d = load_platforms()
  586. if 'platform_l' not in platform_d:
  587. return
  588. for platform_id in platform_d['platform_l']:
  589. # 检查MQTT线程是否存活
  590. # is_connected = platform_id in mqtt_threads and mqtt_threads[platform_id].is_alive()
  591. is_connected = platform_id in mqtt_clients and mqtt_clients[platform_id].is_connected()
  592. current_status = platform_d['platform_l'][platform_id].get('status', '')
  593. # logger.info(f"检查云平台状态 - 云平台: {platform_id}, 当前状态: {current_status}, 是否连接: {is_connected}")
  594. # 如果状态不一致,则更新
  595. if (is_connected and current_status != 'connected') or (not is_connected and current_status != 'disconnected'):
  596. logger.info(f"参数:platform_id={platform_id}, is_connected={is_connected}, current_status ={current_status}")
  597. platform_d['platform_l'][platform_id]['status'] = 'connected' if is_connected else 'disconnected'
  598. logger.info(f"更新云平台状态 - 云平台: {platform_id}, 新状态: {platform_d['platform_l'][platform_id]['status']}")
  599. save_platforms(platform_d)
  600. except Exception as e:
  601. logger.error(f"检查MQTT连接状态出错: {str(e)}")
  602. finally:
  603. # 设置下一次检查(2秒后)
  604. threading_timer['mqtt_status_check_timer'] = threading.Timer(threading_timer['mqtt_status_check_timeout'], check_all_mqtt_connections)
  605. threading_timer['mqtt_status_check_timer'].daemon = True
  606. threading_timer['mqtt_status_check_timer'].start()
  607. def start_mqtt_status_check():
  608. if threading_timer['mqtt_status_check_timer'] is not None:
  609. threading_timer['mqtt_status_check_timer'].cancel()
  610. check_all_mqtt_connections()
  611. def stop_mqtt_client(platform_id):
  612. logger.info(f"断开MQTT连接 - 云平台: {platform_id}")
  613. client = None
  614. # 先获取客户端引用,尽量缩短锁的持有时间
  615. with mqtt_lock:
  616. if platform_id in mqtt_clients:
  617. client = mqtt_clients[platform_id]
  618. # 从字典中移除,避免其他线程访问
  619. del mqtt_clients[platform_id]
  620. # 在锁外执行disconnect,避免死锁
  621. if client:
  622. try:
  623. client.disconnect()
  624. logger.info(f"断开MQTT连接 - 云平台: {platform_id}")
  625. except Exception as e:
  626. logger.error(f"断开MQTT连接失败 - 云平台: {platform_id}, 错误: {str(e)}")
  627. with mqtt_lock:
  628. if platform_id in mqtt_threads:
  629. logger.info(f"等待MQTT线程结束 - 云平台: {platform_id}")
  630. return True
  631. def publish_mqtt_message(platform_id, message, pub_topic=mqtt_pub_topic):
  632. with mqtt_lock:
  633. if platform_id not in mqtt_clients:
  634. logger.error(f"MQTT客户端未连接 - 云平台: {platform_id}")
  635. return False
  636. client = mqtt_clients[platform_id]
  637. platform_d = load_platforms()
  638. # platform = platform_d['platform_l'].get(platform_id, {})
  639. # pub_topic = mqtt_pub_topic # platform.get('pub_topic', '/device/data')
  640. try:
  641. result = client.publish(pub_topic, message, qos=0)
  642. result.wait_for_publish()
  643. if(type(message) == str):
  644. logger.info(f"发布消息成功 - 云平台: {platform_id}, 主题: {pub_topic}, 消息: {message}")
  645. else:
  646. logger.info(f"发布消息成功 - 云平台: {platform_id}, 主题: {pub_topic}, 消息: {message.hex().upper()}")
  647. return True
  648. except Exception as e:
  649. logger.error(f"发布消息失败 - 云平台: {platform_id}, 错误: {str(e)}")
  650. return False
  651. # 辅助函数
  652. def allowed_file(filename):
  653. return '.' in filename and \
  654. filename.rsplit('.', 1)[1].lower() in app.config['ALLOWED_EXTENSIONS']
  655. # 登录验证装饰器
  656. def login_required(f):
  657. @wraps(f)
  658. def decorated_function(*args, **kwargs):
  659. if 'logged_in' not in session:
  660. return redirect(url_for('login', next=request.url))
  661. return f(*args, **kwargs)
  662. return decorated_function
  663. # 路由
  664. @app.route('/')
  665. @login_required
  666. def index():
  667. platform_d = load_platforms()
  668. return render_template(
  669. 'index.html',
  670. title='主页',
  671. # cccc platform_l=platform_d['platform_l'],
  672. platform_c=load_platforms_c(),
  673. current_platform=platform_d['current_platform']
  674. )
  675. @app.route('/api/device_detail/<dev_sn>')
  676. @login_required
  677. def get_device_detail(dev_sn):
  678. devices = load_devices()
  679. device = next((d for d in devices if d['dev_sn'] == dev_sn), None)
  680. if not device:
  681. return jsonify({'error': '设备不存在'}), 404
  682. device['abnormal_sum'] = int(device['receive_sum']) - int(device['send_sum'])
  683. device['success_rate'] = 0 if int(device['receive_sum']) == 0 else 100 * int(device['send_sum']) / int(device['receive_sum'])
  684. # device['speed'] = 0 if int(device['run_time']) == 0 else int(device['send_sum']) / int(device['run_time'] )
  685. device['run_time_str'] = format_timedelta(int(device['run_time']))
  686. return jsonify(device)
  687. @app.route('/login', methods=['GET', 'POST'])
  688. def login():
  689. if request.method == 'POST':
  690. username = request.form['username']
  691. password = request.form['password']
  692. users = load_users()
  693. if username in users and users[username] == password:
  694. session['logged_in'] = True
  695. session['username'] = username
  696. next_page = request.args.get('next', url_for('index'))
  697. return redirect(next_page)
  698. else:
  699. error = '用户名或密码错误'
  700. return render_template('login.html', error=error)
  701. return render_template('login.html', title='用户登录')
  702. @app.route('/logout')
  703. def logout():
  704. session.pop('logged_in', None)
  705. session.pop('username', None)
  706. return redirect(url_for('login'))
  707. @app.route('/app_manage')
  708. @login_required
  709. def app_manage():
  710. logger.info("进入应用管理页面")
  711. return render_template('app_manage.html', title='应用管理')
  712. # 应用管理相关API
  713. @app.route('/api/ping', methods=['GET'])
  714. def ping():
  715. """
  716. 用于检测服务器是否正常运行的简单API
  717. """
  718. return jsonify({"status": "ok", "message": "服务器正常运行中"}), 200
  719. @app.route('/api/restart_app', methods=['POST'])
  720. @login_required
  721. def restart_app():
  722. try:
  723. logger.info("接收到重启应用请求")
  724. # 先返回响应给客户端
  725. response = jsonify({
  726. "status": "restarting",
  727. "message": "应用重启已开始,请等待服务重新启动..."
  728. })
  729. # 使用subprocess.Popen以非阻塞方式执行重启命令
  730. # 确保响应已发送完成后再执行重启
  731. s_p = sys_platform.system().upper()
  732. if s_p == 'WINDOWS':
  733. # 使用start命令在新窗口启动重启脚本,避免阻塞当前进程
  734. subprocess.Popen(['start', 'python', 'restart_app.py'], shell=True)
  735. elif s_p == 'LINUX':
  736. # 在后台执行重启脚本
  737. subprocess.Popen(['./sh_web_app_2006_Svr.sh', 'restart'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  738. logger.info("重启命令已发送,应用将很快重启")
  739. return response, 200
  740. except Exception as e:
  741. logger.error(f"应用重启失败: {str(e)}")
  742. return jsonify({'success': False, 'message': f'重启失败: {str(e)}'})
  743. @app.route('/api/update_app', methods=['POST'])
  744. @login_required
  745. def update_app():
  746. try:
  747. logger.info("接收到更新应用请求")
  748. if 'app_file' not in request.files:
  749. return jsonify({'success': False, 'message': '没有文件被上传'})
  750. file = request.files['app_file']
  751. if file.filename == '':
  752. return jsonify({'success': False, 'message': '没有选择文件'})
  753. # 检查文件类型
  754. if not (file.filename.endswith('.py') or (file.filename.endswith('.html') or file.filename.endswith('.zip'))):
  755. return jsonify({'success': False, 'message': '只支持Python文件(.py)、HTML文件(.html)或压缩包(.zip)'})
  756. # 保存文件到临时目录
  757. temp_dir = app.config['APP_UPGREADE_FOLDER_TEMP']
  758. os.makedirs(temp_dir, exist_ok=True)
  759. bk_dir = 'app_upgrade_bk'
  760. os.makedirs(bk_dir, exist_ok=True)
  761. if file.filename == 'web_app_2006.py':
  762. shutil.copy2(file.filename, os.path.join(bk_dir, file.filename+f'_{APP_VERSION}_bk_{time.time()}'))
  763. file.save(file.filename)
  764. elif file.filename == 'frame.py':
  765. shutil.copy2(os.path.join('pos_manager', file.filename), os.path.join(bk_dir, file.filename+f'_bk_{time.time()}'))
  766. file.save(os.path.join('pos_manager', file.filename))
  767. elif file.filename.endswith('.html') :
  768. shutil.copy2(os.path.join('templates', file.filename), os.path.join(bk_dir, file.filename+f'_bk_{time.time()}'))
  769. file.save(os.path.join('templates', file.filename))
  770. elif file.filename.endswith('.zip') :
  771. file.save(os.path.join(temp_dir, file.filename))
  772. '''
  773. # 解压文件
  774. with zipfile.ZipFile(os.path.join(temp_dir, file.filename), 'r') as zip_ref:
  775. zip_ref.extractall(temp_dir)
  776. # 移动解压后的文件到目标目录
  777. for item in os.listdir(temp_dir):
  778. s = os.path.join(temp_dir, item)
  779. d = os.path.join(app.config['APP_UPGREADE_FOLDER'], item)
  780. if os.path.isdir(s):
  781. shutil.move(s, d)
  782. else:
  783. shutil.copy2(s, d)
  784. # 删除临时文件
  785. os.remove(os.path.join(temp_dir, file.filename))
  786. '''
  787. # 在实际应用中,这里需要实现安全的更新逻辑
  788. # 例如验证文件完整性、备份原有文件、应用更新等
  789. logger.info(f"文件 {file.filename} 上传成功")
  790. return jsonify({'success': True, 'message': f'文件上传成功,重启后生效. {file.filename}, {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}'})
  791. except Exception as e:
  792. logger.error(f"应用更新失败: {str(e)}")
  793. return jsonify({'success': False, 'message': f'更新失败: {str(e)}'})
  794. @app.route('/api/get_logs')
  795. @login_required
  796. def get_logs():
  797. try:
  798. lines = request.args.get('lines', '50')
  799. # logger.info(f"请求获取日志,行数: {lines}")
  800. get_log_file = LOG_FILE
  801. s_p = sys_platform.system().upper()
  802. '''
  803. if s_p == 'WINDOWS':
  804. get_log_file = LOG_FILE
  805. elif s_p == 'LINUX':
  806. get_log_file = 'log.log'
  807. '''
  808. # 检查日志文件是否存在
  809. if not os.path.exists(get_log_file):
  810. return f"日志文件 {get_log_file} 不存在"
  811. # 读取日志文件
  812. with open(get_log_file, 'r', encoding='utf-8') as f:
  813. if lines == 'all':
  814. logs = ''.join(f.readlines()[-500:]) # 最大500行
  815. else:
  816. try:
  817. line_count = int(lines)
  818. logs = ''.join(f.readlines()[-line_count:])
  819. except ValueError:
  820. logs = ''.join(f.readlines()[-50:]) # 默认50行
  821. return logs
  822. except Exception as e:
  823. logger.error(f"获取日志失败: {str(e)}")
  824. return f"获取日志失败: {str(e)}"
  825. @app.route('/firmware', methods=['GET', 'POST'])
  826. @login_required
  827. def firmware():
  828. firmwares = load_firmwares()
  829. if request.method == 'POST':
  830. if 'file' not in request.files:
  831. error = '未选择文件'
  832. return render_template('firmware.html', firmwares=firmwares, error=error)
  833. file = request.files['file']
  834. remark = request.form.get('remark', '').strip()
  835. if file.filename == '':
  836. error = '未选择文件'
  837. return render_template('firmware.html', firmwares=firmwares, error=error)
  838. if not remark:
  839. error = '备注信息不能为空'
  840. return render_template('firmware.html', firmwares=firmwares, error=error)
  841. if file and allowed_file(file.filename):
  842. filename = file.filename
  843. # 检查文件名是否重复
  844. if any(f['filename'] == filename for f in firmwares):
  845. error = f'文件名 "{filename}" 已存在,请更改文件名后再上传'
  846. return render_template('firmware.html', firmwares=firmwares, error=error)
  847. # 检查文件大小
  848. file.seek(0, os.SEEK_END) # 移动指针到文件末尾
  849. file_size = file.tell() # 获取指针位置(即文件大小)
  850. file.seek(0) # 重置指针到文件开头
  851. # 打印文件大小
  852. logger.info(f"文件名: {filename}, 文件大小: {file_size} 字节")
  853. if file_size > 1 * 1024 * 1024: # 1MB
  854. error = f'文件大小不能超过1MB, 当前文件大小: {file_size} 字节'
  855. return render_template('firmware.html', firmwares=firmwares, error=error)
  856. if file_size < 1 * 1024: # 1KB
  857. error = f'文件大小不能小于1KB, 当前文件大小: {file_size} 字节'
  858. return render_template('firmware.html', firmwares=firmwares, error=error)
  859. file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
  860. new_id = max(f['id'] for f in firmwares) + 1 if firmwares else 1
  861. firmwares.append({
  862. 'id': new_id,
  863. 'filename': filename,
  864. 'filesize': '%d 字节' % file_size,
  865. 'upload_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
  866. 'remark': remark
  867. })
  868. save_firmwares(firmwares)
  869. return redirect(url_for('firmware'))
  870. # 处理编辑操作
  871. edit_id = request.args.get('edit')
  872. if edit_id:
  873. new_remark = request.args.get('remark', '').strip()
  874. if new_remark:
  875. for f in firmwares:
  876. if f['id'] == int(edit_id):
  877. f['remark'] = new_remark
  878. break
  879. save_firmwares(firmwares)
  880. return redirect(url_for('firmware'))
  881. # 处理删除操作
  882. delete_id = request.args.get('delete')
  883. if delete_id:
  884. firmwares = [f for f in firmwares if f['id'] != int(delete_id)]
  885. save_firmwares(firmwares)
  886. return redirect(url_for('firmware'))
  887. # 处理下载操作
  888. download_id = request.args.get('download')
  889. if download_id:
  890. firmware = next((f for f in firmwares if f['id'] == int(download_id)), None)
  891. if firmware:
  892. filename = firmware['filename']
  893. file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
  894. if os.path.exists(file_path):
  895. return send_from_directory(app.config['UPLOAD_FOLDER'], filename, as_attachment=True)
  896. else:
  897. error = f'文件 "{filename}" 不存在'
  898. return render_template('firmware.html', firmwares=firmwares, error=error)
  899. else:
  900. error = f'未找到ID为 {download_id} 的固件'
  901. return render_template('firmware.html', firmwares=firmwares, error=error)
  902. return render_template('firmware.html', firmwares=firmwares, title='固件包管理')
  903. @app.route('/devices', methods=['GET', 'POST'])
  904. @login_required
  905. def devices():
  906. devices = load_devices()
  907. platform_l = load_platforms()['platform_l']
  908. firmwares = load_firmwares()
  909. if request.method == 'POST':
  910. # 处理设备配置保存
  911. if 'save_config' in request.form:
  912. config_type = request.form['config_type']
  913. dev_sn = request.form['dev_sn']
  914. target_device = next((device for device in devices if device['dev_sn'] == dev_sn), None)
  915. if target_device:
  916. if config_type == 'all':
  917. # 处理保存所有配置
  918. target_device_wifi_ssid = request.form.get('ssid', '')
  919. target_device_wifi_password = request.form.get('wifi_password', '')
  920. target_device_cloud_platform = request.form.get('cloud-platform', '')
  921. target_device_network_type = request.form.get('network-type', '')
  922. log_msg = f"{target_device['dev_type'].zfill(4)}-{target_device['dev_sn'].zfill(10)}:"
  923. log_msg = log_msg + f"WiFi参数:{target_device_wifi_ssid},{target_device_wifi_password};"
  924. log_msg = log_msg + f"云平台参数:{target_device_cloud_platform};"
  925. log_msg = log_msg + f"网络类型参数:{target_device_network_type}"
  926. logger.info(log_msg)
  927. logger.info("TODO: 发送保存所有配置到设备")
  928. elif config_type == 'wifi':
  929. target_device['wifi_ssid'] = request.form.get('ssid', '')
  930. target_device['wifi_password'] = request.form.get('wifi_password', '')
  931. log_msg = f"WiFi参数:{target_device['dev_type'].zfill(4)}-{target_device['dev_sn'].zfill(10)},{target_device['wifi_ssid']},{target_device['wifi_password']}"
  932. logger.info(log_msg)
  933. logger.info("TODO: 发送WiFi配置到设备")
  934. # platform_id = next((platform_id for platform_id in platform_l if platform_l[platform_id]['name'] == target_device['cloud_platform']), None)
  935. platform_id = load_platforms()['current_platform']
  936. pub_data = frame.get_msg_config_wifi(device=target_device,data="")
  937. pub_topic = f"cpyypt/down/{target_device['dev_type'].zfill(4)}/{target_device['dev_sn'].zfill(10)}"
  938. success = publish_message_dev(platform_id=platform_id, data=pub_data, pub_topic=pub_topic)
  939. if not success: return render_template(
  940. 'device_config.html',
  941. device=target_device,
  942. # cccc platform_l=platform_l.values(),
  943. platform_c=load_platforms_c().values(),
  944. success=False,
  945. message=f"消息发布失败,MQTT客户端未连接 -- {platform_id},请返回主页 【连接】!"
  946. )
  947. elif config_type == 'platform':
  948. log_msg = f"云平台参数:{target_device['dev_type'].zfill(4)}-{target_device['dev_sn'].zfill(10)},{target_device['cloud_platform']}"
  949. logger.info(log_msg)
  950. logger.info("TODO: 发送云平台配置到设备")
  951. # platform_id = next((platform_id for platform_id in platform_l if platform_l[platform_id]['name'] == target_device['cloud_platform']), None)
  952. platform_id = load_platforms()['current_platform']
  953. pub_data = frame.get_msg_config_platform(device=target_device,data=request.form.get('cloud-platform', ''))
  954. pub_topic = f"cpyypt/down/{target_device['dev_type'].zfill(4)}/{target_device['dev_sn'].zfill(10)}"
  955. success = publish_message_dev(platform_id=platform_id, data=pub_data, pub_topic=pub_topic)
  956. if not success: return render_template(
  957. 'device_config.html',
  958. device=target_device,
  959. # cccc platform_l=platform_l.values(),
  960. platform_c=load_platforms_c().values(),
  961. success=False,
  962. message=f"消息发布失败,MQTT客户端未连接 -- {platform_id},请返回主页 【连接】!"
  963. )
  964. target_device['cloud_platform'] = request.form.get('cloud-platform', '')
  965. elif config_type == 'network':
  966. target_device['network_type'] = request.form.get('network-type', '')
  967. log_msg = f"网络类型参数:{target_device['dev_type'].zfill(4)}-{target_device['dev_sn'].zfill(10)},{target_device['network_type']}"
  968. logger.info(log_msg)
  969. logger.info("TODO: 发送网络类型配置到设备")
  970. # platform_id = next((platform_id for platform_id in platform_l if platform_l[platform_id]['name'] == target_device['cloud_platform']), None)
  971. platform_id = load_platforms()['current_platform']
  972. pub_data = frame.get_msg_config_network(device=target_device,data="")
  973. pub_topic = f"cpyypt/down/{target_device['dev_type'].zfill(4)}/{target_device['dev_sn'].zfill(10)}"
  974. success = publish_message_dev(platform_id=platform_id, data=pub_data, pub_topic=pub_topic)
  975. if not success: return render_template(
  976. 'device_config.html',
  977. device=target_device,
  978. # cccc platform_l=platform_l.values(),
  979. platform_c=load_platforms_c().values(),
  980. success=False,
  981. message=f"消息发布失败,MQTT客户端未连接 -- {platform_id},请返回主页 【连接】!"
  982. )
  983. save_devices(devices)
  984. return render_template(
  985. 'device_config.html',
  986. device=target_device,
  987. # cccc platform_l=platform_l.values(),
  988. platform_c=load_platforms_c().values(),
  989. success=True,
  990. message="操作成功,配置已下发!"
  991. )
  992. # 处理批量操作
  993. action = request.form.get('action')
  994. selected_sns = [dev_sn for dev_sn in request.form.getlist('dev_sn')]
  995. if action == 'restart':
  996. # 实际应用中这里会发送重启命令到设备
  997. logger.info(f"批量重启设备: {selected_sns}")
  998. for dev_sn in selected_sns:
  999. target_device = next((device for device in devices if device['dev_sn'] == dev_sn), None)
  1000. if target_device:
  1001. platform_id = load_platforms()['current_platform']
  1002. pub_data = frame.get_msg_restart(device=target_device,data="")
  1003. pub_topic = f"cpyypt/down/{target_device['dev_type'].zfill(4)}/{target_device['dev_sn'].zfill(10)}"
  1004. success = publish_message_dev(platform_id=platform_id, data=pub_data, pub_topic=pub_topic)
  1005. if not success:
  1006. logger.error(f"消息发布失败,MQTT客户端未连接 -- {load_platforms_c()[platform_id]['name']},请返回主页 【连接】!")
  1007. else:
  1008. logger.info(f"消息发布成功 -- dev_sn:{dev_sn}")
  1009. elif action == 'upgrade':
  1010. firmware_id = request.form.get('firmware_id')
  1011. firmware = next((fm for fm in firmwares if fm['id'] == int(firmware_id)), None)
  1012. logger.info(f"批量升级设备 {selected_sns} 到固件 {firmware['filename']}")
  1013. if not firmware:
  1014. return jsonify({'status': 'error', 'message': '固件信息不存在'})
  1015. # 获取文件大小
  1016. bin_file=os.path.join(app.config['UPLOAD_FOLDER'], firmware['filename'])
  1017. try:
  1018. file_size = os.path.getsize(bin_file)
  1019. logger.info(f"文件大小: {file_size} 字节")
  1020. except FileNotFoundError:
  1021. logger.error(f"错误: 文件 '{bin_file}' 不存在")
  1022. return jsonify({'status': 'error', 'message': f'文件{bin_file}名不存在'})
  1023. # 找到第1个被升级设备
  1024. device = next((device for device in devices if device['dev_sn'] == selected_sns[0]), None)
  1025. dev_platform = device.get('cloud_platform', '')
  1026. platform_id = next((platform_id for platform_id in platform_l if platform_l[platform_id]['name'] == dev_platform), None)
  1027. with open(bin_file, 'rb') as f:
  1028. bin_data = f.read()
  1029. dev_upgrade_dict['status'] = 'idel'
  1030. dev_upgrade_dict['current_sn'] = ''
  1031. dev_upgrade_dict['dev_type'] = device.get('dev_type', '2006')
  1032. dev_upgrade_dict['dev_upgrade_sns'] = selected_sns
  1033. dev_upgrade_dict['interval'] = 1
  1034. dev_upgrade_dict['bin_data'] = bin_data
  1035. dev_upgrade_dict['bin_len'] = file_size
  1036. dev_upgrade_dict['pkg_size'] = 512
  1037. dev_upgrade_dict['pkg_total'] = math.ceil(dev_upgrade_dict['bin_len'] / dev_upgrade_dict['pkg_size']) # 向上取整
  1038. dev_upgrade_dict['pkg_cnt'] = 0
  1039. dev_upgrade_dict['platform_id'] = platform_id
  1040. for dev_sn in selected_sns:
  1041. target_device = next((device for device in devices if device['dev_sn'] == dev_sn), None)
  1042. if target_device:
  1043. target_device['upgrade_status'] = '升级中...'
  1044. save_devices(devices)
  1045. start_dev_upgrade_check()
  1046. return jsonify({'status': 'success', 'message': '升级任务已启动'})
  1047. elif action == 'config':
  1048. logger.info(f"批量配置设备: {selected_sns}")
  1049. # 显示设备配置页面
  1050. dev_sn = request.args.get('dev_sn')
  1051. if dev_sn:
  1052. target_device = next((device for device in devices if device['dev_sn'] == dev_sn), None)
  1053. if target_device:
  1054. return render_template(
  1055. 'device_config.html',
  1056. device=target_device,
  1057. # cccc platform_l=platform_l.values(),
  1058. platform_c=load_platforms_c().values(),
  1059. title='设备配置'
  1060. )
  1061. return render_template('devices.html', devices=devices, title='解密机配置')
  1062. @app.route('/device_detail/<dev_sn>')
  1063. @login_required
  1064. def device_detail(dev_sn):
  1065. devices = load_devices()
  1066. device = next((d for d in devices if d['dev_sn'] == dev_sn), None)
  1067. if not device:
  1068. return '设备不存在', 404
  1069. return render_template('device_detail.html', device=device, title='设备详情')
  1070. @app.route('/api/get_firmwares')
  1071. @login_required
  1072. def get_firmwares():
  1073. firmwares = load_firmwares()
  1074. return jsonify(firmwares)
  1075. # 云平台相关API
  1076. @app.route('/api/select_platform', methods=['POST'])
  1077. @login_required
  1078. def select_platform():
  1079. platform_id = request.json.get('platform_id')
  1080. if not platform_id:
  1081. return jsonify({'status': 'error', 'message': '请选择云平台'})
  1082. platform_d = load_platforms()
  1083. if platform_id not in platform_d['platform_l']:
  1084. return jsonify({'status': 'error', 'message': '云平台不存在'})
  1085. platform_d['current_platform'] = platform_id
  1086. save_platforms(platform_d)
  1087. selected_platform = platform_d['platform_l'][platform_id]
  1088. return jsonify({
  1089. 'status': 'success',
  1090. 'platform': {
  1091. 'id': platform_id,
  1092. 'name': selected_platform['name'],
  1093. 'status': selected_platform['status']
  1094. }
  1095. })
  1096. @app.route('/api/toggle_connection', methods=['POST'])
  1097. @login_required
  1098. def toggle_connection():
  1099. platform_id = request.json.get('platform_id')
  1100. if not platform_id:
  1101. return jsonify({'status': 'error', 'message': '请指定云平台'})
  1102. platform_d = load_platforms()
  1103. if platform_id not in platform_d['platform_l']:
  1104. return jsonify({'status': 'error', 'message': '云平台不存在'})
  1105. current_platform = platform_d['platform_l'][platform_id]
  1106. if current_platform['status'] == 'connected':
  1107. stop_mqtt_client(platform_id)
  1108. new_status = 'disconnected'
  1109. logger.info(f"云平台 {platform_id} 已断开")
  1110. else:
  1111. start_mqtt_client(platform_id)
  1112. new_status = 'connected'
  1113. logger.info(f"云平台 {platform_id} 已连接")
  1114. current_platform['status'] = new_status
  1115. save_platforms(platform_d)
  1116. return jsonify({
  1117. 'status': 'success',
  1118. 'new_status': new_status,
  1119. 'platform_name': current_platform['name']
  1120. })
  1121. @app.route('/api/get_platform_status', methods=['GET'])
  1122. @login_required
  1123. def get_platform_status():
  1124. platform_id = request.args.get('platform_id')
  1125. if not platform_id:
  1126. return jsonify({"status": "error", "message": "未指定云平台ID"}), 400
  1127. platform_d = load_platforms()
  1128. if platform_id not in platform_d['platform_l']:
  1129. return jsonify({"status": "error", "message": "云平台不存在"}), 404
  1130. return jsonify({
  1131. "status": "success",
  1132. "platform": platform_d['platform_l'][platform_id]
  1133. })
  1134. @app.route('/api/publish_message', methods=['POST'])
  1135. @login_required
  1136. def publish_message():
  1137. data = request.json
  1138. platform_id = data.get('platform_id')
  1139. message = data.get('message')
  1140. if not platform_id or not message:
  1141. return jsonify({"status": "error", "message": "云平台ID和消息内容都不能为空"}), 400
  1142. success = publish_mqtt_message(platform_id, message)
  1143. return jsonify({
  1144. "status": "success" if success else "error",
  1145. "message": "消息发布成功" if success else "消息发布失败"
  1146. })
  1147. # .......................
  1148. def publish_message_dev(platform_id, data, pub_topic):
  1149. if not platform_id or not data or not pub_topic:
  1150. # return jsonify({"status": "error", "message": "云平台ID、消息内容、发布主题 都不能为空"}), 400
  1151. logger.error(f"publish_message_dev: platform_id={platform_id}, data={data}, pub_topic={pub_topic}")
  1152. return False
  1153. success = publish_mqtt_message(platform_id, data, pub_topic=pub_topic)
  1154. # if not success:
  1155. # logger.error(f"publish_message_dev: platform_id={platform_id}, data={data}, pub_topic={pub_topic}, success={success}")
  1156. return success
  1157. # 定期检查设备状态
  1158. dev_upgrade_dict = {
  1159. 'status' : 'idel',
  1160. 'current_sn' : '',
  1161. 'dev_type' : '2006',
  1162. 'interval' : 1,
  1163. 'bin_data' : None,
  1164. 'bin_len' : 0,
  1165. 'pkg_size' : 512,
  1166. 'pkg_total' : 0,
  1167. 'pkg_cnt':0,
  1168. 'remain':0,
  1169. 'platform_id' : None,
  1170. 'dev_upgrade_sns' : []
  1171. }
  1172. def dev_upgrade_dict_clr():
  1173. dev_upgrade_dict['status'] = 'idel'
  1174. # dev_upgrade_dict['current_sn'] = ''
  1175. # dev_upgrade_dict['dev_type'] = '2006'
  1176. dev_upgrade_dict['interval'] = 1
  1177. # dev_upgrade_dict['bin_data'] = None
  1178. # dev_upgrade_dict['bin_len'] = 0
  1179. # dev_upgrade_dict['pkg_size'] = 512
  1180. # dev_upgrade_dict['pkg_total'] = 0
  1181. dev_upgrade_dict['pkg_cnt'] = 0
  1182. dev_upgrade_dict['remain'] = 0
  1183. dev_upgrade_dict['dev_upgrade_sns'] = []
  1184. def dev_upgrade_check():
  1185. if dev_upgrade_dict['current_sn'] == '':
  1186. if len(dev_upgrade_dict['dev_upgrade_sns']) > 0:
  1187. dev_upgrade_dict['current_sn'] = dev_upgrade_dict['dev_upgrade_sns'][0]
  1188. else:
  1189. dev_upgrade_dict['current_sn'] = ''
  1190. if dev_upgrade_dict['current_sn'] != '':
  1191. # 此处为状态机,需要完善一下,可用。dev_upgrade_dict['status']
  1192. # 升级 状态
  1193. if dev_upgrade_dict['status'] == 'idel':
  1194. logger.info(f"status = idel,等待升级,{dev_upgrade_dict['current_sn']}")
  1195. dev_upgrade_dict['status'] = 'reqeuest'
  1196. dev_upgrade_dict['pkg_cnt'] = 0
  1197. dev_upgrade_dict['remain'] = dev_upgrade_dict['bin_len']
  1198. # 请求 状态
  1199. elif dev_upgrade_dict['status'] == 'reqeuest':
  1200. logger.info(f"status = request,下发升级开始指令,{dev_upgrade_dict['current_sn']}")
  1201. dev_upgrade_dict['status'] = 'upgrade'
  1202. pub_data = frame.get_msg_upgrade_1001(dev_type=dev_upgrade_dict['dev_type'],dev_sn=dev_upgrade_dict['current_sn'])
  1203. pub_topic = f"cpyypt/down/{dev_upgrade_dict['dev_type'].zfill(4)}/{dev_upgrade_dict['current_sn'].zfill(10)}"
  1204. platform_id = load_platforms()['current_platform']
  1205. success = publish_message_dev(platform_id=platform_id, data=pub_data, pub_topic=pub_topic)
  1206. if not success:
  1207. dev_upgrade_dict_clr()
  1208. logger.error(f"dev update failed: platform_id={platform_id}, sn={dev_upgrade_dict['current_sn']}")
  1209. return
  1210. else:
  1211. # time.sleep(2)
  1212. dev_upgrade_dict['status'] = 'upgrade'
  1213. dev_upgrade_dict['interval'] = 0.2
  1214. # 升级 状态
  1215. elif dev_upgrade_dict['status'] == 'upgrade':
  1216. logger.info(f'status = upgrade,升级包发送中,{dev_upgrade_dict["current_sn"]},{dev_upgrade_dict["pkg_cnt"]}/{dev_upgrade_dict["pkg_total"]}')
  1217. if dev_upgrade_dict['remain'] > 0:
  1218. data = dev_upgrade_dict['bin_data'][
  1219. dev_upgrade_dict['pkg_cnt'] * dev_upgrade_dict['pkg_size']: (dev_upgrade_dict['pkg_cnt'] + 1) *
  1220. dev_upgrade_dict['pkg_size']]
  1221. pub_data = frame.get_msg_upgrade_1030(dev_type=dev_upgrade_dict['dev_type'],
  1222. dev_sn=dev_upgrade_dict['current_sn'],
  1223. fileSum=dev_upgrade_dict['bin_len'],
  1224. maxPkgId=dev_upgrade_dict['pkg_total'] - 1,
  1225. curPkgId=dev_upgrade_dict['pkg_cnt'],
  1226. curPkgsize=len(data),
  1227. data=data)
  1228. pub_topic = f"cpyypt/down/{dev_upgrade_dict['dev_type'].zfill(4)}/{dev_upgrade_dict['current_sn'].zfill(10)}"
  1229. platform_id = load_platforms()['current_platform']
  1230. success = publish_message_dev(platform_id=platform_id, data=pub_data, pub_topic=pub_topic)
  1231. if not success:
  1232. dev_upgrade_dict_clr()
  1233. logger.error(f"升级失败,发布MQTT消息失败: platform_id={platform_id}, sn={dev_upgrade_dict['current_sn']}")
  1234. return
  1235. # time.sleep(0.1)
  1236. dev_upgrade_dict['status'] = 'upgrade'
  1237. else:
  1238. dev_upgrade_dict['status'] = 'finish'
  1239. dev_upgrade_dict['pkg_cnt'] += 1
  1240. dev_upgrade_dict['remain'] = dev_upgrade_dict['bin_len'] - dev_upgrade_dict['pkg_cnt'] * dev_upgrade_dict[
  1241. 'pkg_size']
  1242. # 完成 状态
  1243. elif dev_upgrade_dict['status'] == 'finish':
  1244. logger.info(f"status = finish,升级完成,{dev_upgrade_dict['current_sn']}")
  1245. dev_upgrade_dict['status'] = 'idel'
  1246. dev_upgrade_dict['pkg_cnt'] = 0
  1247. dev_upgrade_dict['remain'] = 0
  1248. dev_upgrade_dict['interval'] = 1
  1249. dev_upgrade_dict['dev_upgrade_sns'].remove(dev_upgrade_dict['current_sn'])
  1250. if len(dev_upgrade_dict['dev_upgrade_sns']) > 0:
  1251. dev_upgrade_dict['current_sn'] = dev_upgrade_dict['dev_upgrade_sns'][0]
  1252. else:
  1253. dev_upgrade_dict['current_sn'] = ''
  1254. if dev_upgrade_dict['current_sn'] == '' and len(dev_upgrade_dict['dev_upgrade_sns']) == 0:
  1255. logger.info('未检测到待升级设备')
  1256. else:
  1257. dev_upgrade_check_timer = threading.Timer(dev_upgrade_dict['interval'], dev_upgrade_check)
  1258. dev_upgrade_check_timer.daemon = True
  1259. dev_upgrade_check_timer.start()
  1260. # 启动定时检查 升级状态
  1261. dev_upgrade_check_timer = None
  1262. def start_dev_upgrade_check():
  1263. global dev_upgrade_check_timer
  1264. if dev_upgrade_check_timer is not None:
  1265. dev_upgrade_check_timer.cancel()
  1266. dev_upgrade_check()
  1267. # 启动定时检查 在线状态
  1268. def dev_online_check():
  1269. devices = load_devices()
  1270. for device in devices:
  1271. if device.get('online',None) == '在线':
  1272. # 检查 在线状态
  1273. online_is_changed = False
  1274. if(int(time.time()) - device.get('realtime',0)) > 10:
  1275. device['online'] = '离线'
  1276. logger.warning(f"设备离线: 云平台:{device['cloud_platform']}, sn={device['dev_sn']}")
  1277. online_is_changed = True
  1278. # 检查 速度
  1279. speed_is_changed = False
  1280. dev_sn = device['dev_sn']
  1281. if dev_sn not in device_speed:
  1282. device_speed[dev_sn] = []
  1283. if device.get('send_sum',0) != 0: device_speed[dev_sn].append({
  1284. 'svr_run_time':time.time(),
  1285. 'dev_run_time':device['run_time'],
  1286. 'send_sum':device['send_sum'],
  1287. })
  1288. while len(device_speed[dev_sn]) > 11:
  1289. device_speed[dev_sn].pop(0)
  1290. if len(device_speed[dev_sn]) > 2:
  1291. run_time_delta = device_speed[dev_sn][-1]['dev_run_time'] - device_speed[dev_sn][0]['dev_run_time']
  1292. send_sum_delta = device_speed[dev_sn][-1]['send_sum'] - device_speed[dev_sn][0]['send_sum']
  1293. speed = 0 if run_time_delta <= 0 else (send_sum_delta / run_time_delta)
  1294. else:
  1295. speed = 0
  1296. if device.get('speed',0) != speed:
  1297. device['speed'] = speed
  1298. speed_is_changed = True
  1299. if online_is_changed or speed_is_changed:
  1300. # logger.info(f"dev_online_check, dev_sn={dev_sn}, device_speed={device_speed[dev_sn]}")
  1301. save_devices(devices)
  1302. threading_timer['dev_online_check_timer'] = threading.Timer(threading_timer['dev_online_check_timeout'], dev_online_check)
  1303. threading_timer['dev_online_check_timer'].daemon = True
  1304. threading_timer['dev_online_check_timer'].start()
  1305. def start_dev_online_check():
  1306. if threading_timer['dev_online_check_timer'] is not None:
  1307. threading_timer['dev_online_check_timer'].cancel()
  1308. dev_online_check()
  1309. # 类似守护进程 所有状态
  1310. def per_second():
  1311. # 定期检查 mqtt状态
  1312. if threading_timer['mqtt_status_check_timer'] is None or not threading_timer['mqtt_status_check_timer'].is_alive():
  1313. logger.warning(f"mqtt_status_check_timer 发现定时器停止了, 需要重新启动")
  1314. start_mqtt_status_check()
  1315. # 定期检查 设备在线状态
  1316. if threading_timer['dev_online_check_timer'] is None or not threading_timer['dev_online_check_timer'].is_alive():
  1317. logger.warning(f"dev_online_check_timer 发现定时器停止了, 需要重新启动")
  1318. start_dev_online_check()
  1319. threading_timer['per_second_timer'] = threading.Timer(threading_timer['per_second_timeout'], per_second)
  1320. threading_timer['per_second_timer'].daemon = True
  1321. threading_timer['per_second_timer'].start()
  1322. if __name__ == '__main__':
  1323. logger.info(f"APP_VERSION: {APP_VERSION}")
  1324. init_data()
  1325. # 启动 MQTT连接状态定时检查
  1326. start_mqtt_status_check()
  1327. # 启动 设备在线状态定时检查
  1328. start_dev_online_check()
  1329. # 启动 所有检查
  1330. per_second()
  1331. app.run(host='0.0.0.0', port=9082, debug=False, threaded=True, use_reloader=False) # 启用多线程提高性能
  1332. # app.run(host='0.0.0.0', port=9082, debug=False, threaded=False, use_reloader=False) # 单线程