diff --git a/.env b/.env new file mode 100644 index 0000000..f585799 --- /dev/null +++ b/.env @@ -0,0 +1,27 @@ +# 1804 +PROJECT_NAME = "9000" + +# 柜体型号 9000 3000 1804 1806 +CLIENT_NUMBER = '9000' + +TERMINAL_ID = "c97fef32-8dec-48eb-ab3f-80caa4593871" +# TERMINAL_ID = "" + +# 危化品 +DEFAULT_ARCHIVE_ID = "66a3877e-4c2d-4af8-859b-89921b7be38d" + +# 虹软 +ARCSOFT_APP_ID = '3zedF4M48gsn1XeXaxWVR6DBEJJxDQYxgVk2B3w8KLgA' +ARCSOFT_SDK_KEY = '4HKQrzqK95PrNLSB6vTFaR5GnR47REpoCikzYEd5T3Q2' +# mysql +# DB_URL = 'mysql://root:Yanei!23@192.168.2.101:3344/rms_ge_prod' +# DB_URL = 'mysql://root:123456@127.0.0.1:3306/rms_ge_prod_maomingjianyansuo' +# DB_URL = 'mysql://root:123456@127.0.0.1:3306/shceduler' + +# DB_URL = "sqlite://db.sqlite3" + +DB_URL = "mysql://root:123456@127.0.0.1:3306/agv" + +# Jwt +JWT_SECRET_KEY = "09d25e194faa6ca2556c818166b7a9573b93f7099f4f0f4caa7cf63b88e8d3e7" +JWT_ALGORITHM = "HS256" diff --git a/ModelbusTest.py b/ModelbusTest.py new file mode 100644 index 0000000..e69de29 diff --git a/__pycache__/main.cpython-311.pyc b/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..0d356c7 Binary files /dev/null and b/__pycache__/main.cpython-311.pyc differ diff --git a/__pycache__/settings.cpython-311.pyc b/__pycache__/settings.cpython-311.pyc new file mode 100644 index 0000000..a537d5d Binary files /dev/null and b/__pycache__/settings.cpython-311.pyc differ diff --git a/aerich.sh b/aerich.sh new file mode 100644 index 0000000..380e48f --- /dev/null +++ b/aerich.sh @@ -0,0 +1,39 @@ +#重新配置需要先删除目录 migrations目录下的所有文件 + +# 1、执行aerich初始化:aerich init -t 指定配置 +aerich init -t settings.TORTOISE_ORM + +# 将会在目录下生成空的 migrations 文件夹和 aerich.ini 文件 + +# 2、将模型映射到数据库中: + +aerich init-db + +# 此时数据库中就会生成对应的表 + +# migrations 下将会生成SQL语句 + +# aerich表下会存每个模型的内容 + +# 3、重新生成SQL语句: +aerich migrate + +# 4、把新生成的SQL推送到数据库: +aerich upgrade + +# 表信息 + +# 5、如果要回到上一个版本: +aerich downgrade + +# 6、查看历史迁移记录: +aerich history + +# 7、查看形成当前版本的迁移记录文件: +aerich heads + +# 8、aerich 除了提供命令行之外,还提供了代码内执行的办法,从aerich引入 Command类即可,提供的方法与命令行一样 + +# 注:根据实测,在MySQL5.7上使用会报错: +# 换成MySQL8以后使用正常 + diff --git a/agvtask/__pycache__/agvtasks.cpython-311.pyc b/agvtask/__pycache__/agvtasks.cpython-311.pyc new file mode 100644 index 0000000..70ec53b Binary files /dev/null and b/agvtask/__pycache__/agvtasks.cpython-311.pyc differ diff --git a/agvtask/__pycache__/tasks.cpython-311.pyc b/agvtask/__pycache__/tasks.cpython-311.pyc new file mode 100644 index 0000000..c8ad9cb Binary files /dev/null and b/agvtask/__pycache__/tasks.cpython-311.pyc differ diff --git a/agvtask/agvtasks.py b/agvtask/agvtasks.py new file mode 100644 index 0000000..af87db5 --- /dev/null +++ b/agvtask/agvtasks.py @@ -0,0 +1,291 @@ + +import httpx +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from pytz import timezone +import time +from conf import setting +from helper.logger import logger +from datetime import datetime +import json +# from models.assignment import Assignment +# from models.warehouse import Vacancy +import os +import codecs +from plc.cmd import readStatus + + +scheduler = AsyncIOScheduler(timezone=timezone('Asia/Shanghai')) + + +# async def genCMD(assignment, cmdStdList, cmdNonStdList): + +# cmdStdl = [] +# cmdNonStdl = [] + +# for cmdStd in cmdStdList: + +# if 1 == cmdStd["action"]: +# #1: 归还 +# cmdStdl.append({"Return": cmdStd}) +# elif 3 == cmdStd["action"]: +# #3: 预约领用 +# #find new Vacancy +# qs1 = await Vacancy.filter(is_shelf = True, is_valid = True, is_active = True) +# if len(qs1) > 0: +# print("find new Vacancy: ", qs1) +# vacancy = qs1[0] +# #generate destination with respond to task +# cmdStdl.append({vacancy.number: cmdStd}) +# #update vacancy +# vacancy.is_valid = False +# vacancy.orderids = [] +# await vacancy.save() +# else: +# raise Exception("Shelf vacancy is not enough!") + +# elif 5 == cmdStd["action"]: +# #5: 现场领用 +# qs1 = await Vacancy.filter(is_connect = True, is_valid = True, is_active = True) +# if len(qs1) > 0: +# print("find new Vacancy: ", qs1) +# vacancy = qs1[0] +# #generate destination with respond to task +# cmdStdl.append({vacancy.number: cmdStd}) +# #update vacancy +# vacancy.is_valid = False +# vacancy.orderids = [] +# await vacancy.save() +# else: +# raise Exception("Shelf vacancy is not enough!") + +# for cmdNonStd in cmdNonStdList: + +# if 1 == cmdNonStd["action"]: +# #1: 归还 +# # cmdNonStdl.append({"Return": cmdNonStd}) + +# #where to return after usage +# # to shelf or stay in connection !!! + + +# #check if next assignment will use this tray +# assignmentNext = await Assignment.filter( +# is_canceled = False, +# is_processing = False, +# is_failed = False, +# is_done = False, +# is_valid = True, +# is_active = True +# ).first() + +# if assignmentNext: +# subtasksNext = await assignmentNext.subtasks.all() + +# for subtaskNext in subtasksNext: +# if 9 == subtaskNext.typetask: +# qs4 = await Vacancy.filter(is_connect = True, is_valid = False, is_active = True, traynum=[subtaskNext.traynum]) +# if len(qs4) > 0: +# print("connection area tray num {}, is used in next assignment {}, don't need to move ".format(subtaskNext.traynum, assignmentNext.id)) +# cmdStdl.insert(0, {"Skip": cmdNonStd}) +# return + +# #check if the tray in Schelve +# qs5 = await Vacancy.filter(is_shelf = True, is_valid = False, is_active = True, traynum=[cmdNonStd["traynum"]]) +# if len(qs5) > 0: +# vacancy = qs5[0] +# if vacancy.orderids and len(vacancy) > 0: +# # update destination to Schelve vacancy +# cmdNonStd["coordinates"] = vacancy.number +# # insert into beginning of the list +# cmdStdl.insert(0, {"Return": cmdNonStd}) +# return + +# # normal return process +# cmdStdl.append({"Return": cmdNonStd}) +# elif 3 == cmdNonStd["action"]: +# #3: 预约领用 +# qs1 = await Vacancy.filter(is_shelf = True, is_valid = True, is_active = True) +# if len(qs1) > 0: +# vacancy = qs1[0] + +# #check if tray already at Vacancy +# qs3 = await Vacancy.filter(is_valid = False, is_active = True, traynum=[cmdNonStd["traynum"]]) +# if len(qs3) > 0: +# vacancy3 = qs3[0] +# #if exist then change destination to corresponding vacancy +# cmdNonStd["coordinates"] = vacancy3.number +# #update vacancy +# orderids = vacancy3.orderids +# if not orderids: +# orderids = [ cmdNonStd["orderid"] ] +# else: +# orderids.append(cmdNonStd["orderid"]) + +# vacancy3.is_valid = False +# vacancy3.orderids = orderids +# await vacancy3.save() + +# #generate destination with respond to task +# # cmdNonStdl.append({vacancy.number: cmdNonStd}) +# cmdStdl.append({vacancy.number: cmdNonStd}) +# #update vacancy +# orderids = vacancy.orderids +# if not orderids: +# orderids = [ cmdNonStd["orderid"] ] +# else: +# orderids.append(cmdNonStd["orderid"]) + +# vacancy.is_valid = False +# vacancy.traynum = cmdNonStd["traynum"] +# vacancy.orderids = orderids +# await vacancy.save() +# else: +# raise Exception("Shelf vacancy is not enough!") + +# elif 5 == cmdNonStd["action"]: +# #5: 现场领用 +# qs1 = await Vacancy.filter(is_connect = True, is_valid = True, is_active = True) +# if len(qs1) > 0: +# vacancy = qs1[0] + +# #check if tray already at Vacancy +# qs3 = await Vacancy.filter(is_valid = False, is_active = True, traynum=[cmdNonStd["traynum"]]) +# if len(qs3) > 0: +# vacancy3 = qs3[0] +# #if exist then change destination to corresponding vacancy +# cmdNonStd["coordinates"] = vacancy3.number +# #update vacancy +# orderids = vacancy3.orderids + +# if orderids and len(orderids) > 0: +# lstSet = set(orderids) +# lstSet.discard( cmdNonStd["orderid"] ) +# # Converting set back to list +# orderids=list(lstSet) +# else: +# orderids = [] + +# if len(orderids) > 0: +# vacancy3.is_valid = False +# else: +# vacancy3.is_valid = True + +# vacancy3.orderids = orderids +# await vacancy3.save() + +# #generate destination with respond to task +# # cmdNonStdl.append({vacancy.number: cmdNonStd}) +# # #update vacancy +# # orderids = vacancy.orderids +# # if not orderids: +# # orderids = [ cmdNonStd["orderid"] ] +# # else: +# # orderids.append(cmdNonStd["orderid"]) + +# # vacancy.is_valid = False +# # vacancy.traynum = cmdNonStd["traynum"] +# # # vacancy.orderids = orderids +# # await vacancy.save() + +# cmdStdl.append({vacancy.number: cmdNonStd}) + +# else: +# raise Exception("Connect vacancy is not enough!") + + +# assignment.cmd = cmdStdl + cmdNonStdl +# await assignment.save() + +# async def agv_asign_job(): +# print("Working ", "{}".format(datetime.now())) + +# # assignments = await Assignment.filter(is_canceled = False, is_valid = True, is_active = True) +# assignments = await Assignment.filter( +# is_canceled = False, +# is_processing = False, +# is_failed = False, +# is_done = False, +# is_valid = True, +# is_active = True +# ) + +# path = "agvtask/testdata/test-{}.json".format(datetime.now().strftime("%Y-%m-%d_%H_%M_%S")) + +# assignmentsL = [] + +# for assignment in assignments: +# owner = await assignment.owner +# subtasks = await assignment.subtasks.all() +# assignmentJ = assignment.as_json() + +# subtasksL = [] +# cmdStdList = [] +# cmdNonStdList = [] + +# for subtask in subtasks: +# if 1 == subtask.typetask \ +# or 1 == subtask.typetask \ +# or 3 == subtask.typetask \ +# or 5 == subtask.typetask \ +# or 7 == subtask.typetask: +# # for standard +# cmdStdList.append({ +# "orderid": subtask.orderid, +# "name": subtask.name, +# "typetask": subtask.typetask, +# "action": subtask.action, +# "quantity": subtask.quantity, +# "coordinates": subtask.coordinates, +# "traynum": subtask.traynum, +# }) +# else: +# # for non standard +# cmdNonStdList.append({ +# "orderid": subtask.orderid, +# "name": subtask.name, +# "typetask": subtask.typetask, +# "action": subtask.action, +# "quantity": subtask.quantity, +# "coordinates": subtask.coordinates, +# "traynum": subtask.traynum, +# }) + +# subtasksL.append(subtask.as_json()) + +# assignment.is_processing = True +# await assignment.save() + +# await genCMD(assignment, cmdStdList, cmdNonStdList) + +# assignmentJ["owner"] = owner.as_json() +# assignmentJ["subtasks"] = subtasksL +# assignmentJ["cmd"] = assignment.cmd + +# assignmentsL.append(assignmentJ) + +# # print("assignment: ", assignmentJ) +# with open(path, "w", encoding="utf-8") as fp: +# json.dump(assignmentsL, fp, ensure_ascii=False) + + + +def getAgvStatus(): + status = readStatus() + + print("getAgvStatus: ", status) + + +async def start_scheduler(): + """ """ + second = 10 + logger.info(f"准备启动AGV小车任务派发定时任务, 间隔时间: {second}") + + scheduler.add_job(getAgvStatus, 'interval', seconds=int(second)) + + # scheduler.add_job(agv_asign_job, 'interval', seconds=int(second)) + + # minute = 1 + # logger.info(f"准备启动AGV小车任务派发定时任务, 间隔时间: {minute}") + # scheduler.add_job(agv_asign_job, 'interval', minutes=int(minute)) + # scheduler.add_job(agv_asign_job, 'cron', hour="8,20") + scheduler.start() diff --git a/algorithm/Paths.json b/algorithm/Paths.json new file mode 100644 index 0000000..2deca14 --- /dev/null +++ b/algorithm/Paths.json @@ -0,0 +1,9122 @@ +{ + "0": { + "1": [ + 0, + 1 + ], + "2": [ + 0, + 2 + ], + "3": [ + 0, + 3 + ], + "4": [ + 0, + 4 + ], + "5": [ + 0, + 5 + ], + "6": [ + 0, + 6 + ], + "7": [ + 0, + 7 + ], + "8": [ + 0, + 8 + ], + "9": [ + 0, + 9 + ], + "10": [ + 0, + 10 + ], + "11": [ + 0, + 11 + ], + "12": [ + 0, + 12 + ], + "13": [ + 0, + 13 + ], + "14": [ + 0, + 14 + ], + "15": [ + 0, + 15 + ], + "16": [ + 0, + 16 + ], + "17": [ + 0, + 17 + ], + "18": [ + 0, + 18 + ], + "19": [ + 0, + 19 + ], + "20": [ + 0, + 20 + ], + "21": [ + 0, + 21 + ], + "22": [ + 0, + 22 + ], + "23": [ + 0, + 23 + ], + "24": [ + 0, + 24 + ], + "25": [ + 0, + 25 + ], + "26": [ + 0, + 26 + ], + "27": [ + 0, + 27 + ], + "28": [ + 0, + 28 + ], + "29": [ + 0, + 29 + ], + "30": [ + 0, + 30 + ], + "31": [ + 0, + 31 + ], + "32": [ + 0, + 32 + ], + "33": [ + 0, + 33 + ], + "34": [ + 0, + 34 + ], + "35": [ + 0, + 35 + ], + "36": [ + 0, + 36 + ], + "37": [ + 0, + 37 + ], + "38": [ + 0, + 38 + ], + "39": [ + 0, + 39 + ], + "40": [ + 0, + 40 + ], + "41": [ + 0, + 41 + ], + "42": [ + 0, + 42 + ], + "43": [ + 0, + 43 + ], + "44": [ + 0, + 44 + ], + "45": [ + 0, + 45 + ], + "46": [ + 0, + 46 + ], + "47": [ + 0, + 47 + ] + }, + "1": { + "0": [ + 1, + 0 + ], + "2": [ + 1, + 2 + ], + "3": [ + 1, + 3 + ], + "4": [ + 1, + 4 + ], + "5": [ + 1, + 5 + ], + "6": [ + 1, + 6 + ], + "7": [ + 1, + 7 + ], + "8": [ + 1, + 8 + ], + "9": [ + 1, + 9 + ], + "10": [ + 1, + 10 + ], + "11": [ + 1, + 11 + ], + "12": [ + 1, + 12 + ], + "13": [ + 1, + 13 + ], + "14": [ + 1, + 14 + ], + "15": [ + 1, + 15 + ], + "16": [ + 1, + 16 + ], + "17": [ + 1, + 17 + ], + "18": [ + 1, + 18 + ], + "19": [ + 1, + 19 + ], + "20": [ + 1, + 20 + ], + "21": [ + 1, + 21 + ], + "22": [ + 1, + 22 + ], + "23": [ + 1, + 23 + ], + "24": [ + 1, + 24 + ], + "25": [ + 1, + 25 + ], + "26": [ + 1, + 26 + ], + "27": [ + 1, + 27 + ], + "28": [ + 1, + 28 + ], + "29": [ + 1, + 29 + ], + "30": [ + 1, + 30 + ], + "31": [ + 1, + 31 + ], + "32": [ + 1, + 32 + ], + "33": [ + 1, + 33 + ], + "34": [ + 1, + 34 + ], + "35": [ + 1, + 35 + ], + "36": [ + 1, + 36 + ], + "37": [ + 1, + 37 + ], + "38": [ + 1, + 38 + ], + "39": [ + 1, + 39 + ], + "40": [ + 1, + 40 + ], + "41": [ + 1, + 41 + ], + "42": [ + 1, + 42 + ], + "43": [ + 1, + 43 + ], + "44": [ + 1, + 44 + ], + "45": [ + 1, + 45 + ], + "46": [ + 1, + 46 + ], + "47": [ + 1, + 47 + ] + }, + "2": { + "0": [ + 2, + 0 + ], + "1": [ + 2, + 1 + ], + "3": [ + 2, + 3 + ], + "4": [ + 2, + 4 + ], + "5": [ + 2, + 5 + ], + "6": [ + 2, + 6 + ], + "7": [ + 2, + 7 + ], + "8": [ + 2, + 8 + ], + "9": [ + 2, + 9 + ], + "10": [ + 2, + 10 + ], + "11": [ + 2, + 11 + ], + "12": [ + 2, + 12 + ], + "13": [ + 2, + 13 + ], + "14": [ + 2, + 14 + ], + "15": [ + 2, + 15 + ], + "16": [ + 2, + 16 + ], + "17": [ + 2, + 17 + ], + "18": [ + 2, + 18 + ], + "19": [ + 2, + 19 + ], + "20": [ + 2, + 20 + ], + "21": [ + 2, + 21 + ], + "22": [ + 2, + 22 + ], + "23": [ + 2, + 23 + ], + "24": [ + 2, + 24 + ], + "25": [ + 2, + 25 + ], + "26": [ + 2, + 26 + ], + "27": [ + 2, + 27 + ], + "28": [ + 2, + 28 + ], + "29": [ + 2, + 29 + ], + "30": [ + 2, + 30 + ], + "31": [ + 2, + 31 + ], + "32": [ + 2, + 32 + ], + "33": [ + 2, + 33 + ], + "34": [ + 2, + 34 + ], + "35": [ + 2, + 35 + ], + "36": [ + 2, + 36 + ], + "37": [ + 2, + 37 + ], + "38": [ + 2, + 38 + ], + "39": [ + 2, + 39 + ], + "40": [ + 2, + 40 + ], + "41": [ + 2, + 41 + ], + "42": [ + 2, + 42 + ], + "43": [ + 2, + 43 + ], + "44": [ + 2, + 44 + ], + "45": [ + 2, + 45 + ], + "46": [ + 2, + 46 + ], + "47": [ + 2, + 47 + ] + }, + "3": { + "0": [ + 3, + 0 + ], + "1": [ + 3, + 1 + ], + "2": [ + 3, + 2 + ], + "4": [ + 3, + 4 + ], + "5": [ + 3, + 5 + ], + "6": [ + 3, + 6 + ], + "7": [ + 3, + 7 + ], + "8": [ + 3, + 8 + ], + "9": [ + 3, + 9 + ], + "10": [ + 3, + 10 + ], + "11": [ + 3, + 11 + ], + "12": [ + 3, + 12 + ], + "13": [ + 3, + 13 + ], + "14": [ + 3, + 14 + ], + "15": [ + 3, + 15 + ], + "16": [ + 3, + 16 + ], + "17": [ + 3, + 17 + ], + "18": [ + 3, + 18 + ], + "19": [ + 3, + 19 + ], + "20": [ + 3, + 20 + ], + "21": [ + 3, + 21 + ], + "22": [ + 3, + 22 + ], + "23": [ + 3, + 23 + ], + "24": [ + 3, + 24 + ], + "25": [ + 3, + 25 + ], + "26": [ + 3, + 26 + ], + "27": [ + 3, + 27 + ], + "28": [ + 3, + 28 + ], + "29": [ + 3, + 29 + ], + "30": [ + 3, + 30 + ], + "31": [ + 3, + 31 + ], + "32": [ + 3, + 32 + ], + "33": [ + 3, + 33 + ], + "34": [ + 3, + 34 + ], + "35": [ + 3, + 35 + ], + "36": [ + 3, + 36 + ], + "37": [ + 3, + 37 + ], + "38": [ + 3, + 38 + ], + "39": [ + 3, + 39 + ], + "40": [ + 3, + 40 + ], + "41": [ + 3, + 41 + ], + "42": [ + 3, + 42 + ], + "43": [ + 3, + 43 + ], + "44": [ + 3, + 44 + ], + "45": [ + 3, + 45 + ], + "46": [ + 3, + 46 + ], + "47": [ + 3, + 47 + ] + }, + "4": { + "0": [ + 4, + 0 + ], + "1": [ + 4, + 1 + ], + "2": [ + 4, + 2 + ], + "3": [ + 4, + 3 + ], + "5": [ + 4, + 5 + ], + "6": [ + 4, + 6 + ], + "7": [ + 4, + 7 + ], + "8": [ + 4, + 8 + ], + "9": [ + 4, + 9 + ], + "10": [ + 4, + 10 + ], + "11": [ + 4, + 11 + ], + "12": [ + 4, + 12 + ], + "13": [ + 4, + 13 + ], + "14": [ + 4, + 14 + ], + "15": [ + 4, + 15 + ], + "16": [ + 4, + 16 + ], + "17": [ + 4, + 17 + ], + "18": [ + 4, + 18 + ], + "19": [ + 4, + 19 + ], + "20": [ + 4, + 20 + ], + "21": [ + 4, + 21 + ], + "22": [ + 4, + 22 + ], + "23": [ + 4, + 23 + ], + "24": [ + 4, + 24 + ], + "25": [ + 4, + 25 + ], + "26": [ + 4, + 26 + ], + "27": [ + 4, + 27 + ], + "28": [ + 4, + 28 + ], + "29": [ + 4, + 29 + ], + "30": [ + 4, + 30 + ], + "31": [ + 4, + 31 + ], + "32": [ + 4, + 32 + ], + "33": [ + 4, + 33 + ], + "34": [ + 4, + 34 + ], + "35": [ + 4, + 35 + ], + "36": [ + 4, + 36 + ], + "37": [ + 4, + 37 + ], + "38": [ + 4, + 38 + ], + "39": [ + 4, + 39 + ], + "40": [ + 4, + 40 + ], + "41": [ + 4, + 41 + ], + "42": [ + 4, + 42 + ], + "43": [ + 4, + 43 + ], + "44": [ + 4, + 44 + ], + "45": [ + 4, + 45 + ], + "46": [ + 4, + 46 + ], + "47": [ + 4, + 47 + ] + }, + "5": { + "0": [ + 5, + 0 + ], + "1": [ + 5, + 1 + ], + "2": [ + 5, + 2 + ], + "3": [ + 5, + 3 + ], + "4": [ + 5, + 4 + ], + "6": [ + 5, + 6 + ], + "7": [ + 5, + 7 + ], + "8": [ + 5, + 8 + ], + "9": [ + 5, + 9 + ], + "10": [ + 5, + 10 + ], + "11": [ + 5, + 11 + ], + "12": [ + 5, + 12 + ], + "13": [ + 5, + 13 + ], + "14": [ + 5, + 14 + ], + "15": [ + 5, + 15 + ], + "16": [ + 5, + 16 + ], + "17": [ + 5, + 17 + ], + "18": [ + 5, + 18 + ], + "19": [ + 5, + 19 + ], + "20": [ + 5, + 20 + ], + "21": [ + 5, + 21 + ], + "22": [ + 5, + 22 + ], + "23": [ + 5, + 23 + ], + "24": [ + 5, + 24 + ], + "25": [ + 5, + 25 + ], + "26": [ + 5, + 26 + ], + "27": [ + 5, + 27 + ], + "28": [ + 5, + 28 + ], + "29": [ + 5, + 29 + ], + "30": [ + 5, + 30 + ], + "31": [ + 5, + 31 + ], + "32": [ + 5, + 32 + ], + "33": [ + 5, + 33 + ], + "34": [ + 5, + 34 + ], + "35": [ + 5, + 35 + ], + "36": [ + 5, + 36 + ], + "37": [ + 5, + 37 + ], + "38": [ + 5, + 38 + ], + "39": [ + 5, + 39 + ], + "40": [ + 5, + 40 + ], + "41": [ + 5, + 41 + ], + "42": [ + 5, + 42 + ], + "43": [ + 5, + 43 + ], + "44": [ + 5, + 44 + ], + "45": [ + 5, + 45 + ], + "46": [ + 5, + 46 + ], + "47": [ + 5, + 47 + ] + }, + "6": { + "0": [ + 6, + 0 + ], + "1": [ + 6, + 1 + ], + "2": [ + 6, + 2 + ], + "3": [ + 6, + 3 + ], + "4": [ + 6, + 4 + ], + "5": [ + 6, + 5 + ], + "7": [ + 6, + 7 + ], + "8": [ + 6, + 8 + ], + "9": [ + 6, + 9 + ], + "10": [ + 6, + 10 + ], + "11": [ + 6, + 11 + ], + "12": [ + 6, + 12 + ], + "13": [ + 6, + 13 + ], + "14": [ + 6, + 14 + ], + "15": [ + 6, + 15 + ], + "16": [ + 6, + 16 + ], + "17": [ + 6, + 17 + ], + "18": [ + 6, + 18 + ], + "19": [ + 6, + 19 + ], + "20": [ + 6, + 20 + ], + "21": [ + 6, + 21 + ], + "22": [ + 6, + 22 + ], + "23": [ + 6, + 23 + ], + "24": [ + 6, + 24 + ], + "25": [ + 6, + 25 + ], + "26": [ + 6, + 26 + ], + "27": [ + 6, + 27 + ], + "28": [ + 6, + 28 + ], + "29": [ + 6, + 29 + ], + "30": [ + 6, + 30 + ], + "31": [ + 6, + 31 + ], + "32": [ + 6, + 32 + ], + "33": [ + 6, + 33 + ], + "34": [ + 6, + 34 + ], + "35": [ + 6, + 35 + ], + "36": [ + 6, + 36 + ], + "37": [ + 6, + 37 + ], + "38": [ + 6, + 38 + ], + "39": [ + 6, + 39 + ], + "40": [ + 6, + 40 + ], + "41": [ + 6, + 41 + ], + "42": [ + 6, + 42 + ], + "43": [ + 6, + 43 + ], + "44": [ + 6, + 44 + ], + "45": [ + 6, + 45 + ], + "46": [ + 6, + 46 + ], + "47": [ + 6, + 47 + ] + }, + "7": { + "0": [ + 7, + 0 + ], + "1": [ + 7, + 1 + ], + "2": [ + 7, + 2 + ], + "3": [ + 7, + 3 + ], + "4": [ + 7, + 4 + ], + "5": [ + 7, + 5 + ], + "6": [ + 7, + 6 + ], + "8": [ + 7, + 8 + ], + "9": [ + 7, + 9 + ], + "10": [ + 7, + 10 + ], + "11": [ + 7, + 11 + ], + "12": [ + 7, + 12 + ], + "13": [ + 7, + 13 + ], + "14": [ + 7, + 14 + ], + "15": [ + 7, + 15 + ], + "16": [ + 7, + 16 + ], + "17": [ + 7, + 17 + ], + "18": [ + 7, + 18 + ], + "19": [ + 7, + 19 + ], + "20": [ + 7, + 20 + ], + "21": [ + 7, + 21 + ], + "22": [ + 7, + 22 + ], + "23": [ + 7, + 23 + ], + "24": [ + 7, + 24 + ], + "25": [ + 7, + 25 + ], + "26": [ + 7, + 26 + ], + "27": [ + 7, + 27 + ], + "28": [ + 7, + 28 + ], + "29": [ + 7, + 29 + ], + "30": [ + 7, + 30 + ], + "31": [ + 7, + 31 + ], + "32": [ + 7, + 32 + ], + "33": [ + 7, + 33 + ], + "34": [ + 7, + 34 + ], + "35": [ + 7, + 35 + ], + "36": [ + 7, + 36 + ], + "37": [ + 7, + 37 + ], + "38": [ + 7, + 38 + ], + "39": [ + 7, + 39 + ], + "40": [ + 7, + 40 + ], + "41": [ + 7, + 41 + ], + "42": [ + 7, + 42 + ], + "43": [ + 7, + 43 + ], + "44": [ + 7, + 44 + ], + "45": [ + 7, + 45 + ], + "46": [ + 7, + 46 + ], + "47": [ + 7, + 47 + ] + }, + "8": { + "0": [ + 8, + 0 + ], + "1": [ + 8, + 1 + ], + "2": [ + 8, + 2 + ], + "3": [ + 8, + 3 + ], + "4": [ + 8, + 4 + ], + "5": [ + 8, + 5 + ], + "6": [ + 8, + 6 + ], + "7": [ + 8, + 7 + ], + "9": [ + 8, + 9 + ], + "10": [ + 8, + 10 + ], + "11": [ + 8, + 11 + ], + "12": [ + 8, + 12 + ], + "13": [ + 8, + 13 + ], + "14": [ + 8, + 14 + ], + "15": [ + 8, + 15 + ], + "16": [ + 8, + 16 + ], + "17": [ + 8, + 17 + ], + "18": [ + 8, + 18 + ], + "19": [ + 8, + 19 + ], + "20": [ + 8, + 20 + ], + "21": [ + 8, + 21 + ], + "22": [ + 8, + 22 + ], + "23": [ + 8, + 23 + ], + "24": [ + 8, + 24 + ], + "25": [ + 8, + 25 + ], + "26": [ + 8, + 26 + ], + "27": [ + 8, + 27 + ], + "28": [ + 8, + 28 + ], + "29": [ + 8, + 29 + ], + "30": [ + 8, + 30 + ], + "31": [ + 8, + 31 + ], + "32": [ + 8, + 32 + ], + "33": [ + 8, + 33 + ], + "34": [ + 8, + 34 + ], + "35": [ + 8, + 35 + ], + "36": [ + 8, + 36 + ], + "37": [ + 8, + 37 + ], + "38": [ + 8, + 38 + ], + "39": [ + 8, + 39 + ], + "40": [ + 8, + 40 + ], + "41": [ + 8, + 41 + ], + "42": [ + 8, + 42 + ], + "43": [ + 8, + 43 + ], + "44": [ + 8, + 44 + ], + "45": [ + 8, + 45 + ], + "46": [ + 8, + 46 + ], + "47": [ + 8, + 47 + ] + }, + "9": { + "0": [ + 9, + 0 + ], + "1": [ + 9, + 1 + ], + "2": [ + 9, + 2 + ], + "3": [ + 9, + 3 + ], + "4": [ + 9, + 4 + ], + "5": [ + 9, + 5 + ], + "6": [ + 9, + 6 + ], + "7": [ + 9, + 7 + ], + "8": [ + 9, + 8 + ], + "10": [ + 9, + 10 + ], + "11": [ + 9, + 11 + ], + "12": [ + 9, + 12 + ], + "13": [ + 9, + 13 + ], + "14": [ + 9, + 14 + ], + "15": [ + 9, + 15 + ], + "16": [ + 9, + 16 + ], + "17": [ + 9, + 17 + ], + "18": [ + 9, + 18 + ], + "19": [ + 9, + 19 + ], + "20": [ + 9, + 20 + ], + "21": [ + 9, + 21 + ], + "22": [ + 9, + 22 + ], + "23": [ + 9, + 23 + ], + "24": [ + 9, + 24 + ], + "25": [ + 9, + 25 + ], + "26": [ + 9, + 26 + ], + "27": [ + 9, + 27 + ], + "28": [ + 9, + 28 + ], + "29": [ + 9, + 29 + ], + "30": [ + 9, + 30 + ], + "31": [ + 9, + 31 + ], + "32": [ + 9, + 32 + ], + "33": [ + 9, + 33 + ], + "34": [ + 9, + 34 + ], + "35": [ + 9, + 35 + ], + "36": [ + 9, + 36 + ], + "37": [ + 9, + 37 + ], + "38": [ + 9, + 38 + ], + "39": [ + 9, + 39 + ], + "40": [ + 9, + 40 + ], + "41": [ + 9, + 41 + ], + "42": [ + 9, + 42 + ], + "43": [ + 9, + 43 + ], + "44": [ + 9, + 44 + ], + "45": [ + 9, + 45 + ], + "46": [ + 9, + 46 + ], + "47": [ + 9, + 47 + ] + }, + "10": { + "0": [ + 10, + 0 + ], + "1": [ + 10, + 1 + ], + "2": [ + 10, + 2 + ], + "3": [ + 10, + 3 + ], + "4": [ + 10, + 4 + ], + "5": [ + 10, + 5 + ], + "6": [ + 10, + 6 + ], + "7": [ + 10, + 7 + ], + "8": [ + 10, + 8 + ], + "9": [ + 10, + 9 + ], + "11": [ + 10, + 11 + ], + "12": [ + 10, + 12 + ], + "13": [ + 10, + 13 + ], + "14": [ + 10, + 14 + ], + "15": [ + 10, + 15 + ], + "16": [ + 10, + 16 + ], + "17": [ + 10, + 17 + ], + "18": [ + 10, + 18 + ], + "19": [ + 10, + 19 + ], + "20": [ + 10, + 20 + ], + "21": [ + 10, + 21 + ], + "22": [ + 10, + 22 + ], + "23": [ + 10, + 23 + ], + "24": [ + 10, + 24 + ], + "25": [ + 10, + 25 + ], + "26": [ + 10, + 26 + ], + "27": [ + 10, + 27 + ], + "28": [ + 10, + 28 + ], + "29": [ + 10, + 29 + ], + "30": [ + 10, + 30 + ], + "31": [ + 10, + 31 + ], + "32": [ + 10, + 32 + ], + "33": [ + 10, + 33 + ], + "34": [ + 10, + 34 + ], + "35": [ + 10, + 35 + ], + "36": [ + 10, + 36 + ], + "37": [ + 10, + 37 + ], + "38": [ + 10, + 38 + ], + "39": [ + 10, + 39 + ], + "40": [ + 10, + 40 + ], + "41": [ + 10, + 41 + ], + "42": [ + 10, + 42 + ], + "43": [ + 10, + 43 + ], + "44": [ + 10, + 44 + ], + "45": [ + 10, + 45 + ], + "46": [ + 10, + 46 + ], + "47": [ + 10, + 47 + ] + }, + "11": { + "0": [ + 11, + 0 + ], + "1": [ + 11, + 1 + ], + "2": [ + 11, + 2 + ], + "3": [ + 11, + 3 + ], + "4": [ + 11, + 4 + ], + "5": [ + 11, + 5 + ], + "6": [ + 11, + 6 + ], + "7": [ + 11, + 7 + ], + "8": [ + 11, + 8 + ], + "9": [ + 11, + 9 + ], + "10": [ + 11, + 10 + ], + "12": [ + 11, + 12 + ], + "13": [ + 11, + 13 + ], + "14": [ + 11, + 14 + ], + "15": [ + 11, + 15 + ], + "16": [ + 11, + 16 + ], + "17": [ + 11, + 17 + ], + "18": [ + 11, + 18 + ], + "19": [ + 11, + 19 + ], + "20": [ + 11, + 20 + ], + "21": [ + 11, + 21 + ], + "22": [ + 11, + 22 + ], + "23": [ + 11, + 23 + ], + "24": [ + 11, + 24 + ], + "25": [ + 11, + 25 + ], + "26": [ + 11, + 26 + ], + "27": [ + 11, + 27 + ], + "28": [ + 11, + 28 + ], + "29": [ + 11, + 29 + ], + "30": [ + 11, + 30 + ], + "31": [ + 11, + 31 + ], + "32": [ + 11, + 32 + ], + "33": [ + 11, + 33 + ], + "34": [ + 11, + 34 + ], + "35": [ + 11, + 35 + ], + "36": [ + 11, + 36 + ], + "37": [ + 11, + 37 + ], + "38": [ + 11, + 38 + ], + "39": [ + 11, + 39 + ], + "40": [ + 11, + 40 + ], + "41": [ + 11, + 41 + ], + "42": [ + 11, + 42 + ], + "43": [ + 11, + 43 + ], + "44": [ + 11, + 44 + ], + "45": [ + 11, + 45 + ], + "46": [ + 11, + 46 + ], + "47": [ + 11, + 47 + ] + }, + "12": { + "0": [ + 12, + 0 + ], + "1": [ + 12, + 1 + ], + "2": [ + 12, + 2 + ], + "3": [ + 12, + 3 + ], + "4": [ + 12, + 4 + ], + "5": [ + 12, + 5 + ], + "6": [ + 12, + 6 + ], + "7": [ + 12, + 7 + ], + "8": [ + 12, + 8 + ], + "9": [ + 12, + 9 + ], + "10": [ + 12, + 10 + ], + "11": [ + 12, + 11 + ], + "13": [ + 12, + 13 + ], + "14": [ + 12, + 14 + ], + "15": [ + 12, + 15 + ], + "16": [ + 12, + 16 + ], + "17": [ + 12, + 17 + ], + "18": [ + 12, + 18 + ], + "19": [ + 12, + 19 + ], + "20": [ + 12, + 20 + ], + "21": [ + 12, + 21 + ], + "22": [ + 12, + 22 + ], + "23": [ + 12, + 23 + ], + "24": [ + 12, + 24 + ], + "25": [ + 12, + 25 + ], + "26": [ + 12, + 26 + ], + "27": [ + 12, + 27 + ], + "28": [ + 12, + 28 + ], + "29": [ + 12, + 29 + ], + "30": [ + 12, + 30 + ], + "31": [ + 12, + 31 + ], + "32": [ + 12, + 32 + ], + "33": [ + 12, + 33 + ], + "34": [ + 12, + 34 + ], + "35": [ + 12, + 35 + ], + "36": [ + 12, + 36 + ], + "37": [ + 12, + 37 + ], + "38": [ + 12, + 38 + ], + "39": [ + 12, + 39 + ], + "40": [ + 12, + 40 + ], + "41": [ + 12, + 41 + ], + "42": [ + 12, + 42 + ], + "43": [ + 12, + 43 + ], + "44": [ + 12, + 44 + ], + "45": [ + 12, + 45 + ], + "46": [ + 12, + 46 + ], + "47": [ + 12, + 47 + ] + }, + "13": { + "0": [ + 13, + 0 + ], + "1": [ + 13, + 1 + ], + "2": [ + 13, + 2 + ], + "3": [ + 13, + 3 + ], + "4": [ + 13, + 4 + ], + "5": [ + 13, + 5 + ], + "6": [ + 13, + 6 + ], + "7": [ + 13, + 7 + ], + "8": [ + 13, + 8 + ], + "9": [ + 13, + 9 + ], + "10": [ + 13, + 10 + ], + "11": [ + 13, + 11 + ], + "12": [ + 13, + 12 + ], + "14": [ + 13, + 14 + ], + "15": [ + 13, + 15 + ], + "16": [ + 13, + 16 + ], + "17": [ + 13, + 17 + ], + "18": [ + 13, + 18 + ], + "19": [ + 13, + 19 + ], + "20": [ + 13, + 20 + ], + "21": [ + 13, + 21 + ], + "22": [ + 13, + 22 + ], + "23": [ + 13, + 23 + ], + "24": [ + 13, + 24 + ], + "25": [ + 13, + 25 + ], + "26": [ + 13, + 26 + ], + "27": [ + 13, + 27 + ], + "28": [ + 13, + 28 + ], + "29": [ + 13, + 29 + ], + "30": [ + 13, + 30 + ], + "31": [ + 13, + 31 + ], + "32": [ + 13, + 32 + ], + "33": [ + 13, + 33 + ], + "34": [ + 13, + 34 + ], + "35": [ + 13, + 35 + ], + "36": [ + 13, + 36 + ], + "37": [ + 13, + 37 + ], + "38": [ + 13, + 38 + ], + "39": [ + 13, + 39 + ], + "40": [ + 13, + 40 + ], + "41": [ + 13, + 41 + ], + "42": [ + 13, + 42 + ], + "43": [ + 13, + 43 + ], + "44": [ + 13, + 44 + ], + "45": [ + 13, + 45 + ], + "46": [ + 13, + 46 + ], + "47": [ + 13, + 47 + ] + }, + "14": { + "0": [ + 14, + 0 + ], + "1": [ + 14, + 1 + ], + "2": [ + 14, + 2 + ], + "3": [ + 14, + 3 + ], + "4": [ + 14, + 4 + ], + "5": [ + 14, + 5 + ], + "6": [ + 14, + 6 + ], + "7": [ + 14, + 7 + ], + "8": [ + 14, + 8 + ], + "9": [ + 14, + 9 + ], + "10": [ + 14, + 10 + ], + "11": [ + 14, + 11 + ], + "12": [ + 14, + 12 + ], + "13": [ + 14, + 13 + ], + "15": [ + 14, + 15 + ], + "16": [ + 14, + 16 + ], + "17": [ + 14, + 17 + ], + "18": [ + 14, + 18 + ], + "19": [ + 14, + 19 + ], + "20": [ + 14, + 20 + ], + "21": [ + 14, + 21 + ], + "22": [ + 14, + 22 + ], + "23": [ + 14, + 23 + ], + "24": [ + 14, + 24 + ], + "25": [ + 14, + 25 + ], + "26": [ + 14, + 26 + ], + "27": [ + 14, + 27 + ], + "28": [ + 14, + 28 + ], + "29": [ + 14, + 29 + ], + "30": [ + 14, + 30 + ], + "31": [ + 14, + 31 + ], + "32": [ + 14, + 32 + ], + "33": [ + 14, + 33 + ], + "34": [ + 14, + 34 + ], + "35": [ + 14, + 35 + ], + "36": [ + 14, + 36 + ], + "37": [ + 14, + 37 + ], + "38": [ + 14, + 38 + ], + "39": [ + 14, + 39 + ], + "40": [ + 14, + 40 + ], + "41": [ + 14, + 41 + ], + "42": [ + 14, + 42 + ], + "43": [ + 14, + 43 + ], + "44": [ + 14, + 44 + ], + "45": [ + 14, + 45 + ], + "46": [ + 14, + 46 + ], + "47": [ + 14, + 47 + ] + }, + "15": { + "0": [ + 15, + 0 + ], + "1": [ + 15, + 1 + ], + "2": [ + 15, + 2 + ], + "3": [ + 15, + 3 + ], + "4": [ + 15, + 4 + ], + "5": [ + 15, + 5 + ], + "6": [ + 15, + 6 + ], + "7": [ + 15, + 7 + ], + "8": [ + 15, + 8 + ], + "9": [ + 15, + 9 + ], + "10": [ + 15, + 10 + ], + "11": [ + 15, + 11 + ], + "12": [ + 15, + 12 + ], + "13": [ + 15, + 13 + ], + "14": [ + 15, + 14 + ], + "16": [ + 15, + 16 + ], + "17": [ + 15, + 17 + ], + "18": [ + 15, + 18 + ], + "19": [ + 15, + 19 + ], + "20": [ + 15, + 20 + ], + "21": [ + 15, + 21 + ], + "22": [ + 15, + 22 + ], + "23": [ + 15, + 23 + ], + "24": [ + 15, + 24 + ], + "25": [ + 15, + 25 + ], + "26": [ + 15, + 26 + ], + "27": [ + 15, + 27 + ], + "28": [ + 15, + 28 + ], + "29": [ + 15, + 29 + ], + "30": [ + 15, + 30 + ], + "31": [ + 15, + 31 + ], + "32": [ + 15, + 32 + ], + "33": [ + 15, + 33 + ], + "34": [ + 15, + 34 + ], + "35": [ + 15, + 35 + ], + "36": [ + 15, + 36 + ], + "37": [ + 15, + 37 + ], + "38": [ + 15, + 38 + ], + "39": [ + 15, + 39 + ], + "40": [ + 15, + 40 + ], + "41": [ + 15, + 41 + ], + "42": [ + 15, + 42 + ], + "43": [ + 15, + 43 + ], + "44": [ + 15, + 44 + ], + "45": [ + 15, + 45 + ], + "46": [ + 15, + 46 + ], + "47": [ + 15, + 47 + ] + }, + "16": { + "0": [ + 16, + 0 + ], + "1": [ + 16, + 1 + ], + "2": [ + 16, + 2 + ], + "3": [ + 16, + 3 + ], + "4": [ + 16, + 4 + ], + "5": [ + 16, + 5 + ], + "6": [ + 16, + 6 + ], + "7": [ + 16, + 7 + ], + "8": [ + 16, + 8 + ], + "9": [ + 16, + 9 + ], + "10": [ + 16, + 10 + ], + "11": [ + 16, + 11 + ], + "12": [ + 16, + 12 + ], + "13": [ + 16, + 13 + ], + "14": [ + 16, + 14 + ], + "15": [ + 16, + 15 + ], + "17": [ + 16, + 17 + ], + "18": [ + 16, + 18 + ], + "19": [ + 16, + 19 + ], + "20": [ + 16, + 20 + ], + "21": [ + 16, + 21 + ], + "22": [ + 16, + 22 + ], + "23": [ + 16, + 23 + ], + "24": [ + 16, + 24 + ], + "25": [ + 16, + 25 + ], + "26": [ + 16, + 26 + ], + "27": [ + 16, + 27 + ], + "28": [ + 16, + 28 + ], + "29": [ + 16, + 29 + ], + "30": [ + 16, + 30 + ], + "31": [ + 16, + 31 + ], + "32": [ + 16, + 32 + ], + "33": [ + 16, + 33 + ], + "34": [ + 16, + 34 + ], + "35": [ + 16, + 35 + ], + "36": [ + 16, + 36 + ], + "37": [ + 16, + 37 + ], + "38": [ + 16, + 38 + ], + "39": [ + 16, + 39 + ], + "40": [ + 16, + 40 + ], + "41": [ + 16, + 41 + ], + "42": [ + 16, + 42 + ], + "43": [ + 16, + 43 + ], + "44": [ + 16, + 44 + ], + "45": [ + 16, + 45 + ], + "46": [ + 16, + 46 + ], + "47": [ + 16, + 47 + ] + }, + "17": { + "0": [ + 17, + 0 + ], + "1": [ + 17, + 1 + ], + "2": [ + 17, + 2 + ], + "3": [ + 17, + 3 + ], + "4": [ + 17, + 4 + ], + "5": [ + 17, + 5 + ], + "6": [ + 17, + 6 + ], + "7": [ + 17, + 7 + ], + "8": [ + 17, + 8 + ], + "9": [ + 17, + 9 + ], + "10": [ + 17, + 10 + ], + "11": [ + 17, + 11 + ], + "12": [ + 17, + 12 + ], + "13": [ + 17, + 13 + ], + "14": [ + 17, + 14 + ], + "15": [ + 17, + 15 + ], + "16": [ + 17, + 16 + ], + "18": [ + 17, + 18 + ], + "19": [ + 17, + 19 + ], + "20": [ + 17, + 20 + ], + "21": [ + 17, + 21 + ], + "22": [ + 17, + 22 + ], + "23": [ + 17, + 23 + ], + "24": [ + 17, + 24 + ], + "25": [ + 17, + 25 + ], + "26": [ + 17, + 26 + ], + "27": [ + 17, + 27 + ], + "28": [ + 17, + 28 + ], + "29": [ + 17, + 29 + ], + "30": [ + 17, + 30 + ], + "31": [ + 17, + 31 + ], + "32": [ + 17, + 32 + ], + "33": [ + 17, + 33 + ], + "34": [ + 17, + 34 + ], + "35": [ + 17, + 35 + ], + "36": [ + 17, + 36 + ], + "37": [ + 17, + 37 + ], + "38": [ + 17, + 38 + ], + "39": [ + 17, + 39 + ], + "40": [ + 17, + 40 + ], + "41": [ + 17, + 41 + ], + "42": [ + 17, + 42 + ], + "43": [ + 17, + 43 + ], + "44": [ + 17, + 44 + ], + "45": [ + 17, + 45 + ], + "46": [ + 17, + 46 + ], + "47": [ + 17, + 47 + ] + }, + "18": { + "0": [ + 18, + 0 + ], + "1": [ + 18, + 1 + ], + "2": [ + 18, + 2 + ], + "3": [ + 18, + 3 + ], + "4": [ + 18, + 4 + ], + "5": [ + 18, + 5 + ], + "6": [ + 18, + 6 + ], + "7": [ + 18, + 7 + ], + "8": [ + 18, + 8 + ], + "9": [ + 18, + 9 + ], + "10": [ + 18, + 10 + ], + "11": [ + 18, + 11 + ], + "12": [ + 18, + 12 + ], + "13": [ + 18, + 13 + ], + "14": [ + 18, + 14 + ], + "15": [ + 18, + 15 + ], + "16": [ + 18, + 16 + ], + "17": [ + 18, + 17 + ], + "19": [ + 18, + 19 + ], + "20": [ + 18, + 20 + ], + "21": [ + 18, + 21 + ], + "22": [ + 18, + 22 + ], + "23": [ + 18, + 23 + ], + "24": [ + 18, + 24 + ], + "25": [ + 18, + 25 + ], + "26": [ + 18, + 26 + ], + "27": [ + 18, + 27 + ], + "28": [ + 18, + 28 + ], + "29": [ + 18, + 29 + ], + "30": [ + 18, + 30 + ], + "31": [ + 18, + 31 + ], + "32": [ + 18, + 32 + ], + "33": [ + 18, + 33 + ], + "34": [ + 18, + 34 + ], + "35": [ + 18, + 35 + ], + "36": [ + 18, + 36 + ], + "37": [ + 18, + 37 + ], + "38": [ + 18, + 38 + ], + "39": [ + 18, + 39 + ], + "40": [ + 18, + 40 + ], + "41": [ + 18, + 41 + ], + "42": [ + 18, + 42 + ], + "43": [ + 18, + 43 + ], + "44": [ + 18, + 44 + ], + "45": [ + 18, + 45 + ], + "46": [ + 18, + 46 + ], + "47": [ + 18, + 47 + ] + }, + "19": { + "0": [ + 19, + 0 + ], + "1": [ + 19, + 1 + ], + "2": [ + 19, + 2 + ], + "3": [ + 19, + 3 + ], + "4": [ + 19, + 4 + ], + "5": [ + 19, + 5 + ], + "6": [ + 19, + 6 + ], + "7": [ + 19, + 7 + ], + "8": [ + 19, + 8 + ], + "9": [ + 19, + 9 + ], + "10": [ + 19, + 10 + ], + "11": [ + 19, + 11 + ], + "12": [ + 19, + 12 + ], + "13": [ + 19, + 13 + ], + "14": [ + 19, + 14 + ], + "15": [ + 19, + 15 + ], + "16": [ + 19, + 16 + ], + "17": [ + 19, + 17 + ], + "18": [ + 19, + 18 + ], + "20": [ + 19, + 20 + ], + "21": [ + 19, + 21 + ], + "22": [ + 19, + 22 + ], + "23": [ + 19, + 23 + ], + "24": [ + 19, + 24 + ], + "25": [ + 19, + 25 + ], + "26": [ + 19, + 26 + ], + "27": [ + 19, + 27 + ], + "28": [ + 19, + 28 + ], + "29": [ + 19, + 29 + ], + "30": [ + 19, + 30 + ], + "31": [ + 19, + 31 + ], + "32": [ + 19, + 32 + ], + "33": [ + 19, + 33 + ], + "34": [ + 19, + 34 + ], + "35": [ + 19, + 35 + ], + "36": [ + 19, + 36 + ], + "37": [ + 19, + 37 + ], + "38": [ + 19, + 38 + ], + "39": [ + 19, + 39 + ], + "40": [ + 19, + 40 + ], + "41": [ + 19, + 41 + ], + "42": [ + 19, + 42 + ], + "43": [ + 19, + 43 + ], + "44": [ + 19, + 44 + ], + "45": [ + 19, + 45 + ], + "46": [ + 19, + 46 + ], + "47": [ + 19, + 47 + ] + }, + "20": { + "0": [ + 20, + 0 + ], + "1": [ + 20, + 1 + ], + "2": [ + 20, + 2 + ], + "3": [ + 20, + 3 + ], + "4": [ + 20, + 4 + ], + "5": [ + 20, + 5 + ], + "6": [ + 20, + 6 + ], + "7": [ + 20, + 7 + ], + "8": [ + 20, + 8 + ], + "9": [ + 20, + 9 + ], + "10": [ + 20, + 10 + ], + "11": [ + 20, + 11 + ], + "12": [ + 20, + 12 + ], + "13": [ + 20, + 13 + ], + "14": [ + 20, + 14 + ], + "15": [ + 20, + 15 + ], + "16": [ + 20, + 16 + ], + "17": [ + 20, + 17 + ], + "18": [ + 20, + 18 + ], + "19": [ + 20, + 19 + ], + "21": [ + 20, + 21 + ], + "22": [ + 20, + 22 + ], + "23": [ + 20, + 23 + ], + "24": [ + 20, + 24 + ], + "25": [ + 20, + 25 + ], + "26": [ + 20, + 26 + ], + "27": [ + 20, + 27 + ], + "28": [ + 20, + 28 + ], + "29": [ + 20, + 29 + ], + "30": [ + 20, + 30 + ], + "31": [ + 20, + 31 + ], + "32": [ + 20, + 32 + ], + "33": [ + 20, + 33 + ], + "34": [ + 20, + 34 + ], + "35": [ + 20, + 35 + ], + "36": [ + 20, + 36 + ], + "37": [ + 20, + 37 + ], + "38": [ + 20, + 38 + ], + "39": [ + 20, + 39 + ], + "40": [ + 20, + 40 + ], + "41": [ + 20, + 41 + ], + "42": [ + 20, + 42 + ], + "43": [ + 20, + 43 + ], + "44": [ + 20, + 44 + ], + "45": [ + 20, + 45 + ], + "46": [ + 20, + 46 + ], + "47": [ + 20, + 47 + ] + }, + "21": { + "0": [ + 21, + 0 + ], + "1": [ + 21, + 1 + ], + "2": [ + 21, + 2 + ], + "3": [ + 21, + 3 + ], + "4": [ + 21, + 4 + ], + "5": [ + 21, + 5 + ], + "6": [ + 21, + 6 + ], + "7": [ + 21, + 7 + ], + "8": [ + 21, + 8 + ], + "9": [ + 21, + 9 + ], + "10": [ + 21, + 10 + ], + "11": [ + 21, + 11 + ], + "12": [ + 21, + 12 + ], + "13": [ + 21, + 13 + ], + "14": [ + 21, + 14 + ], + "15": [ + 21, + 15 + ], + "16": [ + 21, + 16 + ], + "17": [ + 21, + 17 + ], + "18": [ + 21, + 18 + ], + "19": [ + 21, + 19 + ], + "20": [ + 21, + 20 + ], + "22": [ + 21, + 22 + ], + "23": [ + 21, + 23 + ], + "24": [ + 21, + 24 + ], + "25": [ + 21, + 25 + ], + "26": [ + 21, + 26 + ], + "27": [ + 21, + 27 + ], + "28": [ + 21, + 28 + ], + "29": [ + 21, + 29 + ], + "30": [ + 21, + 30 + ], + "31": [ + 21, + 31 + ], + "32": [ + 21, + 32 + ], + "33": [ + 21, + 33 + ], + "34": [ + 21, + 34 + ], + "35": [ + 21, + 35 + ], + "36": [ + 21, + 36 + ], + "37": [ + 21, + 37 + ], + "38": [ + 21, + 38 + ], + "39": [ + 21, + 39 + ], + "40": [ + 21, + 40 + ], + "41": [ + 21, + 41 + ], + "42": [ + 21, + 42 + ], + "43": [ + 21, + 43 + ], + "44": [ + 21, + 44 + ], + "45": [ + 21, + 45 + ], + "46": [ + 21, + 46 + ], + "47": [ + 21, + 47 + ] + }, + "22": { + "0": [ + 22, + 0 + ], + "1": [ + 22, + 1 + ], + "2": [ + 22, + 2 + ], + "3": [ + 22, + 3 + ], + "4": [ + 22, + 4 + ], + "5": [ + 22, + 5 + ], + "6": [ + 22, + 6 + ], + "7": [ + 22, + 7 + ], + "8": [ + 22, + 8 + ], + "9": [ + 22, + 9 + ], + "10": [ + 22, + 10 + ], + "11": [ + 22, + 11 + ], + "12": [ + 22, + 12 + ], + "13": [ + 22, + 13 + ], + "14": [ + 22, + 14 + ], + "15": [ + 22, + 15 + ], + "16": [ + 22, + 16 + ], + "17": [ + 22, + 17 + ], + "18": [ + 22, + 18 + ], + "19": [ + 22, + 19 + ], + "20": [ + 22, + 20 + ], + "21": [ + 22, + 21 + ], + "23": [ + 22, + 23 + ], + "24": [ + 22, + 24 + ], + "25": [ + 22, + 25 + ], + "26": [ + 22, + 26 + ], + "27": [ + 22, + 27 + ], + "28": [ + 22, + 28 + ], + "29": [ + 22, + 29 + ], + "30": [ + 22, + 30 + ], + "31": [ + 22, + 31 + ], + "32": [ + 22, + 32 + ], + "33": [ + 22, + 33 + ], + "34": [ + 22, + 34 + ], + "35": [ + 22, + 35 + ], + "36": [ + 22, + 36 + ], + "37": [ + 22, + 37 + ], + "38": [ + 22, + 38 + ], + "39": [ + 22, + 39 + ], + "40": [ + 22, + 40 + ], + "41": [ + 22, + 41 + ], + "42": [ + 22, + 42 + ], + "43": [ + 22, + 43 + ], + "44": [ + 22, + 44 + ], + "45": [ + 22, + 45 + ], + "46": [ + 22, + 46 + ], + "47": [ + 22, + 47 + ] + }, + "23": { + "0": [ + 23, + 0 + ], + "1": [ + 23, + 1 + ], + "2": [ + 23, + 2 + ], + "3": [ + 23, + 3 + ], + "4": [ + 23, + 4 + ], + "5": [ + 23, + 5 + ], + "6": [ + 23, + 6 + ], + "7": [ + 23, + 7 + ], + "8": [ + 23, + 8 + ], + "9": [ + 23, + 9 + ], + "10": [ + 23, + 10 + ], + "11": [ + 23, + 11 + ], + "12": [ + 23, + 12 + ], + "13": [ + 23, + 13 + ], + "14": [ + 23, + 14 + ], + "15": [ + 23, + 15 + ], + "16": [ + 23, + 16 + ], + "17": [ + 23, + 17 + ], + "18": [ + 23, + 18 + ], + "19": [ + 23, + 19 + ], + "20": [ + 23, + 20 + ], + "21": [ + 23, + 21 + ], + "22": [ + 23, + 22 + ], + "24": [ + 23, + 24 + ], + "25": [ + 23, + 25 + ], + "26": [ + 23, + 26 + ], + "27": [ + 23, + 27 + ], + "28": [ + 23, + 28 + ], + "29": [ + 23, + 29 + ], + "30": [ + 23, + 30 + ], + "31": [ + 23, + 31 + ], + "32": [ + 23, + 32 + ], + "33": [ + 23, + 33 + ], + "34": [ + 23, + 34 + ], + "35": [ + 23, + 35 + ], + "36": [ + 23, + 36 + ], + "37": [ + 23, + 37 + ], + "38": [ + 23, + 38 + ], + "39": [ + 23, + 39 + ], + "40": [ + 23, + 40 + ], + "41": [ + 23, + 41 + ], + "42": [ + 23, + 42 + ], + "43": [ + 23, + 43 + ], + "44": [ + 23, + 44 + ], + "45": [ + 23, + 45 + ], + "46": [ + 23, + 46 + ], + "47": [ + 23, + 47 + ] + }, + "24": { + "0": [ + 24, + 0 + ], + "1": [ + 24, + 1 + ], + "2": [ + 24, + 2 + ], + "3": [ + 24, + 3 + ], + "4": [ + 24, + 4 + ], + "5": [ + 24, + 5 + ], + "6": [ + 24, + 6 + ], + "7": [ + 24, + 7 + ], + "8": [ + 24, + 8 + ], + "9": [ + 24, + 9 + ], + "10": [ + 24, + 10 + ], + "11": [ + 24, + 11 + ], + "12": [ + 24, + 12 + ], + "13": [ + 24, + 13 + ], + "14": [ + 24, + 14 + ], + "15": [ + 24, + 15 + ], + "16": [ + 24, + 16 + ], + "17": [ + 24, + 17 + ], + "18": [ + 24, + 18 + ], + "19": [ + 24, + 19 + ], + "20": [ + 24, + 20 + ], + "21": [ + 24, + 21 + ], + "22": [ + 24, + 22 + ], + "23": [ + 24, + 23 + ], + "25": [ + 24, + 25 + ], + "26": [ + 24, + 26 + ], + "27": [ + 24, + 27 + ], + "28": [ + 24, + 28 + ], + "29": [ + 24, + 29 + ], + "30": [ + 24, + 30 + ], + "31": [ + 24, + 31 + ], + "32": [ + 24, + 32 + ], + "33": [ + 24, + 33 + ], + "34": [ + 24, + 34 + ], + "35": [ + 24, + 35 + ], + "36": [ + 24, + 36 + ], + "37": [ + 24, + 37 + ], + "38": [ + 24, + 38 + ], + "39": [ + 24, + 39 + ], + "40": [ + 24, + 40 + ], + "41": [ + 24, + 41 + ], + "42": [ + 24, + 42 + ], + "43": [ + 24, + 43 + ], + "44": [ + 24, + 44 + ], + "45": [ + 24, + 45 + ], + "46": [ + 24, + 46 + ], + "47": [ + 24, + 47 + ] + }, + "25": { + "0": [ + 25, + 0 + ], + "1": [ + 25, + 1 + ], + "2": [ + 25, + 2 + ], + "3": [ + 25, + 3 + ], + "4": [ + 25, + 4 + ], + "5": [ + 25, + 5 + ], + "6": [ + 25, + 6 + ], + "7": [ + 25, + 7 + ], + "8": [ + 25, + 8 + ], + "9": [ + 25, + 9 + ], + "10": [ + 25, + 10 + ], + "11": [ + 25, + 11 + ], + "12": [ + 25, + 12 + ], + "13": [ + 25, + 13 + ], + "14": [ + 25, + 14 + ], + "15": [ + 25, + 15 + ], + "16": [ + 25, + 16 + ], + "17": [ + 25, + 17 + ], + "18": [ + 25, + 18 + ], + "19": [ + 25, + 19 + ], + "20": [ + 25, + 20 + ], + "21": [ + 25, + 21 + ], + "22": [ + 25, + 22 + ], + "23": [ + 25, + 23 + ], + "24": [ + 25, + 24 + ], + "26": [ + 25, + 26 + ], + "27": [ + 25, + 27 + ], + "28": [ + 25, + 28 + ], + "29": [ + 25, + 29 + ], + "30": [ + 25, + 30 + ], + "31": [ + 25, + 31 + ], + "32": [ + 25, + 32 + ], + "33": [ + 25, + 33 + ], + "34": [ + 25, + 34 + ], + "35": [ + 25, + 35 + ], + "36": [ + 25, + 36 + ], + "37": [ + 25, + 37 + ], + "38": [ + 25, + 38 + ], + "39": [ + 25, + 39 + ], + "40": [ + 25, + 40 + ], + "41": [ + 25, + 41 + ], + "42": [ + 25, + 42 + ], + "43": [ + 25, + 43 + ], + "44": [ + 25, + 44 + ], + "45": [ + 25, + 45 + ], + "46": [ + 25, + 46 + ], + "47": [ + 25, + 47 + ] + }, + "26": { + "0": [ + 26, + 0 + ], + "1": [ + 26, + 1 + ], + "2": [ + 26, + 2 + ], + "3": [ + 26, + 3 + ], + "4": [ + 26, + 4 + ], + "5": [ + 26, + 5 + ], + "6": [ + 26, + 6 + ], + "7": [ + 26, + 7 + ], + "8": [ + 26, + 8 + ], + "9": [ + 26, + 9 + ], + "10": [ + 26, + 10 + ], + "11": [ + 26, + 11 + ], + "12": [ + 26, + 12 + ], + "13": [ + 26, + 13 + ], + "14": [ + 26, + 14 + ], + "15": [ + 26, + 15 + ], + "16": [ + 26, + 16 + ], + "17": [ + 26, + 17 + ], + "18": [ + 26, + 18 + ], + "19": [ + 26, + 19 + ], + "20": [ + 26, + 20 + ], + "21": [ + 26, + 21 + ], + "22": [ + 26, + 22 + ], + "23": [ + 26, + 23 + ], + "24": [ + 26, + 24 + ], + "25": [ + 26, + 25 + ], + "27": [ + 26, + 27 + ], + "28": [ + 26, + 28 + ], + "29": [ + 26, + 29 + ], + "30": [ + 26, + 30 + ], + "31": [ + 26, + 31 + ], + "32": [ + 26, + 32 + ], + "33": [ + 26, + 33 + ], + "34": [ + 26, + 34 + ], + "35": [ + 26, + 35 + ], + "36": [ + 26, + 36 + ], + "37": [ + 26, + 37 + ], + "38": [ + 26, + 38 + ], + "39": [ + 26, + 39 + ], + "40": [ + 26, + 40 + ], + "41": [ + 26, + 41 + ], + "42": [ + 26, + 42 + ], + "43": [ + 26, + 43 + ], + "44": [ + 26, + 44 + ], + "45": [ + 26, + 45 + ], + "46": [ + 26, + 46 + ], + "47": [ + 26, + 47 + ] + }, + "27": { + "0": [ + 27, + 0 + ], + "1": [ + 27, + 1 + ], + "2": [ + 27, + 2 + ], + "3": [ + 27, + 3 + ], + "4": [ + 27, + 4 + ], + "5": [ + 27, + 5 + ], + "6": [ + 27, + 6 + ], + "7": [ + 27, + 7 + ], + "8": [ + 27, + 8 + ], + "9": [ + 27, + 9 + ], + "10": [ + 27, + 10 + ], + "11": [ + 27, + 11 + ], + "12": [ + 27, + 12 + ], + "13": [ + 27, + 13 + ], + "14": [ + 27, + 14 + ], + "15": [ + 27, + 15 + ], + "16": [ + 27, + 16 + ], + "17": [ + 27, + 17 + ], + "18": [ + 27, + 18 + ], + "19": [ + 27, + 19 + ], + "20": [ + 27, + 20 + ], + "21": [ + 27, + 21 + ], + "22": [ + 27, + 22 + ], + "23": [ + 27, + 23 + ], + "24": [ + 27, + 24 + ], + "25": [ + 27, + 25 + ], + "26": [ + 27, + 26 + ], + "28": [ + 27, + 28 + ], + "29": [ + 27, + 29 + ], + "30": [ + 27, + 30 + ], + "31": [ + 27, + 31 + ], + "32": [ + 27, + 32 + ], + "33": [ + 27, + 33 + ], + "34": [ + 27, + 34 + ], + "35": [ + 27, + 35 + ], + "36": [ + 27, + 36 + ], + "37": [ + 27, + 37 + ], + "38": [ + 27, + 38 + ], + "39": [ + 27, + 39 + ], + "40": [ + 27, + 40 + ], + "41": [ + 27, + 41 + ], + "42": [ + 27, + 42 + ], + "43": [ + 27, + 43 + ], + "44": [ + 27, + 44 + ], + "45": [ + 27, + 45 + ], + "46": [ + 27, + 46 + ], + "47": [ + 27, + 47 + ] + }, + "28": { + "0": [ + 28, + 0 + ], + "1": [ + 28, + 1 + ], + "2": [ + 28, + 2 + ], + "3": [ + 28, + 3 + ], + "4": [ + 28, + 4 + ], + "5": [ + 28, + 5 + ], + "6": [ + 28, + 6 + ], + "7": [ + 28, + 7 + ], + "8": [ + 28, + 8 + ], + "9": [ + 28, + 9 + ], + "10": [ + 28, + 10 + ], + "11": [ + 28, + 11 + ], + "12": [ + 28, + 12 + ], + "13": [ + 28, + 13 + ], + "14": [ + 28, + 14 + ], + "15": [ + 28, + 15 + ], + "16": [ + 28, + 16 + ], + "17": [ + 28, + 17 + ], + "18": [ + 28, + 18 + ], + "19": [ + 28, + 19 + ], + "20": [ + 28, + 20 + ], + "21": [ + 28, + 21 + ], + "22": [ + 28, + 22 + ], + "23": [ + 28, + 23 + ], + "24": [ + 28, + 24 + ], + "25": [ + 28, + 25 + ], + "26": [ + 28, + 26 + ], + "27": [ + 28, + 27 + ], + "29": [ + 28, + 29 + ], + "30": [ + 28, + 30 + ], + "31": [ + 28, + 31 + ], + "32": [ + 28, + 32 + ], + "33": [ + 28, + 33 + ], + "34": [ + 28, + 34 + ], + "35": [ + 28, + 35 + ], + "36": [ + 28, + 36 + ], + "37": [ + 28, + 37 + ], + "38": [ + 28, + 38 + ], + "39": [ + 28, + 39 + ], + "40": [ + 28, + 40 + ], + "41": [ + 28, + 41 + ], + "42": [ + 28, + 42 + ], + "43": [ + 28, + 43 + ], + "44": [ + 28, + 44 + ], + "45": [ + 28, + 45 + ], + "46": [ + 28, + 46 + ], + "47": [ + 28, + 47 + ] + }, + "29": { + "0": [ + 29, + 0 + ], + "1": [ + 29, + 1 + ], + "2": [ + 29, + 2 + ], + "3": [ + 29, + 3 + ], + "4": [ + 29, + 4 + ], + "5": [ + 29, + 5 + ], + "6": [ + 29, + 6 + ], + "7": [ + 29, + 7 + ], + "8": [ + 29, + 8 + ], + "9": [ + 29, + 9 + ], + "10": [ + 29, + 10 + ], + "11": [ + 29, + 11 + ], + "12": [ + 29, + 12 + ], + "13": [ + 29, + 13 + ], + "14": [ + 29, + 14 + ], + "15": [ + 29, + 15 + ], + "16": [ + 29, + 16 + ], + "17": [ + 29, + 17 + ], + "18": [ + 29, + 18 + ], + "19": [ + 29, + 19 + ], + "20": [ + 29, + 20 + ], + "21": [ + 29, + 21 + ], + "22": [ + 29, + 22 + ], + "23": [ + 29, + 23 + ], + "24": [ + 29, + 24 + ], + "25": [ + 29, + 25 + ], + "26": [ + 29, + 26 + ], + "27": [ + 29, + 27 + ], + "28": [ + 29, + 28 + ], + "30": [ + 29, + 30 + ], + "31": [ + 29, + 31 + ], + "32": [ + 29, + 32 + ], + "33": [ + 29, + 33 + ], + "34": [ + 29, + 34 + ], + "35": [ + 29, + 35 + ], + "36": [ + 29, + 36 + ], + "37": [ + 29, + 37 + ], + "38": [ + 29, + 38 + ], + "39": [ + 29, + 39 + ], + "40": [ + 29, + 40 + ], + "41": [ + 29, + 41 + ], + "42": [ + 29, + 42 + ], + "43": [ + 29, + 43 + ], + "44": [ + 29, + 44 + ], + "45": [ + 29, + 45 + ], + "46": [ + 29, + 46 + ], + "47": [ + 29, + 47 + ] + }, + "30": { + "0": [ + 30, + 0 + ], + "1": [ + 30, + 1 + ], + "2": [ + 30, + 2 + ], + "3": [ + 30, + 3 + ], + "4": [ + 30, + 4 + ], + "5": [ + 30, + 5 + ], + "6": [ + 30, + 6 + ], + "7": [ + 30, + 7 + ], + "8": [ + 30, + 8 + ], + "9": [ + 30, + 9 + ], + "10": [ + 30, + 10 + ], + "11": [ + 30, + 11 + ], + "12": [ + 30, + 12 + ], + "13": [ + 30, + 13 + ], + "14": [ + 30, + 14 + ], + "15": [ + 30, + 15 + ], + "16": [ + 30, + 16 + ], + "17": [ + 30, + 17 + ], + "18": [ + 30, + 18 + ], + "19": [ + 30, + 19 + ], + "20": [ + 30, + 20 + ], + "21": [ + 30, + 21 + ], + "22": [ + 30, + 22 + ], + "23": [ + 30, + 23 + ], + "24": [ + 30, + 24 + ], + "25": [ + 30, + 25 + ], + "26": [ + 30, + 26 + ], + "27": [ + 30, + 27 + ], + "28": [ + 30, + 28 + ], + "29": [ + 30, + 29 + ], + "31": [ + 30, + 31 + ], + "32": [ + 30, + 32 + ], + "33": [ + 30, + 33 + ], + "34": [ + 30, + 34 + ], + "35": [ + 30, + 35 + ], + "36": [ + 30, + 36 + ], + "37": [ + 30, + 37 + ], + "38": [ + 30, + 38 + ], + "39": [ + 30, + 39 + ], + "40": [ + 30, + 40 + ], + "41": [ + 30, + 41 + ], + "42": [ + 30, + 42 + ], + "43": [ + 30, + 43 + ], + "44": [ + 30, + 44 + ], + "45": [ + 30, + 45 + ], + "46": [ + 30, + 46 + ], + "47": [ + 30, + 47 + ] + }, + "31": { + "0": [ + 31, + 0 + ], + "1": [ + 31, + 1 + ], + "2": [ + 31, + 2 + ], + "3": [ + 31, + 3 + ], + "4": [ + 31, + 4 + ], + "5": [ + 31, + 5 + ], + "6": [ + 31, + 6 + ], + "7": [ + 31, + 7 + ], + "8": [ + 31, + 8 + ], + "9": [ + 31, + 9 + ], + "10": [ + 31, + 10 + ], + "11": [ + 31, + 11 + ], + "12": [ + 31, + 12 + ], + "13": [ + 31, + 13 + ], + "14": [ + 31, + 14 + ], + "15": [ + 31, + 15 + ], + "16": [ + 31, + 16 + ], + "17": [ + 31, + 17 + ], + "18": [ + 31, + 18 + ], + "19": [ + 31, + 19 + ], + "20": [ + 31, + 20 + ], + "21": [ + 31, + 21 + ], + "22": [ + 31, + 22 + ], + "23": [ + 31, + 23 + ], + "24": [ + 31, + 24 + ], + "25": [ + 31, + 25 + ], + "26": [ + 31, + 26 + ], + "27": [ + 31, + 27 + ], + "28": [ + 31, + 28 + ], + "29": [ + 31, + 29 + ], + "30": [ + 31, + 30 + ], + "32": [ + 31, + 32 + ], + "33": [ + 31, + 33 + ], + "34": [ + 31, + 34 + ], + "35": [ + 31, + 35 + ], + "36": [ + 31, + 36 + ], + "37": [ + 31, + 37 + ], + "38": [ + 31, + 38 + ], + "39": [ + 31, + 39 + ], + "40": [ + 31, + 40 + ], + "41": [ + 31, + 41 + ], + "42": [ + 31, + 42 + ], + "43": [ + 31, + 43 + ], + "44": [ + 31, + 44 + ], + "45": [ + 31, + 45 + ], + "46": [ + 31, + 46 + ], + "47": [ + 31, + 47 + ] + }, + "32": { + "0": [ + 32, + 0 + ], + "1": [ + 32, + 1 + ], + "2": [ + 32, + 2 + ], + "3": [ + 32, + 3 + ], + "4": [ + 32, + 4 + ], + "5": [ + 32, + 5 + ], + "6": [ + 32, + 6 + ], + "7": [ + 32, + 7 + ], + "8": [ + 32, + 8 + ], + "9": [ + 32, + 9 + ], + "10": [ + 32, + 10 + ], + "11": [ + 32, + 11 + ], + "12": [ + 32, + 12 + ], + "13": [ + 32, + 13 + ], + "14": [ + 32, + 14 + ], + "15": [ + 32, + 15 + ], + "16": [ + 32, + 16 + ], + "17": [ + 32, + 17 + ], + "18": [ + 32, + 18 + ], + "19": [ + 32, + 19 + ], + "20": [ + 32, + 20 + ], + "21": [ + 32, + 21 + ], + "22": [ + 32, + 22 + ], + "23": [ + 32, + 23 + ], + "24": [ + 32, + 24 + ], + "25": [ + 32, + 25 + ], + "26": [ + 32, + 26 + ], + "27": [ + 32, + 27 + ], + "28": [ + 32, + 28 + ], + "29": [ + 32, + 29 + ], + "30": [ + 32, + 30 + ], + "31": [ + 32, + 31 + ], + "33": [ + 32, + 33 + ], + "34": [ + 32, + 34 + ], + "35": [ + 32, + 35 + ], + "36": [ + 32, + 36 + ], + "37": [ + 32, + 37 + ], + "38": [ + 32, + 38 + ], + "39": [ + 32, + 39 + ], + "40": [ + 32, + 40 + ], + "41": [ + 32, + 41 + ], + "42": [ + 32, + 42 + ], + "43": [ + 32, + 43 + ], + "44": [ + 32, + 44 + ], + "45": [ + 32, + 45 + ], + "46": [ + 32, + 46 + ], + "47": [ + 32, + 47 + ] + }, + "33": { + "0": [ + 33, + 0 + ], + "1": [ + 33, + 1 + ], + "2": [ + 33, + 2 + ], + "3": [ + 33, + 3 + ], + "4": [ + 33, + 4 + ], + "5": [ + 33, + 5 + ], + "6": [ + 33, + 6 + ], + "7": [ + 33, + 7 + ], + "8": [ + 33, + 8 + ], + "9": [ + 33, + 9 + ], + "10": [ + 33, + 10 + ], + "11": [ + 33, + 11 + ], + "12": [ + 33, + 12 + ], + "13": [ + 33, + 13 + ], + "14": [ + 33, + 14 + ], + "15": [ + 33, + 15 + ], + "16": [ + 33, + 16 + ], + "17": [ + 33, + 17 + ], + "18": [ + 33, + 18 + ], + "19": [ + 33, + 19 + ], + "20": [ + 33, + 20 + ], + "21": [ + 33, + 21 + ], + "22": [ + 33, + 22 + ], + "23": [ + 33, + 23 + ], + "24": [ + 33, + 24 + ], + "25": [ + 33, + 25 + ], + "26": [ + 33, + 26 + ], + "27": [ + 33, + 27 + ], + "28": [ + 33, + 28 + ], + "29": [ + 33, + 29 + ], + "30": [ + 33, + 30 + ], + "31": [ + 33, + 31 + ], + "32": [ + 33, + 32 + ], + "34": [ + 33, + 34 + ], + "35": [ + 33, + 35 + ], + "36": [ + 33, + 36 + ], + "37": [ + 33, + 37 + ], + "38": [ + 33, + 38 + ], + "39": [ + 33, + 39 + ], + "40": [ + 33, + 40 + ], + "41": [ + 33, + 41 + ], + "42": [ + 33, + 42 + ], + "43": [ + 33, + 43 + ], + "44": [ + 33, + 44 + ], + "45": [ + 33, + 45 + ], + "46": [ + 33, + 46 + ], + "47": [ + 33, + 47 + ] + }, + "34": { + "0": [ + 34, + 0 + ], + "1": [ + 34, + 1 + ], + "2": [ + 34, + 2 + ], + "3": [ + 34, + 3 + ], + "4": [ + 34, + 4 + ], + "5": [ + 34, + 5 + ], + "6": [ + 34, + 6 + ], + "7": [ + 34, + 7 + ], + "8": [ + 34, + 8 + ], + "9": [ + 34, + 9 + ], + "10": [ + 34, + 10 + ], + "11": [ + 34, + 11 + ], + "12": [ + 34, + 12 + ], + "13": [ + 34, + 13 + ], + "14": [ + 34, + 14 + ], + "15": [ + 34, + 15 + ], + "16": [ + 34, + 16 + ], + "17": [ + 34, + 17 + ], + "18": [ + 34, + 18 + ], + "19": [ + 34, + 19 + ], + "20": [ + 34, + 20 + ], + "21": [ + 34, + 21 + ], + "22": [ + 34, + 22 + ], + "23": [ + 34, + 23 + ], + "24": [ + 34, + 24 + ], + "25": [ + 34, + 25 + ], + "26": [ + 34, + 26 + ], + "27": [ + 34, + 27 + ], + "28": [ + 34, + 28 + ], + "29": [ + 34, + 29 + ], + "30": [ + 34, + 30 + ], + "31": [ + 34, + 31 + ], + "32": [ + 34, + 32 + ], + "33": [ + 34, + 33 + ], + "35": [ + 34, + 35 + ], + "36": [ + 34, + 36 + ], + "37": [ + 34, + 37 + ], + "38": [ + 34, + 38 + ], + "39": [ + 34, + 39 + ], + "40": [ + 34, + 40 + ], + "41": [ + 34, + 41 + ], + "42": [ + 34, + 42 + ], + "43": [ + 34, + 43 + ], + "44": [ + 34, + 44 + ], + "45": [ + 34, + 45 + ], + "46": [ + 34, + 46 + ], + "47": [ + 34, + 47 + ] + }, + "35": { + "0": [ + 35, + 0 + ], + "1": [ + 35, + 1 + ], + "2": [ + 35, + 2 + ], + "3": [ + 35, + 3 + ], + "4": [ + 35, + 4 + ], + "5": [ + 35, + 5 + ], + "6": [ + 35, + 6 + ], + "7": [ + 35, + 7 + ], + "8": [ + 35, + 8 + ], + "9": [ + 35, + 9 + ], + "10": [ + 35, + 10 + ], + "11": [ + 35, + 11 + ], + "12": [ + 35, + 12 + ], + "13": [ + 35, + 13 + ], + "14": [ + 35, + 14 + ], + "15": [ + 35, + 15 + ], + "16": [ + 35, + 16 + ], + "17": [ + 35, + 17 + ], + "18": [ + 35, + 18 + ], + "19": [ + 35, + 19 + ], + "20": [ + 35, + 20 + ], + "21": [ + 35, + 21 + ], + "22": [ + 35, + 22 + ], + "23": [ + 35, + 23 + ], + "24": [ + 35, + 24 + ], + "25": [ + 35, + 25 + ], + "26": [ + 35, + 26 + ], + "27": [ + 35, + 27 + ], + "28": [ + 35, + 28 + ], + "29": [ + 35, + 29 + ], + "30": [ + 35, + 30 + ], + "31": [ + 35, + 31 + ], + "32": [ + 35, + 32 + ], + "33": [ + 35, + 33 + ], + "34": [ + 35, + 34 + ], + "36": [ + 35, + 36 + ], + "37": [ + 35, + 37 + ], + "38": [ + 35, + 38 + ], + "39": [ + 35, + 39 + ], + "40": [ + 35, + 40 + ], + "41": [ + 35, + 41 + ], + "42": [ + 35, + 42 + ], + "43": [ + 35, + 43 + ], + "44": [ + 35, + 44 + ], + "45": [ + 35, + 45 + ], + "46": [ + 35, + 46 + ], + "47": [ + 35, + 47 + ] + }, + "36": { + "0": [ + 36, + 0 + ], + "1": [ + 36, + 1 + ], + "2": [ + 36, + 2 + ], + "3": [ + 36, + 3 + ], + "4": [ + 36, + 4 + ], + "5": [ + 36, + 5 + ], + "6": [ + 36, + 6 + ], + "7": [ + 36, + 7 + ], + "8": [ + 36, + 8 + ], + "9": [ + 36, + 9 + ], + "10": [ + 36, + 10 + ], + "11": [ + 36, + 11 + ], + "12": [ + 36, + 12 + ], + "13": [ + 36, + 13 + ], + "14": [ + 36, + 14 + ], + "15": [ + 36, + 15 + ], + "16": [ + 36, + 16 + ], + "17": [ + 36, + 17 + ], + "18": [ + 36, + 18 + ], + "19": [ + 36, + 19 + ], + "20": [ + 36, + 20 + ], + "21": [ + 36, + 21 + ], + "22": [ + 36, + 22 + ], + "23": [ + 36, + 23 + ], + "24": [ + 36, + 24 + ], + "25": [ + 36, + 25 + ], + "26": [ + 36, + 26 + ], + "27": [ + 36, + 27 + ], + "28": [ + 36, + 28 + ], + "29": [ + 36, + 29 + ], + "30": [ + 36, + 30 + ], + "31": [ + 36, + 31 + ], + "32": [ + 36, + 32 + ], + "33": [ + 36, + 33 + ], + "34": [ + 36, + 34 + ], + "35": [ + 36, + 35 + ], + "37": [ + 36, + 37 + ], + "38": [ + 36, + 38 + ], + "39": [ + 36, + 39 + ], + "40": [ + 36, + 40 + ], + "41": [ + 36, + 41 + ], + "42": [ + 36, + 42 + ], + "43": [ + 36, + 43 + ], + "44": [ + 36, + 44 + ], + "45": [ + 36, + 45 + ], + "46": [ + 36, + 46 + ], + "47": [ + 36, + 47 + ] + }, + "37": { + "0": [ + 37, + 0 + ], + "1": [ + 37, + 1 + ], + "2": [ + 37, + 2 + ], + "3": [ + 37, + 3 + ], + "4": [ + 37, + 4 + ], + "5": [ + 37, + 5 + ], + "6": [ + 37, + 6 + ], + "7": [ + 37, + 7 + ], + "8": [ + 37, + 8 + ], + "9": [ + 37, + 9 + ], + "10": [ + 37, + 10 + ], + "11": [ + 37, + 11 + ], + "12": [ + 37, + 12 + ], + "13": [ + 37, + 13 + ], + "14": [ + 37, + 14 + ], + "15": [ + 37, + 15 + ], + "16": [ + 37, + 16 + ], + "17": [ + 37, + 17 + ], + "18": [ + 37, + 18 + ], + "19": [ + 37, + 19 + ], + "20": [ + 37, + 20 + ], + "21": [ + 37, + 21 + ], + "22": [ + 37, + 22 + ], + "23": [ + 37, + 23 + ], + "24": [ + 37, + 24 + ], + "25": [ + 37, + 25 + ], + "26": [ + 37, + 26 + ], + "27": [ + 37, + 27 + ], + "28": [ + 37, + 28 + ], + "29": [ + 37, + 29 + ], + "30": [ + 37, + 30 + ], + "31": [ + 37, + 31 + ], + "32": [ + 37, + 32 + ], + "33": [ + 37, + 33 + ], + "34": [ + 37, + 34 + ], + "35": [ + 37, + 35 + ], + "36": [ + 37, + 36 + ], + "38": [ + 37, + 38 + ], + "39": [ + 37, + 39 + ], + "40": [ + 37, + 40 + ], + "41": [ + 37, + 41 + ], + "42": [ + 37, + 42 + ], + "43": [ + 37, + 43 + ], + "44": [ + 37, + 44 + ], + "45": [ + 37, + 45 + ], + "46": [ + 37, + 46 + ], + "47": [ + 37, + 47 + ] + }, + "38": { + "0": [ + 38, + 0 + ], + "1": [ + 38, + 1 + ], + "2": [ + 38, + 2 + ], + "3": [ + 38, + 3 + ], + "4": [ + 38, + 4 + ], + "5": [ + 38, + 5 + ], + "6": [ + 38, + 6 + ], + "7": [ + 38, + 7 + ], + "8": [ + 38, + 8 + ], + "9": [ + 38, + 9 + ], + "10": [ + 38, + 10 + ], + "11": [ + 38, + 11 + ], + "12": [ + 38, + 12 + ], + "13": [ + 38, + 13 + ], + "14": [ + 38, + 14 + ], + "15": [ + 38, + 15 + ], + "16": [ + 38, + 16 + ], + "17": [ + 38, + 17 + ], + "18": [ + 38, + 18 + ], + "19": [ + 38, + 19 + ], + "20": [ + 38, + 20 + ], + "21": [ + 38, + 21 + ], + "22": [ + 38, + 22 + ], + "23": [ + 38, + 23 + ], + "24": [ + 38, + 24 + ], + "25": [ + 38, + 25 + ], + "26": [ + 38, + 26 + ], + "27": [ + 38, + 27 + ], + "28": [ + 38, + 28 + ], + "29": [ + 38, + 29 + ], + "30": [ + 38, + 30 + ], + "31": [ + 38, + 31 + ], + "32": [ + 38, + 32 + ], + "33": [ + 38, + 33 + ], + "34": [ + 38, + 34 + ], + "35": [ + 38, + 35 + ], + "36": [ + 38, + 36 + ], + "37": [ + 38, + 37 + ], + "39": [ + 38, + 39 + ], + "40": [ + 38, + 40 + ], + "41": [ + 38, + 41 + ], + "42": [ + 38, + 42 + ], + "43": [ + 38, + 43 + ], + "44": [ + 38, + 44 + ], + "45": [ + 38, + 45 + ], + "46": [ + 38, + 46 + ], + "47": [ + 38, + 47 + ] + }, + "39": { + "0": [ + 39, + 0 + ], + "1": [ + 39, + 1 + ], + "2": [ + 39, + 2 + ], + "3": [ + 39, + 3 + ], + "4": [ + 39, + 4 + ], + "5": [ + 39, + 5 + ], + "6": [ + 39, + 6 + ], + "7": [ + 39, + 7 + ], + "8": [ + 39, + 8 + ], + "9": [ + 39, + 9 + ], + "10": [ + 39, + 10 + ], + "11": [ + 39, + 11 + ], + "12": [ + 39, + 12 + ], + "13": [ + 39, + 13 + ], + "14": [ + 39, + 14 + ], + "15": [ + 39, + 15 + ], + "16": [ + 39, + 16 + ], + "17": [ + 39, + 17 + ], + "18": [ + 39, + 18 + ], + "19": [ + 39, + 19 + ], + "20": [ + 39, + 20 + ], + "21": [ + 39, + 21 + ], + "22": [ + 39, + 22 + ], + "23": [ + 39, + 23 + ], + "24": [ + 39, + 24 + ], + "25": [ + 39, + 25 + ], + "26": [ + 39, + 26 + ], + "27": [ + 39, + 27 + ], + "28": [ + 39, + 28 + ], + "29": [ + 39, + 29 + ], + "30": [ + 39, + 30 + ], + "31": [ + 39, + 31 + ], + "32": [ + 39, + 32 + ], + "33": [ + 39, + 33 + ], + "34": [ + 39, + 34 + ], + "35": [ + 39, + 35 + ], + "36": [ + 39, + 36 + ], + "37": [ + 39, + 37 + ], + "38": [ + 39, + 38 + ], + "40": [ + 39, + 40 + ], + "41": [ + 39, + 41 + ], + "42": [ + 39, + 42 + ], + "43": [ + 39, + 43 + ], + "44": [ + 39, + 44 + ], + "45": [ + 39, + 45 + ], + "46": [ + 39, + 46 + ], + "47": [ + 39, + 47 + ] + }, + "40": { + "0": [ + 40, + 0 + ], + "1": [ + 40, + 1 + ], + "2": [ + 40, + 2 + ], + "3": [ + 40, + 3 + ], + "4": [ + 40, + 4 + ], + "5": [ + 40, + 5 + ], + "6": [ + 40, + 6 + ], + "7": [ + 40, + 7 + ], + "8": [ + 40, + 8 + ], + "9": [ + 40, + 9 + ], + "10": [ + 40, + 10 + ], + "11": [ + 40, + 11 + ], + "12": [ + 40, + 12 + ], + "13": [ + 40, + 13 + ], + "14": [ + 40, + 14 + ], + "15": [ + 40, + 15 + ], + "16": [ + 40, + 16 + ], + "17": [ + 40, + 17 + ], + "18": [ + 40, + 18 + ], + "19": [ + 40, + 19 + ], + "20": [ + 40, + 20 + ], + "21": [ + 40, + 21 + ], + "22": [ + 40, + 22 + ], + "23": [ + 40, + 23 + ], + "24": [ + 40, + 24 + ], + "25": [ + 40, + 25 + ], + "26": [ + 40, + 26 + ], + "27": [ + 40, + 27 + ], + "28": [ + 40, + 28 + ], + "29": [ + 40, + 29 + ], + "30": [ + 40, + 30 + ], + "31": [ + 40, + 31 + ], + "32": [ + 40, + 32 + ], + "33": [ + 40, + 33 + ], + "34": [ + 40, + 34 + ], + "35": [ + 40, + 35 + ], + "36": [ + 40, + 36 + ], + "37": [ + 40, + 37 + ], + "38": [ + 40, + 38 + ], + "39": [ + 40, + 39 + ], + "41": [ + 40, + 41 + ], + "42": [ + 40, + 42 + ], + "43": [ + 40, + 43 + ], + "44": [ + 40, + 44 + ], + "45": [ + 40, + 45 + ], + "46": [ + 40, + 46 + ], + "47": [ + 40, + 47 + ] + }, + "41": { + "0": [ + 41, + 0 + ], + "1": [ + 41, + 1 + ], + "2": [ + 41, + 2 + ], + "3": [ + 41, + 3 + ], + "4": [ + 41, + 4 + ], + "5": [ + 41, + 5 + ], + "6": [ + 41, + 6 + ], + "7": [ + 41, + 7 + ], + "8": [ + 41, + 8 + ], + "9": [ + 41, + 9 + ], + "10": [ + 41, + 10 + ], + "11": [ + 41, + 11 + ], + "12": [ + 41, + 12 + ], + "13": [ + 41, + 13 + ], + "14": [ + 41, + 14 + ], + "15": [ + 41, + 15 + ], + "16": [ + 41, + 16 + ], + "17": [ + 41, + 17 + ], + "18": [ + 41, + 18 + ], + "19": [ + 41, + 19 + ], + "20": [ + 41, + 20 + ], + "21": [ + 41, + 21 + ], + "22": [ + 41, + 22 + ], + "23": [ + 41, + 23 + ], + "24": [ + 41, + 24 + ], + "25": [ + 41, + 25 + ], + "26": [ + 41, + 26 + ], + "27": [ + 41, + 27 + ], + "28": [ + 41, + 28 + ], + "29": [ + 41, + 29 + ], + "30": [ + 41, + 30 + ], + "31": [ + 41, + 31 + ], + "32": [ + 41, + 32 + ], + "33": [ + 41, + 33 + ], + "34": [ + 41, + 34 + ], + "35": [ + 41, + 35 + ], + "36": [ + 41, + 36 + ], + "37": [ + 41, + 37 + ], + "38": [ + 41, + 38 + ], + "39": [ + 41, + 39 + ], + "40": [ + 41, + 40 + ], + "42": [ + 41, + 42 + ], + "43": [ + 41, + 43 + ], + "44": [ + 41, + 44 + ], + "45": [ + 41, + 45 + ], + "46": [ + 41, + 46 + ], + "47": [ + 41, + 47 + ] + }, + "42": { + "0": [ + 42, + 0 + ], + "1": [ + 42, + 1 + ], + "2": [ + 42, + 2 + ], + "3": [ + 42, + 3 + ], + "4": [ + 42, + 4 + ], + "5": [ + 42, + 5 + ], + "6": [ + 42, + 6 + ], + "7": [ + 42, + 7 + ], + "8": [ + 42, + 8 + ], + "9": [ + 42, + 9 + ], + "10": [ + 42, + 10 + ], + "11": [ + 42, + 11 + ], + "12": [ + 42, + 12 + ], + "13": [ + 42, + 13 + ], + "14": [ + 42, + 14 + ], + "15": [ + 42, + 15 + ], + "16": [ + 42, + 16 + ], + "17": [ + 42, + 17 + ], + "18": [ + 42, + 18 + ], + "19": [ + 42, + 19 + ], + "20": [ + 42, + 20 + ], + "21": [ + 42, + 21 + ], + "22": [ + 42, + 22 + ], + "23": [ + 42, + 23 + ], + "24": [ + 42, + 24 + ], + "25": [ + 42, + 25 + ], + "26": [ + 42, + 26 + ], + "27": [ + 42, + 27 + ], + "28": [ + 42, + 28 + ], + "29": [ + 42, + 29 + ], + "30": [ + 42, + 30 + ], + "31": [ + 42, + 31 + ], + "32": [ + 42, + 32 + ], + "33": [ + 42, + 33 + ], + "34": [ + 42, + 34 + ], + "35": [ + 42, + 35 + ], + "36": [ + 42, + 36 + ], + "37": [ + 42, + 37 + ], + "38": [ + 42, + 38 + ], + "39": [ + 42, + 39 + ], + "40": [ + 42, + 40 + ], + "41": [ + 42, + 41 + ], + "43": [ + 42, + 43 + ], + "44": [ + 42, + 44 + ], + "45": [ + 42, + 45 + ], + "46": [ + 42, + 46 + ], + "47": [ + 42, + 47 + ] + }, + "43": { + "0": [ + 43, + 0 + ], + "1": [ + 43, + 1 + ], + "2": [ + 43, + 2 + ], + "3": [ + 43, + 3 + ], + "4": [ + 43, + 4 + ], + "5": [ + 43, + 5 + ], + "6": [ + 43, + 6 + ], + "7": [ + 43, + 7 + ], + "8": [ + 43, + 8 + ], + "9": [ + 43, + 9 + ], + "10": [ + 43, + 10 + ], + "11": [ + 43, + 11 + ], + "12": [ + 43, + 12 + ], + "13": [ + 43, + 13 + ], + "14": [ + 43, + 14 + ], + "15": [ + 43, + 15 + ], + "16": [ + 43, + 16 + ], + "17": [ + 43, + 17 + ], + "18": [ + 43, + 18 + ], + "19": [ + 43, + 19 + ], + "20": [ + 43, + 20 + ], + "21": [ + 43, + 21 + ], + "22": [ + 43, + 22 + ], + "23": [ + 43, + 23 + ], + "24": [ + 43, + 24 + ], + "25": [ + 43, + 25 + ], + "26": [ + 43, + 26 + ], + "27": [ + 43, + 27 + ], + "28": [ + 43, + 28 + ], + "29": [ + 43, + 29 + ], + "30": [ + 43, + 30 + ], + "31": [ + 43, + 31 + ], + "32": [ + 43, + 32 + ], + "33": [ + 43, + 33 + ], + "34": [ + 43, + 34 + ], + "35": [ + 43, + 35 + ], + "36": [ + 43, + 36 + ], + "37": [ + 43, + 37 + ], + "38": [ + 43, + 38 + ], + "39": [ + 43, + 39 + ], + "40": [ + 43, + 40 + ], + "41": [ + 43, + 41 + ], + "42": [ + 43, + 42 + ], + "44": [ + 43, + 44 + ], + "45": [ + 43, + 45 + ], + "46": [ + 43, + 46 + ], + "47": [ + 43, + 47 + ] + }, + "44": { + "0": [ + 44, + 0 + ], + "1": [ + 44, + 1 + ], + "2": [ + 44, + 2 + ], + "3": [ + 44, + 3 + ], + "4": [ + 44, + 4 + ], + "5": [ + 44, + 5 + ], + "6": [ + 44, + 6 + ], + "7": [ + 44, + 7 + ], + "8": [ + 44, + 8 + ], + "9": [ + 44, + 9 + ], + "10": [ + 44, + 10 + ], + "11": [ + 44, + 11 + ], + "12": [ + 44, + 12 + ], + "13": [ + 44, + 13 + ], + "14": [ + 44, + 14 + ], + "15": [ + 44, + 15 + ], + "16": [ + 44, + 16 + ], + "17": [ + 44, + 17 + ], + "18": [ + 44, + 18 + ], + "19": [ + 44, + 19 + ], + "20": [ + 44, + 20 + ], + "21": [ + 44, + 21 + ], + "22": [ + 44, + 22 + ], + "23": [ + 44, + 23 + ], + "24": [ + 44, + 24 + ], + "25": [ + 44, + 25 + ], + "26": [ + 44, + 26 + ], + "27": [ + 44, + 27 + ], + "28": [ + 44, + 28 + ], + "29": [ + 44, + 29 + ], + "30": [ + 44, + 30 + ], + "31": [ + 44, + 31 + ], + "32": [ + 44, + 32 + ], + "33": [ + 44, + 33 + ], + "34": [ + 44, + 34 + ], + "35": [ + 44, + 35 + ], + "36": [ + 44, + 36 + ], + "37": [ + 44, + 37 + ], + "38": [ + 44, + 38 + ], + "39": [ + 44, + 39 + ], + "40": [ + 44, + 40 + ], + "41": [ + 44, + 41 + ], + "42": [ + 44, + 42 + ], + "43": [ + 44, + 43 + ], + "45": [ + 44, + 45 + ], + "46": [ + 44, + 46 + ], + "47": [ + 44, + 47 + ] + }, + "45": { + "0": [ + 45, + 0 + ], + "1": [ + 45, + 1 + ], + "2": [ + 45, + 2 + ], + "3": [ + 45, + 3 + ], + "4": [ + 45, + 4 + ], + "5": [ + 45, + 5 + ], + "6": [ + 45, + 6 + ], + "7": [ + 45, + 7 + ], + "8": [ + 45, + 8 + ], + "9": [ + 45, + 9 + ], + "10": [ + 45, + 10 + ], + "11": [ + 45, + 11 + ], + "12": [ + 45, + 12 + ], + "13": [ + 45, + 13 + ], + "14": [ + 45, + 14 + ], + "15": [ + 45, + 15 + ], + "16": [ + 45, + 16 + ], + "17": [ + 45, + 17 + ], + "18": [ + 45, + 18 + ], + "19": [ + 45, + 19 + ], + "20": [ + 45, + 20 + ], + "21": [ + 45, + 21 + ], + "22": [ + 45, + 22 + ], + "23": [ + 45, + 23 + ], + "24": [ + 45, + 24 + ], + "25": [ + 45, + 25 + ], + "26": [ + 45, + 26 + ], + "27": [ + 45, + 27 + ], + "28": [ + 45, + 28 + ], + "29": [ + 45, + 29 + ], + "30": [ + 45, + 30 + ], + "31": [ + 45, + 31 + ], + "32": [ + 45, + 32 + ], + "33": [ + 45, + 33 + ], + "34": [ + 45, + 34 + ], + "35": [ + 45, + 35 + ], + "36": [ + 45, + 36 + ], + "37": [ + 45, + 37 + ], + "38": [ + 45, + 38 + ], + "39": [ + 45, + 39 + ], + "40": [ + 45, + 40 + ], + "41": [ + 45, + 41 + ], + "42": [ + 45, + 42 + ], + "43": [ + 45, + 43 + ], + "44": [ + 45, + 44 + ], + "46": [ + 45, + 46 + ], + "47": [ + 45, + 47 + ] + }, + "46": { + "0": [ + 46, + 0 + ], + "1": [ + 46, + 1 + ], + "2": [ + 46, + 2 + ], + "3": [ + 46, + 3 + ], + "4": [ + 46, + 4 + ], + "5": [ + 46, + 5 + ], + "6": [ + 46, + 6 + ], + "7": [ + 46, + 7 + ], + "8": [ + 46, + 8 + ], + "9": [ + 46, + 9 + ], + "10": [ + 46, + 10 + ], + "11": [ + 46, + 11 + ], + "12": [ + 46, + 12 + ], + "13": [ + 46, + 13 + ], + "14": [ + 46, + 14 + ], + "15": [ + 46, + 15 + ], + "16": [ + 46, + 16 + ], + "17": [ + 46, + 17 + ], + "18": [ + 46, + 18 + ], + "19": [ + 46, + 19 + ], + "20": [ + 46, + 20 + ], + "21": [ + 46, + 21 + ], + "22": [ + 46, + 22 + ], + "23": [ + 46, + 23 + ], + "24": [ + 46, + 24 + ], + "25": [ + 46, + 25 + ], + "26": [ + 46, + 26 + ], + "27": [ + 46, + 27 + ], + "28": [ + 46, + 28 + ], + "29": [ + 46, + 29 + ], + "30": [ + 46, + 30 + ], + "31": [ + 46, + 31 + ], + "32": [ + 46, + 32 + ], + "33": [ + 46, + 33 + ], + "34": [ + 46, + 34 + ], + "35": [ + 46, + 35 + ], + "36": [ + 46, + 36 + ], + "37": [ + 46, + 37 + ], + "38": [ + 46, + 38 + ], + "39": [ + 46, + 39 + ], + "40": [ + 46, + 40 + ], + "41": [ + 46, + 41 + ], + "42": [ + 46, + 42 + ], + "43": [ + 46, + 43 + ], + "44": [ + 46, + 44 + ], + "45": [ + 46, + 45 + ], + "47": [ + 46, + 47 + ] + }, + "47": { + "0": [ + 47, + 0 + ], + "1": [ + 47, + 1 + ], + "2": [ + 47, + 2 + ], + "3": [ + 47, + 3 + ], + "4": [ + 47, + 4 + ], + "5": [ + 47, + 5 + ], + "6": [ + 47, + 6 + ], + "7": [ + 47, + 7 + ], + "8": [ + 47, + 8 + ], + "9": [ + 47, + 9 + ], + "10": [ + 47, + 10 + ], + "11": [ + 47, + 11 + ], + "12": [ + 47, + 12 + ], + "13": [ + 47, + 13 + ], + "14": [ + 47, + 14 + ], + "15": [ + 47, + 15 + ], + "16": [ + 47, + 16 + ], + "17": [ + 47, + 17 + ], + "18": [ + 47, + 18 + ], + "19": [ + 47, + 19 + ], + "20": [ + 47, + 20 + ], + "21": [ + 47, + 21 + ], + "22": [ + 47, + 22 + ], + "23": [ + 47, + 23 + ], + "24": [ + 47, + 24 + ], + "25": [ + 47, + 25 + ], + "26": [ + 47, + 26 + ], + "27": [ + 47, + 27 + ], + "28": [ + 47, + 28 + ], + "29": [ + 47, + 29 + ], + "30": [ + 47, + 30 + ], + "31": [ + 47, + 31 + ], + "32": [ + 47, + 32 + ], + "33": [ + 47, + 33 + ], + "34": [ + 47, + 34 + ], + "35": [ + 47, + 35 + ], + "36": [ + 47, + 36 + ], + "37": [ + 47, + 37 + ], + "38": [ + 47, + 38 + ], + "39": [ + 47, + 39 + ], + "40": [ + 47, + 40 + ], + "41": [ + 47, + 41 + ], + "42": [ + 47, + 42 + ], + "43": [ + 47, + 43 + ], + "44": [ + 47, + 44 + ], + "45": [ + 47, + 45 + ], + "46": [ + 47, + 46 + ] + } +} \ No newline at end of file diff --git a/algorithm/Weights.json b/algorithm/Weights.json new file mode 100644 index 0000000..a40af6a --- /dev/null +++ b/algorithm/Weights.json @@ -0,0 +1,2402 @@ +[ + [ + 0, + 4727, + 1205, + 6363, + 3657, + 3130, + 2414, + 563, + 463, + 5654, + 1713, + 1604, + 2368, + 2201, + 1290, + 1004, + 3833, + 2258, + 3419, + 2267, + 2957, + 720, + 1700, + 5279, + 2578, + 6076, + 3465, + 2654, + 3625, + 3115, + 1574, + 3951, + 1748, + 2142, + 6755, + 2383, + 3306, + 1029, + 3530, + 825, + 2188, + 4820, + 3489, + 1947, + 6835, + 1542, + 2379, + 3744 + ], + [ + 4727, + 0, + 3588, + 2012, + 1842, + 6977, + 6501, + 5187, + 5028, + 2327, + 4148, + 4723, + 3635, + 3125, + 4907, + 3930, + 7463, + 6338, + 7243, + 5105, + 4043, + 4022, + 3677, + 2863, + 3106, + 1850, + 7173, + 6630, + 1204, + 6814, + 6001, + 3447, + 5253, + 2656, + 3123, + 6274, + 7183, + 5622, + 3085, + 4564, + 2756, + 1591, + 7027, + 6186, + 3472, + 5461, + 4390, + 2088 + ], + [ + 1205, + 3588, + 0, + 5163, + 2458, + 3678, + 3071, + 1742, + 1444, + 4462, + 1184, + 1520, + 1498, + 1103, + 1501, + 951, + 4298, + 2903, + 3967, + 2169, + 2209, + 652, + 828, + 4136, + 1518, + 4873, + 3954, + 3254, + 2446, + 3581, + 2441, + 2960, + 1966, + 950, + 5564, + 2916, + 3878, + 2035, + 2482, + 1027, + 1395, + 3617, + 3891, + 2686, + 5661, + 2023, + 1867, + 2560 + ], + [ + 6363, + 2012, + 5163, + 0, + 2799, + 8064, + 7727, + 6878, + 6581, + 1402, + 5366, + 5946, + 4679, + 4378, + 6225, + 5709, + 8417, + 7578, + 8296, + 6135, + 4802, + 5707, + 4982, + 2322, + 4178, + 320, + 8186, + 7800, + 2778, + 7859, + 7408, + 3763, + 6461, + 4223, + 1427, + 7451, + 8263, + 7131, + 3669, + 6011, + 4638, + 1681, + 7987, + 7502, + 1877, + 6758, + 5360, + 2844 + ], + [ + 3657, + 1842, + 2458, + 2799, + 0, + 5330, + 4946, + 4200, + 3824, + 2012, + 2573, + 3157, + 1924, + 1580, + 3427, + 3179, + 5749, + 4793, + 5577, + 3409, + 2223, + 3066, + 2185, + 1860, + 1401, + 2491, + 5486, + 5035, + 894, + 5141, + 4611, + 1669, + 3677, + 1590, + 3113, + 4682, + 5533, + 4352, + 1252, + 3227, + 2426, + 1169, + 5313, + 4706, + 3241, + 3962, + 2651, + 304 + ], + [ + 3130, + 6977, + 3678, + 8064, + 5330, + 0, + 743, + 3209, + 2670, + 6929, + 2831, + 2266, + 3407, + 3854, + 2178, + 4076, + 727, + 881, + 293, + 1930, + 3310, + 3672, + 3315, + 6199, + 3932, + 7745, + 365, + 482, + 5774, + 261, + 1659, + 4513, + 1746, + 4431, + 7910, + 769, + 207, + 2225, + 4435, + 2681, + 5053, + 6384, + 550, + 1224, + 7805, + 1670, + 2704, + 5230 + ], + [ + 2414, + 6501, + 3071, + 7727, + 4946, + 743, + 0, + 2468, + 1952, + 6673, + 2380, + 1795, + 3051, + 3405, + 1604, + 3382, + 1469, + 168, + 1020, + 1681, + 3110, + 2993, + 2827, + 6009, + 3552, + 7412, + 1104, + 267, + 5300, + 821, + 916, + 4348, + 1270, + 3890, + 7698, + 332, + 900, + 1484, + 4185, + 2049, + 4415, + 6051, + 1219, + 482, + 7635, + 1054, + 2432, + 4884 + ], + [ + 563, + 5187, + 1742, + 6878, + 4200, + 3209, + 2468, + 0, + 718, + 6203, + 2241, + 2051, + 2920, + 2762, + 1687, + 1304, + 3932, + 2331, + 3487, + 2669, + 3487, + 1175, + 2260, + 5840, + 3141, + 6596, + 3563, + 2728, + 4120, + 3240, + 1559, + 4507, + 2082, + 2658, + 7304, + 2512, + 3364, + 985, + 4091, + 1319, + 2544, + 5358, + 3632, + 1987, + 7391, + 1785, + 2879, + 4296 + ], + [ + 463, + 5028, + 1444, + 6581, + 3824, + 2670, + 1952, + 718, + 0, + 5789, + 1602, + 1343, + 2330, + 2291, + 970, + 1451, + 3376, + 1796, + 2959, + 1951, + 2835, + 1112, + 1725, + 5346, + 2628, + 6285, + 3007, + 2193, + 3889, + 2661, + 1122, + 3920, + 1372, + 2391, + 6883, + 1927, + 2845, + 611, + 3543, + 676, + 2590, + 4993, + 3039, + 1486, + 6934, + 1112, + 2196, + 3876 + ], + [ + 5654, + 2327, + 4462, + 1402, + 2012, + 6929, + 6673, + 6203, + 5789, + 0, + 4392, + 4947, + 3648, + 3501, + 5274, + 5183, + 7216, + 6535, + 7140, + 5022, + 3621, + 5077, + 4090, + 922, + 3207, + 1131, + 7014, + 6714, + 2437, + 6707, + 6477, + 2476, + 5432, + 3599, + 1102, + 6376, + 7121, + 6284, + 2497, + 5160, + 4318, + 937, + 6795, + 6507, + 1268, + 5773, + 4249, + 1914 + ], + [ + 1713, + 4148, + 1184, + 5366, + 2573, + 2831, + 2380, + 2241, + 1602, + 4392, + 0, + 586, + 766, + 1029, + 883, + 2040, + 3353, + 2224, + 3100, + 1049, + 1246, + 1625, + 503, + 3841, + 1196, + 5054, + 3042, + 2488, + 2945, + 2676, + 2087, + 2331, + 1114, + 1650, + 5459, + 2132, + 3037, + 1958, + 1997, + 931, + 2513, + 3701, + 2923, + 2137, + 5459, + 1394, + 711, + 2534 + ], + [ + 1604, + 4723, + 1520, + 5946, + 3157, + 2266, + 1795, + 2051, + 1343, + 4947, + 586, + 0, + 1299, + 1612, + 406, + 2208, + 2824, + 1639, + 2542, + 694, + 1586, + 1767, + 1050, + 4357, + 1770, + 5633, + 2498, + 1907, + 3520, + 2128, + 1558, + 2778, + 531, + 2171, + 6003, + 1552, + 2472, + 1538, + 2506, + 791, + 2912, + 4277, + 2403, + 1564, + 5983, + 827, + 892, + 3109 + ], + [ + 2368, + 3635, + 1498, + 4679, + 1924, + 3407, + 3051, + 2920, + 2330, + 3648, + 766, + 1299, + 0, + 646, + 1642, + 2446, + 3840, + 2905, + 3655, + 1488, + 730, + 2096, + 697, + 3076, + 533, + 4363, + 3567, + 3122, + 2453, + 3219, + 2842, + 1592, + 1791, + 1480, + 4706, + 2772, + 3610, + 2721, + 1232, + 1656, + 2550, + 3001, + 3403, + 2860, + 4697, + 2126, + 756, + 1836 + ], + [ + 2201, + 3125, + 1103, + 4378, + 1580, + 3854, + 3405, + 2762, + 2291, + 3501, + 1029, + 1612, + 646, + 0, + 1853, + 2026, + 4349, + 3247, + 4119, + 1997, + 1341, + 1753, + 606, + 3078, + 419, + 4070, + 4052, + 3517, + 1923, + 3690, + 3032, + 1866, + 2142, + 838, + 4593, + 3161, + 4060, + 2788, + 1380, + 1663, + 1932, + 2736, + 3915, + 3138, + 4647, + 2395, + 1351, + 1592 + ], + [ + 1290, + 4907, + 1501, + 6225, + 3427, + 2178, + 1604, + 1687, + 970, + 5274, + 883, + 406, + 1642, + 1853, + 0, + 2029, + 2803, + 1438, + 2466, + 986, + 1987, + 1593, + 1253, + 4716, + 2072, + 5915, + 2454, + 1764, + 3710, + 2082, + 1204, + 3164, + 497, + 2287, + 6342, + 1419, + 2379, + 1134, + 2867, + 554, + 2885, + 4569, + 2405, + 1289, + 6338, + 555, + 1297, + 3406 + ], + [ + 1004, + 3930, + 951, + 5709, + 3179, + 4076, + 3382, + 1304, + 1451, + 5183, + 2040, + 2208, + 2446, + 2026, + 2029, + 0, + 4759, + 3220, + 4368, + 2900, + 3151, + 442, + 1765, + 4960, + 2444, + 5443, + 4396, + 3610, + 2932, + 4034, + 2572, + 3891, + 2525, + 1590, + 6278, + 3313, + 4261, + 2033, + 3398, + 1476, + 1241, + 4287, + 4390, + 2928, + 6419, + 2428, + 2749, + 3337 + ], + [ + 3833, + 7463, + 4298, + 8417, + 5749, + 727, + 1469, + 3932, + 3376, + 7216, + 3353, + 2824, + 3840, + 4349, + 2803, + 4759, + 0, + 1601, + 477, + 2359, + 3617, + 4345, + 3851, + 6433, + 4372, + 8098, + 370, + 1206, + 6267, + 726, + 2384, + 4754, + 2335, + 4991, + 8148, + 1452, + 609, + 2949, + 4752, + 3331, + 5687, + 6746, + 437, + 1948, + 8005, + 2334, + 3098, + 5618 + ], + [ + 2258, + 6338, + 2903, + 7578, + 4793, + 881, + 168, + 2331, + 1796, + 6535, + 2224, + 1639, + 2905, + 3247, + 1438, + 3220, + 1601, + 0, + 1165, + 1563, + 2988, + 2829, + 2666, + 5882, + 3401, + 7263, + 1233, + 399, + 5138, + 923, + 794, + 4227, + 1117, + 3724, + 7565, + 286, + 1049, + 1348, + 4051, + 1881, + 4248, + 5903, + 1322, + 355, + 7508, + 887, + 2302, + 4736 + ], + [ + 3419, + 7243, + 3967, + 8296, + 5577, + 293, + 1020, + 3487, + 2959, + 7140, + 3100, + 2542, + 3655, + 4119, + 2466, + 4368, + 477, + 1165, + 0, + 2170, + 3520, + 3965, + 3588, + 6393, + 4183, + 7977, + 202, + 767, + 6041, + 438, + 1932, + 4706, + 2027, + 4711, + 8107, + 1061, + 132, + 2503, + 4652, + 2972, + 5344, + 6617, + 486, + 1501, + 7989, + 1962, + 2939, + 5469 + ], + [ + 2267, + 5105, + 2169, + 6135, + 3409, + 1930, + 1681, + 2669, + 1951, + 5022, + 1049, + 694, + 1488, + 1997, + 986, + 2900, + 2359, + 1563, + 2170, + 0, + 1430, + 2460, + 1547, + 4333, + 2019, + 5817, + 2079, + 1694, + 3910, + 1733, + 1813, + 2668, + 654, + 2694, + 6029, + 1366, + 2130, + 1991, + 2525, + 1474, + 3542, + 4455, + 1923, + 1641, + 5957, + 1071, + 777, + 3302 + ], + [ + 2957, + 4043, + 2209, + 4802, + 2223, + 3310, + 3110, + 3487, + 2835, + 3621, + 1246, + 1586, + 730, + 1341, + 1987, + 3151, + 3617, + 2988, + 3520, + 1430, + 0, + 2779, + 1387, + 2905, + 1062, + 4482, + 3398, + 3119, + 2922, + 3087, + 3115, + 1240, + 1953, + 2175, + 4607, + 2796, + 3501, + 3119, + 1136, + 2173, + 3268, + 3136, + 3189, + 3029, + 4527, + 2355, + 711, + 2042 + ], + [ + 720, + 4022, + 652, + 5707, + 3066, + 3672, + 2993, + 1175, + 1112, + 5077, + 1625, + 1767, + 2096, + 1753, + 1593, + 442, + 4345, + 2829, + 3965, + 2460, + 2779, + 0, + 1401, + 4781, + 2166, + 5427, + 3984, + 3212, + 2946, + 3620, + 2224, + 3603, + 2089, + 1496, + 6178, + 2906, + 3861, + 1719, + 3132, + 1040, + 1479, + 4211, + 3969, + 2553, + 6290, + 2012, + 2336, + 3189 + ], + [ + 1700, + 3677, + 828, + 4982, + 2185, + 3315, + 2827, + 2260, + 1725, + 4090, + 503, + 1050, + 697, + 606, + 1253, + 1765, + 3851, + 2666, + 3588, + 1547, + 1387, + 1401, + 0, + 3621, + 903, + 4675, + 3537, + 2954, + 2475, + 3169, + 2427, + 2254, + 1578, + 1148, + 5177, + 2598, + 3521, + 2194, + 1833, + 1074, + 2054, + 3340, + 3423, + 2541, + 5213, + 1801, + 1077, + 2190 + ], + [ + 5279, + 2863, + 4136, + 2322, + 1860, + 6199, + 6009, + 5840, + 5346, + 922, + 3841, + 4357, + 3076, + 3078, + 4716, + 4960, + 6433, + 5882, + 6393, + 4333, + 2905, + 4781, + 3621, + 0, + 2718, + 2042, + 6254, + 6024, + 2569, + 5966, + 5913, + 1687, + 4807, + 3384, + 1716, + 5699, + 6384, + 5787, + 1852, + 4687, + 4285, + 1272, + 6022, + 5892, + 1629, + 5178, + 3581, + 1639 + ], + [ + 2578, + 3106, + 1518, + 4178, + 1401, + 3932, + 3552, + 3141, + 2628, + 3207, + 1196, + 1770, + 533, + 419, + 2072, + 2444, + 4372, + 3401, + 4183, + 2019, + 1062, + 2166, + 903, + 2718, + 0, + 3864, + 4097, + 3635, + 1932, + 3748, + 3274, + 1448, + 2284, + 1164, + 4286, + 3283, + 4136, + 3086, + 967, + 1973, + 2285, + 2507, + 3935, + 3331, + 4312, + 2589, + 1284, + 1340 + ], + [ + 6076, + 1850, + 4873, + 320, + 2491, + 7745, + 7412, + 6596, + 6285, + 1131, + 5054, + 5633, + 4363, + 4070, + 5915, + 5443, + 8098, + 7263, + 7977, + 5817, + 4482, + 5427, + 4675, + 2042, + 3864, + 0, + 7866, + 7483, + 2515, + 7539, + 7101, + 3449, + 6146, + 3938, + 1375, + 7134, + 7944, + 6831, + 3349, + 5709, + 4397, + 1363, + 7667, + 7190, + 1798, + 6446, + 5041, + 2528 + ], + [ + 3465, + 7173, + 3954, + 8186, + 5486, + 365, + 1104, + 3563, + 3007, + 7014, + 3042, + 2498, + 3567, + 4052, + 2454, + 4396, + 370, + 1233, + 202, + 2079, + 3398, + 3984, + 3537, + 6254, + 4097, + 7866, + 0, + 839, + 5973, + 374, + 2019, + 4569, + 1996, + 4669, + 7970, + 1085, + 305, + 2581, + 4532, + 2976, + 5339, + 6509, + 287, + 1581, + 7844, + 1974, + 2838, + 5369 + ], + [ + 2654, + 6630, + 3254, + 7800, + 5035, + 482, + 267, + 2728, + 2193, + 6714, + 2488, + 1907, + 3122, + 3517, + 1764, + 3610, + 1206, + 399, + 767, + 1694, + 3119, + 3212, + 2954, + 6024, + 3635, + 7483, + 839, + 0, + 5427, + 558, + 1181, + 4349, + 1377, + 4044, + 7723, + 356, + 653, + 1744, + 4218, + 2241, + 4614, + 6121, + 955, + 743, + 7644, + 1231, + 2465, + 4957 + ], + [ + 3625, + 1204, + 2446, + 2778, + 894, + 5774, + 5300, + 4120, + 3889, + 2437, + 2945, + 3520, + 2453, + 1923, + 3710, + 2932, + 6267, + 5138, + 6041, + 3910, + 2922, + 2946, + 2475, + 2569, + 1932, + 2515, + 5973, + 5427, + 0, + 5612, + 4824, + 2550, + 4050, + 1498, + 3476, + 5071, + 5980, + 4470, + 2096, + 3388, + 1911, + 1501, + 5831, + 4994, + 3704, + 4264, + 3209, + 1196 + ], + [ + 3115, + 6814, + 3581, + 7859, + 5141, + 261, + 821, + 3240, + 2661, + 6707, + 2676, + 2128, + 3219, + 3690, + 2082, + 4034, + 726, + 923, + 438, + 1733, + 3087, + 3620, + 3169, + 5966, + 3748, + 7539, + 374, + 558, + 5612, + 0, + 1716, + 4280, + 1624, + 4298, + 7679, + 735, + 420, + 2263, + 4216, + 2606, + 4967, + 6179, + 400, + 1277, + 7567, + 1609, + 2501, + 5032 + ], + [ + 1574, + 6001, + 2441, + 7408, + 4611, + 1659, + 916, + 1559, + 1122, + 6477, + 2087, + 1558, + 2842, + 3032, + 1204, + 2572, + 2384, + 794, + 1932, + 1813, + 3115, + 2224, + 2427, + 5913, + 3274, + 7101, + 2019, + 1181, + 4824, + 1716, + 0, + 4330, + 1180, + 3346, + 7545, + 1023, + 1808, + 578, + 4062, + 1438, + 3693, + 5763, + 2115, + 440, + 7537, + 763, + 2404, + 4603 + ], + [ + 3951, + 3447, + 2960, + 3763, + 1669, + 4513, + 4348, + 4507, + 3920, + 2476, + 2331, + 2778, + 1592, + 1866, + 3164, + 3891, + 4754, + 4227, + 4706, + 2668, + 1240, + 3603, + 2254, + 1687, + 1448, + 3449, + 4569, + 4349, + 2550, + 4280, + 4330, + 0, + 3184, + 2510, + 3402, + 4031, + 4698, + 4281, + 533, + 3245, + 3612, + 2187, + 4339, + 4265, + 3296, + 3576, + 1941, + 1381 + ], + [ + 1748, + 5253, + 1966, + 6461, + 3677, + 1746, + 1270, + 2082, + 1372, + 5432, + 1114, + 531, + 1791, + 2142, + 497, + 2525, + 2335, + 1117, + 2027, + 654, + 1953, + 2089, + 1578, + 4807, + 2284, + 6146, + 1996, + 1377, + 4050, + 1624, + 1180, + 3184, + 0, + 2685, + 6475, + 1022, + 1952, + 1341, + 2963, + 1050, + 3358, + 4787, + 1926, + 1086, + 6436, + 422, + 1244, + 3619 + ], + [ + 2142, + 2656, + 950, + 4223, + 1590, + 4431, + 3890, + 2658, + 2391, + 3599, + 1650, + 2171, + 1480, + 838, + 2287, + 1590, + 4991, + 3724, + 4711, + 2694, + 2175, + 1496, + 1148, + 3384, + 1164, + 3938, + 4669, + 4044, + 1498, + 4298, + 3346, + 2510, + 2685, + 0, + 4697, + 3693, + 4636, + 2975, + 1981, + 1909, + 1124, + 2718, + 4565, + 3548, + 4830, + 2839, + 2140, + 1751 + ], + [ + 6755, + 3123, + 5564, + 1427, + 3113, + 7910, + 7698, + 7304, + 6883, + 1102, + 5459, + 6003, + 4706, + 4593, + 6342, + 6278, + 8148, + 7565, + 8107, + 6029, + 4607, + 6178, + 5177, + 1716, + 4286, + 1375, + 7970, + 7723, + 3476, + 7679, + 7545, + 3402, + 6475, + 4697, + 0, + 7393, + 8097, + 7370, + 3515, + 6249, + 5379, + 2001, + 7738, + 7556, + 461, + 6829, + 5267, + 3013 + ], + [ + 2383, + 6274, + 2916, + 7451, + 4682, + 769, + 332, + 2512, + 1927, + 6376, + 2132, + 1552, + 2772, + 3161, + 1419, + 3313, + 1452, + 286, + 1061, + 1366, + 2796, + 2906, + 2598, + 5699, + 3283, + 7134, + 1085, + 356, + 5071, + 735, + 1023, + 4031, + 1022, + 3693, + 7393, + 0, + 965, + 1542, + 3883, + 1913, + 4286, + 5772, + 1121, + 600, + 7322, + 902, + 2128, + 4608 + ], + [ + 3306, + 7183, + 3878, + 8263, + 5533, + 207, + 900, + 3364, + 2845, + 7121, + 3037, + 2472, + 3610, + 4060, + 2379, + 4261, + 609, + 1049, + 132, + 2130, + 3501, + 3861, + 3521, + 6384, + 4136, + 7944, + 305, + 653, + 5980, + 420, + 1808, + 4698, + 1952, + 4636, + 8097, + 965, + 0, + 2380, + 4629, + 2877, + 5250, + 6583, + 570, + 1380, + 7986, + 1866, + 2904, + 5432 + ], + [ + 1029, + 5622, + 2035, + 7131, + 4352, + 2225, + 1484, + 985, + 611, + 6284, + 1958, + 1538, + 2721, + 2788, + 1134, + 2033, + 2949, + 1348, + 2503, + 1991, + 3119, + 1719, + 2194, + 5787, + 3086, + 6831, + 2581, + 1744, + 4470, + 2263, + 578, + 4281, + 1341, + 2975, + 7370, + 1542, + 2380, + 0, + 3952, + 1127, + 3197, + 5518, + 2658, + 1002, + 7395, + 951, + 2429, + 4380 + ], + [ + 3530, + 3085, + 2482, + 3669, + 1252, + 4435, + 4185, + 4091, + 3543, + 2497, + 1997, + 2506, + 1232, + 1380, + 2867, + 3398, + 4752, + 4051, + 4652, + 2525, + 1136, + 3132, + 1833, + 1852, + 967, + 3349, + 4532, + 4218, + 2096, + 4216, + 4062, + 533, + 2963, + 1981, + 3515, + 3883, + 4629, + 3952, + 0, + 2873, + 3080, + 2012, + 4324, + 4046, + 3478, + 3328, + 1755, + 1000 + ], + [ + 825, + 4564, + 1027, + 6011, + 3227, + 2681, + 2049, + 1319, + 676, + 5160, + 931, + 791, + 1656, + 1663, + 554, + 1476, + 3331, + 1881, + 2972, + 1474, + 2173, + 1040, + 1074, + 4687, + 1973, + 5709, + 2976, + 2241, + 3388, + 2606, + 1438, + 3245, + 1050, + 1909, + 6249, + 1913, + 2877, + 1127, + 2873, + 0, + 2374, + 4392, + 2943, + 1659, + 6285, + 1012, + 1563, + 3254 + ], + [ + 2188, + 2756, + 1395, + 4638, + 2426, + 5053, + 4415, + 2544, + 2590, + 4318, + 2513, + 2912, + 2550, + 1932, + 2885, + 1241, + 5687, + 4248, + 5344, + 3542, + 3268, + 1479, + 2054, + 4285, + 2285, + 4397, + 5339, + 4614, + 1911, + 4967, + 3693, + 3612, + 3358, + 1124, + 5379, + 4286, + 5250, + 3197, + 3080, + 2374, + 0, + 3386, + 5284, + 3997, + 5585, + 3386, + 3125, + 2664 + ], + [ + 4820, + 1591, + 3617, + 1681, + 1169, + 6384, + 6051, + 5358, + 4993, + 937, + 3701, + 4277, + 3001, + 2736, + 4569, + 4287, + 6746, + 5903, + 6617, + 4455, + 3136, + 4211, + 3340, + 1272, + 2507, + 1363, + 6509, + 6121, + 1501, + 6179, + 5763, + 2187, + 4787, + 2718, + 2001, + 5772, + 6583, + 5518, + 2012, + 4392, + 3386, + 0, + 6314, + 5837, + 2205, + 5095, + 3680, + 1169 + ], + [ + 3489, + 7027, + 3891, + 7987, + 5313, + 550, + 1219, + 3632, + 3039, + 6795, + 2923, + 2403, + 3403, + 3915, + 2405, + 4390, + 437, + 1322, + 486, + 1923, + 3189, + 3969, + 3423, + 6022, + 3935, + 7667, + 287, + 955, + 5831, + 400, + 2115, + 4339, + 1926, + 4565, + 7738, + 1121, + 570, + 2658, + 4324, + 2943, + 5284, + 6314, + 0, + 1676, + 7603, + 1964, + 2662, + 5184 + ], + [ + 1947, + 6186, + 2686, + 7502, + 4706, + 1224, + 482, + 1987, + 1486, + 6507, + 2137, + 1564, + 2860, + 3138, + 1289, + 2928, + 1948, + 355, + 1501, + 1641, + 3029, + 2553, + 2541, + 5892, + 3331, + 7190, + 1581, + 743, + 4994, + 1277, + 440, + 4265, + 1086, + 3548, + 7556, + 600, + 1380, + 1002, + 4046, + 1659, + 3997, + 5837, + 1676, + 0, + 7521, + 744, + 2325, + 4670 + ], + [ + 6835, + 3472, + 5661, + 1877, + 3241, + 7805, + 7635, + 7391, + 6934, + 1268, + 5459, + 5983, + 4697, + 4647, + 6338, + 6419, + 8005, + 7508, + 7989, + 5957, + 4527, + 6290, + 5213, + 1629, + 4312, + 1798, + 7844, + 7644, + 3704, + 7567, + 7537, + 3296, + 6436, + 4830, + 461, + 7322, + 7986, + 7395, + 3478, + 6285, + 5585, + 2205, + 7603, + 7521, + 0, + 6805, + 5208, + 3102 + ], + [ + 1542, + 5461, + 2023, + 6758, + 3962, + 1670, + 1054, + 1785, + 1112, + 5773, + 1394, + 827, + 2126, + 2395, + 555, + 2428, + 2334, + 887, + 1962, + 1071, + 2355, + 2012, + 1801, + 5178, + 2589, + 6446, + 1974, + 1231, + 4264, + 1609, + 763, + 3576, + 422, + 2839, + 6829, + 902, + 1866, + 951, + 3328, + 1012, + 3386, + 5095, + 1964, + 744, + 6805, + 0, + 1644, + 3928 + ], + [ + 2379, + 4390, + 1867, + 5360, + 2651, + 2704, + 2432, + 2879, + 2196, + 4249, + 711, + 892, + 756, + 1351, + 1297, + 2749, + 3098, + 2302, + 2939, + 777, + 711, + 2336, + 1077, + 3581, + 1284, + 5041, + 2838, + 2465, + 3209, + 2501, + 2404, + 1941, + 1244, + 2140, + 5267, + 2128, + 2904, + 2429, + 1755, + 1563, + 3125, + 3680, + 2662, + 2325, + 5208, + 1644, + 0, + 2532 + ], + [ + 3744, + 2088, + 2560, + 2844, + 304, + 5230, + 4884, + 4296, + 3876, + 1914, + 2534, + 3109, + 1836, + 1592, + 3406, + 3337, + 5618, + 4736, + 5469, + 3302, + 2042, + 3189, + 2190, + 1639, + 1340, + 2528, + 5369, + 4957, + 1196, + 5032, + 4603, + 1381, + 3619, + 1751, + 3013, + 4608, + 5432, + 4380, + 1000, + 3254, + 2664, + 1169, + 5184, + 4670, + 3102, + 3928, + 2532, + 0 + ] +] \ No newline at end of file diff --git a/algorithm/__pycache__/dataset.cpython-311.pyc b/algorithm/__pycache__/dataset.cpython-311.pyc new file mode 100644 index 0000000..da2f10b Binary files /dev/null and b/algorithm/__pycache__/dataset.cpython-311.pyc differ diff --git a/algorithm/dataset.py b/algorithm/dataset.py new file mode 100644 index 0000000..a3ed5e5 --- /dev/null +++ b/algorithm/dataset.py @@ -0,0 +1,123 @@ +INF=100000 + +# shelfMatrixLabel = ["SH1-L1", "SH1-L2", "SH1-L3", "SH1-L4", "SH1-L5", "SH2-L1", "SH2-L2", "SH2-L3", "SH2-L4", "SH2-L5", "SH3-L1", "SH3-L2", "SH3-L3", "SH3-L4", "SH3-L5"] +# shelfMatrix = [] + + +# dmatrixLabel = ['B7', 'C1', 'C4', 'D0', 'E2', 'E4', 'E9', 'E8', 'F2', 'F4', 'G6', 'H1', 'I0', 'I6', 'I9', 'J0', 'J1', 'J6', 'K8', 'L2', 'L3', 'L7', 'L8', 'M7', 'M9', 'N1', 'N2', 'N3', 'N4', 'N6', 'O1', 'O2', 'O3', 'Q3', 'Q6', 'Q7', 'R3', 'S8', 'T4', 'U0', 'U5', 'U2', 'V1', 'V3', 'V7', 'X6', 'Y5', 'Z4'] + +# dmatrixLabel = ['A0', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2', 'E1', 'E2', 'F1', 'F2', 'G1', 'G2', 'H1', 'H2', 'I1', 'I2', 'J1', 'J2', 'K1', 'K2', 'L1', 'L2', 'M1', 'M2', 'N1', 'N2', 'P1', 'P2', 'Q1', 'Q2', 'R1', 'R2', 'S1', 'S2', 'T1', 'T2', 'U1', 'U2', 'V1', 'V2', 'W1', 'W2', 'X1', 'X2', 'Y1', 'Y2'] + +dmatrixLabel = ['A0', 'A2', 'B1', 'B2', 'C1', 'C2', 'D1', 'D2', 'E1', 'E2', 'F1', 'F2', 'G1', 'G2', 'H1', 'H2', 'I1', 'I2', 'J1', 'J2', 'K1', 'K2', 'L1', 'L2', 'M1', 'M2', 'N1', 'N2', 'P1', 'P2', 'Q1', 'Q2', 'R1', "SH1-L1", "SH1-L2", "SH1-L3", "SH1-L4", "SH1-L5", "SH2-L1", "SH2-L2", "SH2-L3", "SH2-L4", "SH2-L5", "SH3-L1", "SH3-L2", "SH3-L3", "SH3-L4", "SH3-L5"] + + +dmatrix = [ + [ 0, 4727, 1205, 6363, 3657, 3130, 2414, 563, 463, 5654, 1713, 1604, 2368, 2201, 1290, 1004, 3833, 2258, 3419, 2267, 2957, 720, 1700, 5279, 2578, 6076, 3465, 2654, 3625, 3115, 1574, 3951, 1748, 2142, 6755, 2383, 3306, 1029, 3530, 825, 2188, 4820, 3489, 1947, 6835, 1542, 2379, 3744], + [ 4727, 0, 3588, 2012, 1842, 6977, 6501, 5187, 5028, 2327, 4148, 4723, 3635, 3125, 4907, 3930, 7463, 6338, 7243, 5105, 4043, 4022, 3677, 2863, 3106, 1850, 7173, 6630, 1204, 6814, 6001, 3447, 5253, 2656, 3123, 6274, 7183, 5622, 3085, 4564, 2756, 1591, 7027, 6186, 3472, 5461, 4390, 2088], + [ 1205, 3588, 0, 5163, 2458, 3678, 3071, 1742, 1444, 4462, 1184, 1520, 1498, 1103, 1501, 951, 4298, 2903, 3967, 2169, 2209, 652, 828, 4136, 1518, 4873, 3954, 3254, 2446, 3581, 2441, 2960, 1966, 950, 5564, 2916, 3878, 2035, 2482, 1027, 1395, 3617, 3891, 2686, 5661, 2023, 1867, 2560], + [ 6363, 2012, 5163, 0, 2799, 8064, 7727, 6878, 6581, 1402, 5366, 5946, 4679, 4378, 6225, 5709, 8417, 7578, 8296, 6135, 4802, 5707, 4982, 2322, 4178, 320, 8186, 7800, 2778, 7859, 7408, 3763, 6461, 4223, 1427, 7451, 8263, 7131, 3669, 6011, 4638, 1681, 7987, 7502, 1877, 6758, 5360, 2844], + [ 3657, 1842, 2458, 2799, 0, 5330, 4946, 4200, 3824, 2012, 2573, 3157, 1924, 1580, 3427, 3179, 5749, 4793, 5577, 3409, 2223, 3066, 2185, 1860, 1401, 2491, 5486, 5035, 894, 5141, 4611, 1669, 3677, 1590, 3113, 4682, 5533, 4352, 1252, 3227, 2426, 1169, 5313, 4706, 3241, 3962, 2651, 304], + [ 3130, 6977, 3678, 8064, 5330, 0, 743, 3209, 2670, 6929, 2831, 2266, 3407, 3854, 2178, 4076, 727, 881, 293, 1930, 3310, 3672, 3315, 6199, 3932, 7745, 365, 482, 5774, 261, 1659, 4513, 1746, 4431, 7910, 769, 207, 2225, 4435, 2681, 5053, 6384, 550, 1224, 7805, 1670, 2704, 5230], + [ 2414, 6501, 3071, 7727, 4946, 743, 0, 2468, 1952, 6673, 2380, 1795, 3051, 3405, 1604, 3382, 1469, 168, 1020, 1681, 3110, 2993, 2827, 6009, 3552, 7412, 1104, 267, 5300, 821, 916, 4348, 1270, 3890, 7698, 332, 900, 1484, 4185, 2049, 4415, 6051, 1219, 482, 7635, 1054, 2432, 4884], + [ 563, 5187, 1742, 6878, 4200, 3209, 2468, 0, 718, 6203, 2241, 2051, 2920, 2762, 1687, 1304, 3932, 2331, 3487, 2669, 3487, 1175, 2260, 5840, 3141, 6596, 3563, 2728, 4120, 3240, 1559, 4507, 2082, 2658, 7304, 2512, 3364, 985, 4091, 1319, 2544, 5358, 3632, 1987, 7391, 1785, 2879, 4296], + [ 463, 5028, 1444, 6581, 3824, 2670, 1952, 718, 0, 5789, 1602, 1343, 2330, 2291, 970, 1451, 3376, 1796, 2959, 1951, 2835, 1112, 1725, 5346, 2628, 6285, 3007, 2193, 3889, 2661, 1122, 3920, 1372, 2391, 6883, 1927, 2845, 611, 3543, 676, 2590, 4993, 3039, 1486, 6934, 1112, 2196, 3876], + [ 5654, 2327, 4462, 1402, 2012, 6929, 6673, 6203, 5789, 0, 4392, 4947, 3648, 3501, 5274, 5183, 7216, 6535, 7140, 5022, 3621, 5077, 4090, 922, 3207, 1131, 7014, 6714, 2437, 6707, 6477, 2476, 5432, 3599, 1102, 6376, 7121, 6284, 2497, 5160, 4318, 937, 6795, 6507, 1268, 5773, 4249, 1914], + [ 1713, 4148, 1184, 5366, 2573, 2831, 2380, 2241, 1602, 4392, 0, 586, 766, 1029, 883, 2040, 3353, 2224, 3100, 1049, 1246, 1625, 503, 3841, 1196, 5054, 3042, 2488, 2945, 2676, 2087, 2331, 1114, 1650, 5459, 2132, 3037, 1958, 1997, 931, 2513, 3701, 2923, 2137, 5459, 1394, 711, 2534], + [ 1604, 4723, 1520, 5946, 3157, 2266, 1795, 2051, 1343, 4947, 586, 0, 1299, 1612, 406, 2208, 2824, 1639, 2542, 694, 1586, 1767, 1050, 4357, 1770, 5633, 2498, 1907, 3520, 2128, 1558, 2778, 531, 2171, 6003, 1552, 2472, 1538, 2506, 791, 2912, 4277, 2403, 1564, 5983, 827, 892, 3109], + [ 2368, 3635, 1498, 4679, 1924, 3407, 3051, 2920, 2330, 3648, 766, 1299, 0, 646, 1642, 2446, 3840, 2905, 3655, 1488, 730, 2096, 697, 3076, 533, 4363, 3567, 3122, 2453, 3219, 2842, 1592, 1791, 1480, 4706, 2772, 3610, 2721, 1232, 1656, 2550, 3001, 3403, 2860, 4697, 2126, 756, 1836], + [ 2201, 3125, 1103, 4378, 1580, 3854, 3405, 2762, 2291, 3501, 1029, 1612, 646, 0, 1853, 2026, 4349, 3247, 4119, 1997, 1341, 1753, 606, 3078, 419, 4070, 4052, 3517, 1923, 3690, 3032, 1866, 2142, 838, 4593, 3161, 4060, 2788, 1380, 1663, 1932, 2736, 3915, 3138, 4647, 2395, 1351, 1592], + [ 1290, 4907, 1501, 6225, 3427, 2178, 1604, 1687, 970, 5274, 883, 406, 1642, 1853, 0, 2029, 2803, 1438, 2466, 986, 1987, 1593, 1253, 4716, 2072, 5915, 2454, 1764, 3710, 2082, 1204, 3164, 497, 2287, 6342, 1419, 2379, 1134, 2867, 554, 2885, 4569, 2405, 1289, 6338, 555, 1297, 3406], + [ 1004, 3930, 951, 5709, 3179, 4076, 3382, 1304, 1451, 5183, 2040, 2208, 2446, 2026, 2029, 0, 4759, 3220, 4368, 2900, 3151, 442, 1765, 4960, 2444, 5443, 4396, 3610, 2932, 4034, 2572, 3891, 2525, 1590, 6278, 3313, 4261, 2033, 3398, 1476, 1241, 4287, 4390, 2928, 6419, 2428, 2749, 3337], + [ 3833, 7463, 4298, 8417, 5749, 727, 1469, 3932, 3376, 7216, 3353, 2824, 3840, 4349, 2803, 4759, 0, 1601, 477, 2359, 3617, 4345, 3851, 6433, 4372, 8098, 370, 1206, 6267, 726, 2384, 4754, 2335, 4991, 8148, 1452, 609, 2949, 4752, 3331, 5687, 6746, 437, 1948, 8005, 2334, 3098, 5618], + [ 2258, 6338, 2903, 7578, 4793, 881, 168, 2331, 1796, 6535, 2224, 1639, 2905, 3247, 1438, 3220, 1601, 0, 1165, 1563, 2988, 2829, 2666, 5882, 3401, 7263, 1233, 399, 5138, 923, 794, 4227, 1117, 3724, 7565, 286, 1049, 1348, 4051, 1881, 4248, 5903, 1322, 355, 7508, 887, 2302, 4736], + [ 3419, 7243, 3967, 8296, 5577, 293, 1020, 3487, 2959, 7140, 3100, 2542, 3655, 4119, 2466, 4368, 477, 1165, 0, 2170, 3520, 3965, 3588, 6393, 4183, 7977, 202, 767, 6041, 438, 1932, 4706, 2027, 4711, 8107, 1061, 132, 2503, 4652, 2972, 5344, 6617, 486, 1501, 7989, 1962, 2939, 5469], + [ 2267, 5105, 2169, 6135, 3409, 1930, 1681, 2669, 1951, 5022, 1049, 694, 1488, 1997, 986, 2900, 2359, 1563, 2170, 0, 1430, 2460, 1547, 4333, 2019, 5817, 2079, 1694, 3910, 1733, 1813, 2668, 654, 2694, 6029, 1366, 2130, 1991, 2525, 1474, 3542, 4455, 1923, 1641, 5957, 1071, 777, 3302], + [ 2957, 4043, 2209, 4802, 2223, 3310, 3110, 3487, 2835, 3621, 1246, 1586, 730, 1341, 1987, 3151, 3617, 2988, 3520, 1430, 0, 2779, 1387, 2905, 1062, 4482, 3398, 3119, 2922, 3087, 3115, 1240, 1953, 2175, 4607, 2796, 3501, 3119, 1136, 2173, 3268, 3136, 3189, 3029, 4527, 2355, 711, 2042], + [ 720, 4022, 652, 5707, 3066, 3672, 2993, 1175, 1112, 5077, 1625, 1767, 2096, 1753, 1593, 442, 4345, 2829, 3965, 2460, 2779, 0, 1401, 4781, 2166, 5427, 3984, 3212, 2946, 3620, 2224, 3603, 2089, 1496, 6178, 2906, 3861, 1719, 3132, 1040, 1479, 4211, 3969, 2553, 6290, 2012, 2336, 3189], + [ 1700, 3677, 828, 4982, 2185, 3315, 2827, 2260, 1725, 4090, 503, 1050, 697, 606, 1253, 1765, 3851, 2666, 3588, 1547, 1387, 1401, 0, 3621, 903, 4675, 3537, 2954, 2475, 3169, 2427, 2254, 1578, 1148, 5177, 2598, 3521, 2194, 1833, 1074, 2054, 3340, 3423, 2541, 5213, 1801, 1077, 2190], + [ 5279, 2863, 4136, 2322, 1860, 6199, 6009, 5840, 5346, 922, 3841, 4357, 3076, 3078, 4716, 4960, 6433, 5882, 6393, 4333, 2905, 4781, 3621, 0, 2718, 2042, 6254, 6024, 2569, 5966, 5913, 1687, 4807, 3384, 1716, 5699, 6384, 5787, 1852, 4687, 4285, 1272, 6022, 5892, 1629, 5178, 3581, 1639], + [ 2578, 3106, 1518, 4178, 1401, 3932, 3552, 3141, 2628, 3207, 1196, 1770, 533, 419, 2072, 2444, 4372, 3401, 4183, 2019, 1062, 2166, 903, 2718, 0, 3864, 4097, 3635, 1932, 3748, 3274, 1448, 2284, 1164, 4286, 3283, 4136, 3086, 967, 1973, 2285, 2507, 3935, 3331, 4312, 2589, 1284, 1340], + [ 6076, 1850, 4873, 320, 2491, 7745, 7412, 6596, 6285, 1131, 5054, 5633, 4363, 4070, 5915, 5443, 8098, 7263, 7977, 5817, 4482, 5427, 4675, 2042, 3864, 0, 7866, 7483, 2515, 7539, 7101, 3449, 6146, 3938, 1375, 7134, 7944, 6831, 3349, 5709, 4397, 1363, 7667, 7190, 1798, 6446, 5041, 2528], + [ 3465, 7173, 3954, 8186, 5486, 365, 1104, 3563, 3007, 7014, 3042, 2498, 3567, 4052, 2454, 4396, 370, 1233, 202, 2079, 3398, 3984, 3537, 6254, 4097, 7866, 0, 839, 5973, 374, 2019, 4569, 1996, 4669, 7970, 1085, 305, 2581, 4532, 2976, 5339, 6509, 287, 1581, 7844, 1974, 2838, 5369], + [ 2654, 6630, 3254, 7800, 5035, 482, 267, 2728, 2193, 6714, 2488, 1907, 3122, 3517, 1764, 3610, 1206, 399, 767, 1694, 3119, 3212, 2954, 6024, 3635, 7483, 839, 0, 5427, 558, 1181, 4349, 1377, 4044, 7723, 356, 653, 1744, 4218, 2241, 4614, 6121, 955, 743, 7644, 1231, 2465, 4957], + [ 3625, 1204, 2446, 2778, 894, 5774, 5300, 4120, 3889, 2437, 2945, 3520, 2453, 1923, 3710, 2932, 6267, 5138, 6041, 3910, 2922, 2946, 2475, 2569, 1932, 2515, 5973, 5427, 0, 5612, 4824, 2550, 4050, 1498, 3476, 5071, 5980, 4470, 2096, 3388, 1911, 1501, 5831, 4994, 3704, 4264, 3209, 1196], + [ 3115, 6814, 3581, 7859, 5141, 261, 821, 3240, 2661, 6707, 2676, 2128, 3219, 3690, 2082, 4034, 726, 923, 438, 1733, 3087, 3620, 3169, 5966, 3748, 7539, 374, 558, 5612, 0, 1716, 4280, 1624, 4298, 7679, 735, 420, 2263, 4216, 2606, 4967, 6179, 400, 1277, 7567, 1609, 2501, 5032], + [ 1574, 6001, 2441, 7408, 4611, 1659, 916, 1559, 1122, 6477, 2087, 1558, 2842, 3032, 1204, 2572, 2384, 794, 1932, 1813, 3115, 2224, 2427, 5913, 3274, 7101, 2019, 1181, 4824, 1716, 0, 4330, 1180, 3346, 7545, 1023, 1808, 578, 4062, 1438, 3693, 5763, 2115, 440, 7537, 763, 2404, 4603], + [ 3951, 3447, 2960, 3763, 1669, 4513, 4348, 4507, 3920, 2476, 2331, 2778, 1592, 1866, 3164, 3891, 4754, 4227, 4706, 2668, 1240, 3603, 2254, 1687, 1448, 3449, 4569, 4349, 2550, 4280, 4330, 0, 3184, 2510, 3402, 4031, 4698, 4281, 533, 3245, 3612, 2187, 4339, 4265, 3296, 3576, 1941, 1381], + [ 1748, 5253, 1966, 6461, 3677, 1746, 1270, 2082, 1372, 5432, 1114, 531, 1791, 2142, 497, 2525, 2335, 1117, 2027, 654, 1953, 2089, 1578, 4807, 2284, 6146, 1996, 1377, 4050, 1624, 1180, 3184, 0, 2685, 6475, 1022, 1952, 1341, 2963, 1050, 3358, 4787, 1926, 1086, 6436, 422, 1244, 3619], + [ 2142, 2656, 950, 4223, 1590, 4431, 3890, 2658, 2391, 3599, 1650, 2171, 1480, 838, 2287, 1590, 4991, 3724, 4711, 2694, 2175, 1496, 1148, 3384, 1164, 3938, 4669, 4044, 1498, 4298, 3346, 2510, 2685, 0, 4697, 3693, 4636, 2975, 1981, 1909, 1124, 2718, 4565, 3548, 4830, 2839, 2140, 1751], + [ 6755, 3123, 5564, 1427, 3113, 7910, 7698, 7304, 6883, 1102, 5459, 6003, 4706, 4593, 6342, 6278, 8148, 7565, 8107, 6029, 4607, 6178, 5177, 1716, 4286, 1375, 7970, 7723, 3476, 7679, 7545, 3402, 6475, 4697, 0, 7393, 8097, 7370, 3515, 6249, 5379, 2001, 7738, 7556, 461, 6829, 5267, 3013], + [ 2383, 6274, 2916, 7451, 4682, 769, 332, 2512, 1927, 6376, 2132, 1552, 2772, 3161, 1419, 3313, 1452, 286, 1061, 1366, 2796, 2906, 2598, 5699, 3283, 7134, 1085, 356, 5071, 735, 1023, 4031, 1022, 3693, 7393, 0, 965, 1542, 3883, 1913, 4286, 5772, 1121, 600, 7322, 902, 2128, 4608], + [ 3306, 7183, 3878, 8263, 5533, 207, 900, 3364, 2845, 7121, 3037, 2472, 3610, 4060, 2379, 4261, 609, 1049, 132, 2130, 3501, 3861, 3521, 6384, 4136, 7944, 305, 653, 5980, 420, 1808, 4698, 1952, 4636, 8097, 965, 0, 2380, 4629, 2877, 5250, 6583, 570, 1380, 7986, 1866, 2904, 5432], + [ 1029, 5622, 2035, 7131, 4352, 2225, 1484, 985, 611, 6284, 1958, 1538, 2721, 2788, 1134, 2033, 2949, 1348, 2503, 1991, 3119, 1719, 2194, 5787, 3086, 6831, 2581, 1744, 4470, 2263, 578, 4281, 1341, 2975, 7370, 1542, 2380, 0, 3952, 1127, 3197, 5518, 2658, 1002, 7395, 951, 2429, 4380], + [ 3530, 3085, 2482, 3669, 1252, 4435, 4185, 4091, 3543, 2497, 1997, 2506, 1232, 1380, 2867, 3398, 4752, 4051, 4652, 2525, 1136, 3132, 1833, 1852, 967, 3349, 4532, 4218, 2096, 4216, 4062, 533, 2963, 1981, 3515, 3883, 4629, 3952, 0, 2873, 3080, 2012, 4324, 4046, 3478, 3328, 1755, 1000], + [ 825, 4564, 1027, 6011, 3227, 2681, 2049, 1319, 676, 5160, 931, 791, 1656, 1663, 554, 1476, 3331, 1881, 2972, 1474, 2173, 1040, 1074, 4687, 1973, 5709, 2976, 2241, 3388, 2606, 1438, 3245, 1050, 1909, 6249, 1913, 2877, 1127, 2873, 0, 2374, 4392, 2943, 1659, 6285, 1012, 1563, 3254], + [ 2188, 2756, 1395, 4638, 2426, 5053, 4415, 2544, 2590, 4318, 2513, 2912, 2550, 1932, 2885, 1241, 5687, 4248, 5344, 3542, 3268, 1479, 2054, 4285, 2285, 4397, 5339, 4614, 1911, 4967, 3693, 3612, 3358, 1124, 5379, 4286, 5250, 3197, 3080, 2374, 0, 3386, 5284, 3997, 5585, 3386, 3125, 2664], + [ 4820, 1591, 3617, 1681, 1169, 6384, 6051, 5358, 4993, 937, 3701, 4277, 3001, 2736, 4569, 4287, 6746, 5903, 6617, 4455, 3136, 4211, 3340, 1272, 2507, 1363, 6509, 6121, 1501, 6179, 5763, 2187, 4787, 2718, 2001, 5772, 6583, 5518, 2012, 4392, 3386, 0, 6314, 5837, 2205, 5095, 3680, 1169], + [ 3489, 7027, 3891, 7987, 5313, 550, 1219, 3632, 3039, 6795, 2923, 2403, 3403, 3915, 2405, 4390, 437, 1322, 486, 1923, 3189, 3969, 3423, 6022, 3935, 7667, 287, 955, 5831, 400, 2115, 4339, 1926, 4565, 7738, 1121, 570, 2658, 4324, 2943, 5284, 6314, 0, 1676, 7603, 1964, 2662, 5184], + [ 1947, 6186, 2686, 7502, 4706, 1224, 482, 1987, 1486, 6507, 2137, 1564, 2860, 3138, 1289, 2928, 1948, 355, 1501, 1641, 3029, 2553, 2541, 5892, 3331, 7190, 1581, 743, 4994, 1277, 440, 4265, 1086, 3548, 7556, 600, 1380, 1002, 4046, 1659, 3997, 5837, 1676, 0, 7521, 744, 2325, 4670], + [ 6835, 3472, 5661, 1877, 3241, 7805, 7635, 7391, 6934, 1268, 5459, 5983, 4697, 4647, 6338, 6419, 8005, 7508, 7989, 5957, 4527, 6290, 5213, 1629, 4312, 1798, 7844, 7644, 3704, 7567, 7537, 3296, 6436, 4830, 461, 7322, 7986, 7395, 3478, 6285, 5585, 2205, 7603, 7521, 0, 6805, 5208, 3102], + [ 1542, 5461, 2023, 6758, 3962, 1670, 1054, 1785, 1112, 5773, 1394, 827, 2126, 2395, 555, 2428, 2334, 887, 1962, 1071, 2355, 2012, 1801, 5178, 2589, 6446, 1974, 1231, 4264, 1609, 763, 3576, 422, 2839, 6829, 902, 1866, 951, 3328, 1012, 3386, 5095, 1964, 744, 6805, 0, 1644, 3928], + [ 2379, 4390, 1867, 5360, 2651, 2704, 2432, 2879, 2196, 4249, 711, 892, 756, 1351, 1297, 2749, 3098, 2302, 2939, 777, 711, 2336, 1077, 3581, 1284, 5041, 2838, 2465, 3209, 2501, 2404, 1941, 1244, 2140, 5267, 2128, 2904, 2429, 1755, 1563, 3125, 3680, 2662, 2325, 5208, 1644, 0, 2532], + [ 3744, 2088, 2560, 2844, 304, 5230, 4884, 4296, 3876, 1914, 2534, 3109, 1836, 1592, 3406, 3337, 5618, 4736, 5469, 3302, 2042, 3189, 2190, 1639, 1340, 2528, 5369, 4957, 1196, 5032, 4603, 1381, 3619, 1751, 3013, 4608, 5432, 4380, 1000, 3254, 2664, 1169, 5184, 4670, 3102, 3928, 2532, 0], +] + + +# dmatrix = [ +# [0, 4, INF, 10], +# [4, 0, 50, 20], +# [INF, 50, 0, 3], +# [10, 20, 3, 0], +# ] + + +# # # p01_d.txt +# dmatrix = [ +# [0, 29, 82, 46, 68, 52, 72, 42, 51, 55, 29, 74, 23, 72, 46], +# [29, 0, 55, 46, 42, 43, 43, 23, 23, 31, 41, 51, 11, 52, 21], +# [82, 55, 0, 68, 46, 55, 23, 43, 41, 29, 79, 21, 64, 31, 51], +# [46, 46, 68, 0, 82, 15, 72, 31, 62, 42, 21, 51, 51, 43, 64], +# [68, 42, 46, 82, 0, 74, 23, 52, 21, 46, 82, 58, 46, 65, 23], +# [52, 43, 55, 15, 74, 0, 61, 23, 55, 31, 33, 37, 51, 29, 59], +# [72, 43, 23, 72, 23, 61, 0, 42, 23, 31, 77, 37, 51, 46, 33], +# [42, 23, 43, 31, 52, 23, 42, 0, 33, 15, 37, 33, 33, 31, 37], +# [51, 23, 41, 62, 21, 55, 23, 33, 0, 29, 62, 46, 29, 51, 11], +# [55, 31, 29, 42, 46, 31, 31, 15, 29, 0, 51, 21, 41, 23, 37], +# [29, 41, 79, 21, 82, 33, 77, 37, 62, 51, 0, 65, 42, 59, 61], +# [74, 51, 21, 51, 58, 37, 37, 33, 46, 21, 65, 0, 61, 11, 55], +# [23, 11, 64, 51, 46, 51, 51, 33, 29, 41, 42, 61, 0, 62, 23], +# [72, 52, 31, 43, 65, 29, 46, 31, 51, 23, 59, 11, 62, 0, 59], +# [46, 21, 51, 64, 23, 59, 33, 37, 11, 37, 61, 55, 23, 59, 0], +# ] + + +# # #five_d.txt +# dmatrix = [ +# [0.0, 3.0, 4.0, 2.0, 7.0], +# [3.0, 0.0, 4.0, 6.0, 3.0], +# [4.0, 4.0, 0.0, 5.0, 8.0], +# [2.0, 6.0, 5.0, 0.0, 6.0], +# [7.0, 3.0, 8.0, 6.0, 0.0], +# ] + + +# # #gr17_d.txt +# dmatrix = [ +# [ 0, 633, 257, 91, 412, 150, 80, 134, 259, 505, 353, 324, 70, 211, 268, 246, 121], +# [ 633, 0, 390, 661, 227, 488, 572, 530, 555, 289, 282, 638, 567, 466, 420, 745, 518], +# [ 257, 390, 0, 228, 169, 112, 196, 154, 372, 262, 110, 437, 191, 74, 53, 472, 142], +# [ 91, 661, 228, 0, 383, 120, 77, 105, 175, 476, 324, 240, 27, 182, 239, 237, 84], +# [ 412, 227, 169, 383, 0, 267, 351, 309, 338, 196, 61, 421, 346, 243, 199, 528, 297], +# [ 150, 488, 112, 120, 267, 0, 63, 34, 264, 360, 208, 329, 83, 105, 123, 364, 35], +# [ 80, 572, 196, 77, 351, 63, 0, 29, 232, 444, 292, 297, 47, 150, 207, 332, 29], +# [ 134, 530, 154, 105, 309, 34, 29, 0, 249, 402, 250, 314, 68, 108, 165, 349, 36], +# [ 259, 555, 372, 175, 338, 264, 232, 249, 0, 495, 352, 95, 189, 326, 383, 202, 236], +# [ 505, 289, 262, 476, 196, 360, 444, 402, 495, 0, 154, 578, 439, 336, 240, 685, 390], +# [ 353, 282, 110, 324, 61, 208, 292, 250, 352, 154, 0, 435, 287, 184, 140, 542, 238], +# [ 324, 638, 437, 240, 421, 329, 297, 314, 95, 578, 435, 0, 254, 391, 448, 157, 301], +# [ 70, 567, 191, 27, 346, 83, 47, 68, 189, 439, 287, 254, 0, 145, 202, 289, 55], +# [ 211, 466, 74, 182, 243, 105, 150, 108, 326, 336, 184, 391, 145, 0, 57, 426, 96], +# [ 268, 420, 53, 239, 199, 123, 207, 165, 383, 240, 140, 448, 202, 57, 0, 483, 153], +# [ 246, 745, 472, 237, 528, 364, 332, 349, 202, 685, 542, 157, 289, 426, 483, 0, 336], +# [ 121, 518, 142, 84, 297, 35, 29, 36, 236, 390, 238, 301, 55, 96, 153, 336, 0], +# ] diff --git a/algorithm/floyd.py b/algorithm/floyd.py new file mode 100644 index 0000000..32fb004 --- /dev/null +++ b/algorithm/floyd.py @@ -0,0 +1,84 @@ +from dataset import dmatrix, dmatrixLabel +import json + +def floyd(graph): + length = len(graph) + path = {} + + for i in range(length): + path.setdefault(i, {}) + for j in range(length): + if i == j: + continue + + path[i].setdefault(j, [i,j]) + new_node = None + + for k in range(length): + if k == j: + continue + + new_len = graph[i][k] + graph[k][j] + if graph[i][j] > new_len: + graph[i][j] = new_len + new_node = k + if new_node: + path[i][j].insert(-1, new_node) + + return graph, path + +def floyd_dict(graph): + length = len(graph) + path = {} + + for src in graph: + path.setdefault(src, {}) + for dst in graph[src]: + if src == dst: + continue + path[src].setdefault(dst, [src,dst]) + new_node = None + + for mid in graph: + if mid == dst: + continue + + new_len = graph[src][mid] + graph[mid][dst] + if graph[src][dst] > new_len: + graph[src][dst] = new_len + new_node = mid + if new_node: + path[src][dst].insert(-1, new_node) + + return graph, path + + +if __name__ == '__main__': + ini = float('inf') + # graph_list = [ [0, 2, 1, 4, 5, 1], + # [1, 0, 4, 2, 3, 4], + # [2, 1, 0, 1, 2, 4], + # [3, 5, 2, 0, 3, 3], + # [2, 4, 3, 4, 0, 1], + # [3, 4, 7, 3, 1, 0]] + + # graph_dict = { "s1":{"s1": 0, "s2": 2, "s10": 1, "s12": 4}, + # "s2":{"s1": 1, "s2": 0, "s10": 4, "s12": 2}, + # "s10":{"s1": 2, "s2": 1, "s10": 0, "s12":1}, + # "s12":{"s1": 3, "s2": 5, "s10": 2, "s12":0}, + # } + + # #new_graph, path= floyd_dict(graph_dict) + # new_graph, path = floyd(graph_list) + # print(new_graph, '\n\n\n', path) + + + new_graph, path = floyd(dmatrix) + print("Weights: ", new_graph) + print("Paths: ", path) + + with open("Weights.json", "w") as fp: + json.dump(new_graph, fp) + + with open("Paths.json", "w") as fp: + json.dump(path, fp) \ No newline at end of file diff --git a/conf/__init__.py b/conf/__init__.py new file mode 100644 index 0000000..967218d --- /dev/null +++ b/conf/__init__.py @@ -0,0 +1,2 @@ +from .database import register_mysql +from .setting import setting diff --git a/conf/__pycache__/__init__.cpython-311.pyc b/conf/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..7a69408 Binary files /dev/null and b/conf/__pycache__/__init__.cpython-311.pyc differ diff --git a/conf/__pycache__/database.cpython-311.pyc b/conf/__pycache__/database.cpython-311.pyc new file mode 100644 index 0000000..31e2f45 Binary files /dev/null and b/conf/__pycache__/database.cpython-311.pyc differ diff --git a/conf/__pycache__/setting.cpython-311.pyc b/conf/__pycache__/setting.cpython-311.pyc new file mode 100644 index 0000000..75e582f Binary files /dev/null and b/conf/__pycache__/setting.cpython-311.pyc differ diff --git a/conf/database.py b/conf/database.py new file mode 100644 index 0000000..4ce516c --- /dev/null +++ b/conf/database.py @@ -0,0 +1,25 @@ +from fastapi import FastAPI +from tortoise.contrib.fastapi import register_tortoise + +from .setting import setting + + +def register_mysql(app: FastAPI): + register_tortoise( + app, + config={ + "connections": { + "default": setting.DB_URL + }, + "apps": { + "models": { + "models": ["aerich.models", "models"], + "default_connection": "default" + } + }, + 'use_tz': False, + 'timezone': setting.TIMEZONE + }, + generate_schemas=False, + add_exception_handlers=False, + ) diff --git a/conf/redis.py b/conf/redis.py new file mode 100644 index 0000000..5bb5abb --- /dev/null +++ b/conf/redis.py @@ -0,0 +1,10 @@ +# import aioredis +# +# from . import setting +# +# redis = aioredis.from_url( +# setting.REDIS_URL, +# encoding='utf-8', +# decode_responses=True, +# max_connections=5 +# ) diff --git a/conf/setting.py b/conf/setting.py new file mode 100644 index 0000000..0cfb08a --- /dev/null +++ b/conf/setting.py @@ -0,0 +1,76 @@ +import os +import sys +import logging +from pydantic import BaseSettings + + +class AGVPLC: + ''' + AGV配置PLC + ''' + HOST = '192.168.193.202' + PORT = 502 + +class Setting(BaseSettings): + # 项目信息 + PROJECT_NAME: str = 'agv' + VERSION: str = '1.0.0' + DESCRIPTION: str = '' + BASE_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) + + # 查看models/module.py的BelongTo类 + RUN_AT_OS: int = 0 + + # log + YANEI_LOG = BASE_DIR + os.sep + 'logs' # drcc日志目录 + YANEI_LOG_LEVEL = logging.INFO + LOG_MAX_SIZE = 200 * 1024 * 1024 # 文件最大值, 超过该size则切割(b-字节) + LOG_MAX_TIME = 30 * 24 * 3600 # 切割日志文件最大保留时间(s-秒) + LOG_PRE_FIX = 'yanei' + + # 调试模式 + DEBUG_MODE: bool = True + DEBUG_DEV_MODE: bool = True # 如是否自动热加载服务等 + + # SQL 调试 + SQL_DEBUG_MODE: bool = False + + # 服务端口号 + PORT: int = 8003 + # 数据库连接地址(必填) + DB_URL: str + REDIS_URL: str = 'redis://localhost' + # 终端ID(必填) + TERMINAL_ID: str + # 时区 + TIMEZONE: str = 'Asia/Shanghai' + # Jwt + JWT_SECRET_KEY: str + JWT_ALGORITHM: str = 'HS256' + + YANEI_CORS_ORIGINS: list = ['*'] # 默认的跨域请求, 但是在很多情况下不能直接写成* + + # 账号注册初始密码 + REGISTER_INIT_PASSWORD: str = '000000' + + AGVID = "001" + + # set AGV tray capacity + # 设置AGV小车托盘库存 + AGVCAP = { + 1: 6, #标准品A(1) + 3: 2, #标准品B(3) + 5: 0, #标准品C(5) + 7: 0, #标准品D(7) + 9: 20 #非标准品E(9) 假定一个托盘之多装20个非标品,需要根据实际情况确定!!! + } + + # 最大搜索距离 + # MAXSEARCHDEPTH = 3000 + MAXSEARCHDEPTH = 100000000000 + + class Config: + env_file = '.env' + + +setting = Setting() diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000..fd3b7e2 Binary files /dev/null and b/db.sqlite3 differ diff --git a/helper/Balance.py b/helper/Balance.py new file mode 100644 index 0000000..8572859 --- /dev/null +++ b/helper/Balance.py @@ -0,0 +1,143 @@ +import sys +from helper.SerialPort import * +from collections import Counter +import time +import threading + +from conf import setting + +# 天平收发数据文件 + + +class Balance: + lockSend=threading.Lock() + def __init__(self): + self.ser = SerialPort(setting.BALANCE_SERIAL, 9600) + # self.ser = SerialPort('com5', 9600) + self.balanceData = 0 + + # # 启动服务 + # def start(self): + # self.balanceData = 0 + # self.taskKillFlag = False + # p = threading.Thread(target=self.readDataThread) + # p.start() + # + # # 停止服务 + # def stop(self): + # self.taskKillFlag = True + # self.balanceData = 0 + + def readDataThread(self): + while not (self.taskKillFlag): + try: + dataStr = self.readRealTimeVal() + if (dataStr): + if (dataStr[0] == '+'): + self.balanceData = float(dataStr[1:]) + else: + self.balanceData = float(dataStr[1:]) * -1 + else: + self.balanceData = -999999 + + time.sleep(0.1) + except Exception as e: + print(str(e)) + + def getData(self): + try: + dataStr = self.readRealTimeVal() + if (dataStr): + if (dataStr[0] == '+'): + self.balanceData = float(dataStr[1:]) + else: + self.balanceData = float(dataStr[1:]) * -1 + else: + self.balanceData = -999999 + except Exception as e: + print(str(e)) + self.balanceData = -999999 + return self.balanceData + + def readRealTimeVal(self): + try: + data = None + content = self.write('001?\r\n') + #print('11111') + #print(content) + if content: + list_data = content.split('\r\n') + # print(list_data) + for i in list_data: + if "WT:" in i: + data = i + break + if data: + data = data.replace(' ', '') + data = data.replace('WT:', '') + data = data.replace('g', '') + if '-' not in data: + data = '+' + data + #print(data) + return data + + else: + return data + except Exception as e: + print(str(e)) + + # 去皮 + def tare(self): + self.write('T\r\n') + + # 归零 + def zero(self): + self.write('Z') + + def write(self, data): + try: + self.lockSend.acquire() + self.ser.Write(data.encode()) + #print(data) + content = bytes.decode(self.ser.Read()) + # print(content) + return content + except Exception as e: + print(str(e)) + return None + finally: + self.lockSend.release() + + def read(self): + # 过滤天平串口返回 + content = self.write('001?\r\n') + print(content) + if content: + list_data = content.split('\r\n') + # print(list_data) + data = None + for i in list_data: + if "WT:" in i: + data = i + break + if data: + data = data.replace(' ', '') + data = data.replace('WT:', '') + data = data.replace('g', '') + if '-' not in data: + data = '+' + data + print(data) + return data + + else: + return None + +balance = Balance() +# if __name__ == '__main__': +# # balance.write('#') +# balance.tare() +# while 1: +# data = balance.getData() +# print(data) +# time.sleep(1) +# # print(a.readRealTimeVal()) diff --git a/helper/GaoPaiYi.py b/helper/GaoPaiYi.py new file mode 100644 index 0000000..78d4cea --- /dev/null +++ b/helper/GaoPaiYi.py @@ -0,0 +1,168 @@ +import requests +import time +import json +import uuid +import sys +import os +import threading + +# 获取分辨率索引 +url = "http://localhost:6543/GetAllDisplayInfo" +param = { + "dev_idx": "0", +} + +import inspect +import ctypes + + +# 关闭线程 +def _async_raise(tid, exctype): + """raises the exception, performs cleanup if needed""" + tid = ctypes.c_long(tid) + if not inspect.isclass(exctype): + exctype = type(exctype) + res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype)) + if res == 0: + raise ValueError("invalid thread id") + elif res != 1: + # """if it returns a number greater than one, you're in trouble, + # and you should call it again with exc=NULL to revert the effect""" + ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None) + raise SystemError("PyThreadState_SetAsyncExc failed") + + +def stop_thread(thread): + _async_raise(thread.ident, SystemExit) + + +class GaoPaiYi: + # path = "/home/yanyi/Project/MainManage/img" # 默认存储地址 + path = os.path.join(os.getcwd(), "img") + if not os.path.exists(path): + os.umask(0) + os.makedirs(path) + + def __init__(self): + url = "http://localhost:6543/GetAllDisplayInfo" + try: + res = requests.get(url=url) # 启动高拍仪 + print(json.loads(res.text)) + print(json.loads(res.text)['data']) + print(json.loads(res.text)['code'] == '0') + except: + pass + + def openCamera(self): + try: + url = "http://127.0.0.1:6543/GetAllDisplayInfo" + param = {} + header = {'Content-Type': 'application/json;charset=utf-8'} + res = requests.get(url=url, data=json.dumps(param), headers=header, timeout=10) + print(json.loads(res.text)) + url = 'http://127.0.0.1:6543/video=stream&camidx=0' + + res = requests.get(url=url, data=json.dumps(param), headers=header, timeout=10) + print(json.loads(res.text)) + print(json.loads(res.text)['filepath']) + print('打开相机:::', json.loads(res.text)['code']) + except Exception as e: + print(e) + return {'code': -1, 'msg': e} + + # win 版本 + # def getPic(self): + # try: + # p = threading.Thread(target=self.openCamera) + # p.start() + # time.sleep(2) + # # 摄像头拍照 + # url = 'http://127.0.0.1:6543/video=grabimage' + # # url = 'https://restapi.getui.com/v2/h8m8bkLRUG6C313Xw8sNV8/auth' + # param = { + # "filepath": f"{self.path}\\{str(uuid.uuid1())}.jpg", + # "rotate": "90", #// 图像旋转角度,90的整数倍,默认:"0" + # "deskew": "0", #// 纠偏(主头有效),参数:0:不纠偏;1:纠偏 + # "deskewval":"0", #// 纠偏像素值:正常给0,正数时多裁,负数时少裁 + # "camidx": "0", #// 摄像头索引,参数:0:主头;1:副头 + # "ColorMode": "0", #// 色彩模式,图片保存本地时调用。0:彩色 1:灰色 2:黑白 3:白纸印章 4:去背景色(普通文件) 5:去背景色(身份证) + # "quality": "0", #// 图片质量,图片保存本地调用 + + # # 。0:默认质量;1:高质量;2:较高质量;3:中质量;4:较低质量;5:低质量 + # "bAutoAdjust":"1", #// 是否自动摆正: 0:不摆正 1:摆正 + # "bIsPrint1to1":"", #// 是否1:1打印 + # "watermark": { + # "pos": "", #// 水印在图像中的位置,0:左上;1:右上;2:左下;3:右下;4:中间 + # "content": "", #// 水印内容,必须utf-8编码,当水印内容为空,将当前时间作为水印 + # "transparency": "", #// 透明度,0~255,0:完全透明;255:不透明 + # "fontsize": "", #// 字体大小,默认:32 + # "font": "", #// 字体 + # "color": "" # // 水印颜色,colorname + # } + # } + # header ={'Content-Type':'application/json;charset=utf-8'} + # res = requests.post(url = url,data=json.dumps(param),headers=header) + # print(json.loads(res.text).get('filepath')) + # if json.loads(res.text)['code'] == "0": + # res_data = {'code':0,'msg':"图片保存成功","path":json.loads(res.text)['filepath'].split("\\")[-1]} + # else: + # res_data= {'code':-1, 'msg':"图片保存失败", "path":json.loads(res.text)['code']} + # print(res_data) + # # 关闭摄像头 + # url = 'http://127.0.0.1:6543/video=close' + # param = {"camidx":"0"} + # header = {'Content-Type': 'application/json;charset=utf-8'} + # res = requests.post(url=url, data=json.dumps(param), headers=header,timeout=10) + # print('关闭摄像头:::::::',json.loads(res.text)['code']) + # stop_thread(thread=p) + + # return res_data + + # except Exception as e: + # print('报错信息: ' + e + ' ,File: ' + __file__ + ', Line ' + str(sys._getframe().f_lineno)) + # return {'code':-1,'msg':e} + # # linux 版本 + def getPic(self): + try: + # if path: + # self.path = path + # print('----------预览视频-------------------------') + # 打開視頻 + url = "http://localhost:6543/StartPreview?dev_idx=0&res_id=0&pixfmt=pixfmt" + res = requests.get(url=url) + print(res.text) + # print(json.loads(res.text)['data']) + # print(json.loads(res.text)['code']=='0') + if json.loads(res.text)['returnCode'] != 0: + return {'code': -1, 'msg': "打开视频失败"} + time.sleep(5) + + # print('----------拍照-------------------------',__file__) + # 打開視頻 + url = "http://localhost:6543/getPic?savepath=" + self.path + "&quality=80" + res = requests.get(url=url) + + # print(json.loads(res.text)['data']["path"]) + # print(json.loads(res.text)['returnCode']==0) /home/yanyi/Project/MainManage/img/2022-9-9 15-36-35.jpg + if json.loads(res.text)['returnCode'] == 0: + res_data = {'code': 0, 'msg': "图片保存成功", "path": json.loads(res.text)['data']["path"].split("/")[-1]} + else: + res_data = {'code': -1, 'msg': "图片保存失败", "path": json.loads(res.text)['returnCode']} + + # print('----------关闭视频-------------------------') + url = "http://localhost:6543/StopPreview?dev_idx=0" + res = requests.get(url=url) + + return res_data + + except Exception as e: + print('报错信息: ' + str(e) + ' ,File: ' + __file__ + ', Line ' + str(sys._getframe().f_lineno)) + return {'code': -1, 'msg': "高拍仪连接失败,请打开高拍仪地址重新连接一下,{0}".format(str(e))} + + +if __name__ == "__main__": + obj = GaoPaiYi() + obj.getPic() + path = '/tmp' + # pic_url = GaoPaiYi().getPic(path) + # print(pic_url) diff --git a/helper/SerialPort.py b/helper/SerialPort.py new file mode 100644 index 0000000..63938ca --- /dev/null +++ b/helper/SerialPort.py @@ -0,0 +1,216 @@ +import binascii +import re +import time + +import serial +from time import sleep +import threading +class SerialPort(object): + + def __init__(self,Name,BaudRate,timeout=0.1): + # 串口通信同步锁 + self.lockSend=threading.Lock() + self.ser = None + self.encoding = '' # 编码格式 + try: + self.ser = serial.Serial(Name, BaudRate, timeout=timeout,) + except Exception as e: + print('serialserialserial',str(e)) + + # 发送的指令'00 01 00 00 23 ...' + def send_cmd_hex(self,cmd,readCount=0): + try: + self.lockSend.acquire() + data=None + cmd = bytes.fromhex(cmd) # 将发送的 + retry_times = 0 # 默认读取一次 + self.ser.flushInput() + self.ser.write(cmd) + time.sleep(0.2) + while retry_times ==0: + count = self.ser.inWaiting() #获取接收缓存区的字节数 + retry_times += 1 + if(readCount==0): + data = self.ser.readall() + if data == None: + continue + else: + break + else: + if(readCount==1): + data = self.ser.read(count) + else: + data = self.ser.read(readCount) + if data == None: + continue + else: + break + #sleep(0.002) + # self.ser.flushInput() + #print('ggggggggggg::',data) # b'\x01\x03\x04\x00\x00\x00\x00\xfa3' + return data + except Exception as e: + raise e + except OSError as e: + raise e + finally: + self.lockSend.release() + + # 发送给的指令 #~000~qst~start~@,对字符串进行转换成byte类型再发送传给 + def send_cmd(self,cmd,readCount=0): + try: + self.lockSend.acquire() + data=None + if self.encoding: + cmd = cmd.encode(self.encoding) + else: + cmd = cmd.encode() + + retry_times = 0 # 默认读取一次 + self.ser.flushInput() + self.ser.write(cmd) + time.sleep(0.2) + while retry_times ==0: + count = self.ser.inWaiting() #获取接收缓存区的字节数 + retry_times += 1 + if(readCount==0): + data = self.ser.readall() + + if self.encoding: + data = data.decode(self.encoding) + else: + data = data.decode() + + if data == None: + continue + else: + break + else: + if(readCount==1): + data = self.ser.read(count) + else: + data = self.ser.read(readCount) + if data == None: + continue + else: + break + #sleep(0.002) + # self.ser.flushInput() + print('ggggggggggg::',data) + return data.replace("\r\n", '') + except Exception as e: + raise e + except OSError as e: + raise e + finally: + self.lockSend.release() + + def Read(self,readCount=0): + try: + self.lockSend.acquire() + data=None + while True: + count = self.ser.inWaiting() #获取接收缓存区的字节数 + if(readCount==0): + data = self.ser.readall() + if data == None: + continue + else: + break + else: + if(readCount==1): + data = self.ser.read(count) + else: + data = self.ser.read(readCount) + if data == None: + continue + else: + break + #sleep(0.002) + # self.ser.flushInput() + return data + except Exception as e: + raise e + except OSError as e: + raise e + finally: + self.lockSend.release() + + def ReadAll(self,readCount=0): + try: + self.lockSend.acquire() + data=None + while True: + #count = self.ser.inWaiting() #获取接收缓存区的字节数 + if(readCount==0): + data = self.ser.readall() + if data == None: + continue + else: + break + else: + data = self.ser.read(readCount) + if data == None: + continue + else: + break + #sleep(0.002) + #self.ser.flushInput() + return data + except Exception as e: + raise e + except OSError as e: + raise e + finally: + self.lockSend.release() + + def FlushInput(self): #清空缓存 + try: + self.lockSend.acquire() + self.ser.flushInput() + except Exception as e: + raise e + except OSError as e: + raise e + finally: + self.lockSend.release() + + def Write(self,data): + try: + self.lockSend.acquire() + self.ser.flushInput() + self.ser.write(data) + except Exception as e: + raise e + except OSError as e: + raise e + finally: + self.lockSend.release() + + def Close(self): + try: + self.ser.close() + except Exception as e: + print('serialClose:',e) + + + + +if __name__ == "__main__": + + # sp = SerialPort.SerialPort('/dev/ttyS0',115200) + # while True: + # #cmd=input('输入发送:') + # sp.Write('#~000~qst~lock0~@\r\n'.encode()) + # #print("发送:"+cmd) + # d = sp.Read() + # print("接收:"+bytes.decode(d)) + + + # import SerialPort + ser = SerialPort('com1', 9600) + # ser = SerialPort('/dev/ttyUSB0', 9600) + print('rrrrrrrrrrrrrrrrrrrrrrrrrrrr:::::',ser) + cmd = "01 03 12 07 00 14 F1 7C" + data = ser.send_cmd_hex(cmd) + print('ssssssssssssssssssssssssssssssssssssssss:::',data) diff --git a/helper/VocSocket.py b/helper/VocSocket.py new file mode 100644 index 0000000..ed198f3 --- /dev/null +++ b/helper/VocSocket.py @@ -0,0 +1,32 @@ +import socket +import binascii + +def convert_to_decimal(hex_str): + return int(hex_str, 16) + +def main(): + TCP_IP = '192.168.0.7' + TCP_PORT = 26 + MESSAGE = bytes.fromhex('01 03 00 00 00 02 C4 0B') + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect((TCP_IP, TCP_PORT)) + s.send(MESSAGE) + data = s.recv(1024) + + hex_data = binascii.hexlify(data).decode('utf-8') + print("Received Hex Data:", hex_data) + + # 获取校验位和所需的数据部分 + # checksum = hex_data[-4:] # 校验位是最后两个字节 + required_data = hex_data[10:-4] # 数据部分是从索引12开始到校验位之前 + + # decimal_checksum = convert_to_decimal(checksum) + decimal_required_data = (int)(convert_to_decimal(required_data)/1000) + + # print("Decimal Checksum:", decimal_checksum) + print("Decimal Required Data:", decimal_required_data) + +if __name__ == "__main__": + main() + diff --git a/helper/__init__.py b/helper/__init__.py new file mode 100644 index 0000000..a33eaeb --- /dev/null +++ b/helper/__init__.py @@ -0,0 +1,41 @@ +import hashlib +from collections import namedtuple +from datetime import datetime +from urllib import parse + +from fastapi import Request, HTTPException +from fastapi.encoders import jsonable_encoder +from fastapi.responses import JSONResponse +from pytz import timezone + +from conf import setting + + +async def login_required(request: Request): + from models import User + + users = request.headers.get('X-TOKEN', '').split(',') + # 用户所选柜体 + client_id = request.headers.get("CLIENT-ID", '') + # 大类id + archive_id = request.headers.get("ARCHIVE-ID") + + if len(users) == 0: + raise HTTPException(status_code=401) + current_user_id = users[0] + current_user = await User.get_or_none(id=current_user_id) + if not current_user: + raise HTTPException(status_code=401) + + request.state.current_user = current_user + request.state.users = users + request.state.members = parse.unquote(request.headers.get('X-TOKEN-NAME', '')) + request.state.archive_id = archive_id + request.state.client_id = client_id + + +def respond_to(code: int = 200, desc: str = '成功', data=None): + if data is None: + return JSONResponse(content={"code": code, "desc": desc}) + else: + return JSONResponse(content={"code": code, "desc": desc, "data": jsonable_encoder(data)}) diff --git a/helper/__pycache__/__init__.cpython-311.pyc b/helper/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..726eca1 Binary files /dev/null and b/helper/__pycache__/__init__.cpython-311.pyc differ diff --git a/helper/__pycache__/common.cpython-311.pyc b/helper/__pycache__/common.cpython-311.pyc new file mode 100644 index 0000000..736a491 Binary files /dev/null and b/helper/__pycache__/common.cpython-311.pyc differ diff --git a/helper/__pycache__/logger.cpython-311.pyc b/helper/__pycache__/logger.cpython-311.pyc new file mode 100644 index 0000000..fc5b18a Binary files /dev/null and b/helper/__pycache__/logger.cpython-311.pyc differ diff --git a/helper/__pycache__/tools.cpython-311.pyc b/helper/__pycache__/tools.cpython-311.pyc new file mode 100644 index 0000000..b9b9725 Binary files /dev/null and b/helper/__pycache__/tools.cpython-311.pyc differ diff --git a/helper/__pycache__/utils.cpython-311.pyc b/helper/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..be441eb Binary files /dev/null and b/helper/__pycache__/utils.cpython-311.pyc differ diff --git a/helper/arial.ttf b/helper/arial.ttf new file mode 100644 index 0000000..3d6805d Binary files /dev/null and b/helper/arial.ttf differ diff --git a/helper/barcode_builders.py b/helper/barcode_builders.py new file mode 100644 index 0000000..67a7b93 --- /dev/null +++ b/helper/barcode_builders.py @@ -0,0 +1,9 @@ +from models.barcode_builders import BarcodeBuilders + + +async def get_barcode_index(): + data = await BarcodeBuilders.get(id=1).values("id", "index") + return data.index + +async def update_barcode_index(value): + await BarcodeBuilders.filter(id=1).update(index=value) diff --git a/helper/camera.py b/helper/camera.py new file mode 100644 index 0000000..810f419 --- /dev/null +++ b/helper/camera.py @@ -0,0 +1,207 @@ +import cv2 +import time +import threading +import platform +import os +import shutil + +# 背景介绍: +# 1、采用USB摄像头后每台柜子的成本能降低200元,以每年300台柜子的比例来说,一年就能节约60000元(由于旧的USB摄像头容易松动,从而导致售后问题,只要每年因为摄像头的售后费用低于60000元,采用USB摄像头是可行的) +# 2、旧的3.0系统发了很多的客户,主柜基本都是USB摄像头,考虑这批客户的USB摄像头都不能使用(大约数百个至1000个左右),全部采用海康的摄像头后,一是增加了成本,二是大量的旧件无法使用,故需要考虑在5.0系统中集成USB摄像头 +# +# 其他: +# 1、5.0的方案中由于下位机采用了网络通讯,故交换机并不会受到摄像头方案切换的影响(不能带入方案成本计算中) +# 2、5.0的方案中由于考虑了副柜的情况,故如果有副柜的主柜常常会配置扩展硬盘(常见是:1TB),用于视频录制,在摄像头方案的成本的时候只有主柜才会有扩展硬盘,硬盘选择与否与摄像头是网络摄像头还是USB摄像头无关 +# 3、如果有其他人问到USB摄像头和网络摄像头的方案技术选择问题,请转述上述的2个观点,如果有动机相关的问题,请转述“背景介绍”中的内容 +# +# 目前的方案如下,共2种: +# 1、RTSP -> SRS -> liveStream -> OpenCv -> Detect -> MP4 (RMS5.0初代,只支持网络摄像头) +# + -> webrtc前端渲染 +# +# 2、USB Camera -> VLC -> liveStream -> OpenCv -> Detect -> MP4 (支持USB摄像头和网络摄像头) +# + -> 前端渲染 +# +# 主柜采用黑色的USB摄像头,需要注意: +# 1、当主柜的人脸USB摄像头也接入的时候,camera_path可能需要调整,或者通过udev绑定设备 +# 2、如果有其他的程序使用了大量的磁盘空间,可能会导致所有的视频文件被删除 +# 3、人脸和人体检测会导致CPU负载上升 + +class Camera: + def __init__(self,debug_flag=False,camera_path=0,folder_path="usb_camera",face_detect=False,body_detect=False): + # 用于本地调试 + self.debug = debug_flag + # 人脸检测和上半身检测开关 + self.face_detect=face_detect + self.body_detect=body_detect + + # 获取操作系统类型 + self.sys_plat = platform.system().lower() + + # 初始化Linux下的视频文件夹 + self.video_folder_in_linux = "/data/"+folder_path + self.init_linux_video_folder() + # 初始化USB摄像头 + self.cap = cv2.VideoCapture(camera_path) + + # camera_worker状态控制 + self.worker_status = False + + # 获取摄像头的宽度和高度 + self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + + # 视频编解码器和VideoWriter对象 + self.fourcc = cv2.VideoWriter_fourcc(*'mp4v') + self.out = None + + # 初始化前一帧、移动侦测状态和人脸检测器 + self.ret, frame1 = self.cap.read() + self.prev_frame = cv2.cvtColor(frame1, cv2.COLOR_BGR2GRAY) + self.motion_detected = False + self.motion_start_time = None + + # 人脸检测 + self.face_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_frontalface_default.xml') + # 上半身检测 + self.upperbody_cascade = cv2.CascadeClassifier(cv2.data.haarcascades + 'haarcascade_upperbody.xml') + + + def start(self): + self.worker_status=True + p = threading.Thread(target=self.camera_worker) + p.start() + + def camera_worker(self): + while self.worker_status: + ret, frame2 = self.cap.read() + if not ret: + break + + # 将当前帧转换为灰度图像 + curr_frame = cv2.cvtColor(frame2, cv2.COLOR_BGR2GRAY) + + # 计算当前帧和前一帧的差异 + frame_diff = cv2.absdiff(curr_frame, self.prev_frame) + + # 应用阈值处理 + _, thresh = cv2.threshold(frame_diff, 30, 255, cv2.THRESH_BINARY) + + # 找到轮廓 + contours, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) + + # 移动侦测并绘制矩形框显示移动区域(调试) + for contour in contours: + if cv2.contourArea(contour) > 1000: + if self.debug: + (x, y, w, h) = cv2.boundingRect(contour) + cv2.rectangle(frame2, (x, y), (x+w, y+h), (0, 255, 0), 2) + self.motion_detected = True + self.motion_start_time = time.time() + + # 人脸和上半身检测(注意性能) + faces=[] + upperbodies=[] + if self.face_detect: + faces = self.face_cascade.detectMultiScale(curr_frame, 1.1, 4) + if self.body_detect: + upperbodies = self.upperbody_cascade.detectMultiScale(curr_frame, 1.1, 4) + + # 如果检测到移动、人脸、上半身,则开始录制视频 + if self.motion_detected or len(faces) > 0 or len(upperbodies) > 0: + self.motion_start_time = time.time() + if self.out is None: + filename = self.new_video_filename() + self.out = cv2.VideoWriter(filename, self.fourcc, 20.0, (self.width, self.height)) + self.out.write(frame2) + else: + if self.motion_start_time is not None and time.time() - self.motion_start_time > 3: + if self.out is not None: + self.out.release() + self.out = None + # 如果是Linux,如果磁盘空间超过阈值则删除目录下的文件 + if self.sys_plat == "linux": + self.delete_oldest_mp4_files(self.video_folder_in_linux) + + if self.debug: + # 显示结果 + cv2.imshow('Motion Detection', frame2) + + # 更新前一帧和移动侦测状态 + self.prev_frame = curr_frame + self.motion_detected = False + + if self.debug: + # 按下q键退出循环 + if cv2.waitKey(1) & 0xFF == ord('q'): + break + + def stop(self): + self.worker_status=False + # 释放资源 + # self.cap.release() + if self.out is not None: + self.out.release() + if self.debug: + cv2.destroyAllWindows() + + def new_video_filename(self): + if self.sys_plat == "windows": + timestamp = time.strftime("%Y%m%d%H%M%S") + filename = "{}.mp4".format(timestamp) + else: + timestamp = time.strftime("%Y%m%d%H%M%S") + filename = "{}.mp4".format(timestamp) + filename = self.video_folder_in_linux+"/"+filename + return filename + + def init_linux_video_folder(self): + if self.sys_plat == "linux": + self.create_directory(self.video_folder_in_linux) + + # 递归创建文件夹 + def create_directory(self,path): + if not os.path.exists(path): + parent_directory = os.path.dirname(path) + if parent_directory != '': + self.create_directory(parent_directory) + os.mkdir(path) + + + # 磁盘空间超过阈值的时候有且只删除最久的10个MP4文件 + def delete_oldest_mp4_files(self,folder_path): + total, used, free = shutil.disk_usage(folder_path) + if (used / total) > 0.7: + mp4_files = [] + for root, dirs, filenames in os.walk(folder_path): + for file in filenames: + if file.endswith(".mp4"): + mp4_files.append(os.path.join(root, file)) + mp4_files.sort(key=os.path.getmtime) + for file in mp4_files[:10]: + os.remove(file) + +class UsbCamera(Camera): + pass + +class NetworkCamera(Camera): + def __init__(self, debug_flag=False, camera_path=0, folder_path="usb_camera", face_detect=False, body_detect=False): + super().__init__(debug_flag, camera_path, folder_path, face_detect, body_detect) + +def start_service(): + # 正常业务逻辑 + from conf import setting + from models.hikvision import Hikvision + current_terminal_id = setting.TERMINAL_ID + camera_list = Hikvision.filter(terminal_id=current_terminal_id).all() + usb_cameras = list(filter(lambda x:x.camera_type==0,camera_list)) + network_cameras = list(filter(lambda x:x.camera_type==1,camera_list)) + if usb_cameras: + print("启动USB摄像头录制") + UsbCamera().start() + if network_cameras: + for c in network_cameras: + print(f"启动网络摄像头:{c.username}:{c.password}@{c.ip} , channel:{c.channel}") + NetworkCamera(False,f"rtsp://{c.username}:{c.password}@{c.ip}:554/Streaming/Channels/102",c.channel).start() + +if __name__ == "__main__": + Camera(True).start() diff --git a/helper/common.py b/helper/common.py new file mode 100644 index 0000000..9db9ecf --- /dev/null +++ b/helper/common.py @@ -0,0 +1,410 @@ +import os +import base64 +import socket +from functools import lru_cache, wraps +from typing import Union + +import json +import time +import math +import datetime +import platform +import subprocess +import ntpath + + +def compute_last_time(time1, ctime): + """计算time1与当前时间的差值,转换为D H M S格式""" + + day, hour, mins, sec = 0, 0, 0, 0 + dt1 = datetime.datetime.strptime(str(time1), '%Y-%m-%d %H:%M:%S') + dt2 = datetime.datetime.strptime(str(ctime), '%Y-%m-%d %H:%M:%S') + diff_str = str(dt2 - dt1) + if diff_str == '0:00:00': + return '0秒' + # 时、分、秒用小写字母h、m、s,天用大写字母D + if 'day' not in diff_str: + sec, mins, hour = list(map(int, diff_str.split(":")))[::-1] + else: + day = diff_str.split(',')[0].split(' ')[0] + sec_min_hour = diff_str.split(",")[1].strip().split(":") + sec, mins, hour = list(map(int, sec_min_hour))[::-1] + + day = int(day) + all_time = (F'{day}天', F'{hour}时', F'{mins}分', F'{sec}秒') + # 处理为仅仅两个单位,找到第一个非0的单位,取两个值即可 + start = 0 + for i, e in enumerate(all_time): + if int(e[:-1]) != 0: + start = i + break + real_time_str = ' '.join(all_time[start : start + 2]) + return real_time_str + + +def is_today_time(time1): + """判断是否今天内发生的""" + + today = str(datetime.datetime.today()).split(' ')[0] + ' 0:0:0' + dt1 = datetime.datetime.strptime(str(time1), '%Y-%m-%d %H:%M:%S') + dt2 = datetime.datetime.strptime(today, '%Y-%m-%d %H:%M:%S') + diff_str = str(dt1 - dt2) + if 'day' in diff_str: + return False, time1 + else: + return True, time1.split(' ')[1] + + +def time_convert(s: int) -> str: + """ + 秒转化为 + :param s: int 秒 + :return: str 转化后的时间, 数据加单位 + """ + m, s = divmod(s, 60) + + if not m: + return F"{s}秒" + + h, m = divmod(m, 60) + + if not h: + return F"{m}分{s}秒" + + d, h = divmod(h, 24) + + if not d: + return F"{h}时{m}分" + + return F"{d}天{h}时" + + +def time_convert_2_list(s: int) -> list: + """ + 秒转化为 + :param s: int 秒 + :return: list list[0]转化后的时间,list[1]数据单位 + """ + m, s = divmod(s, 60) + + if not m: + return [s, "秒"] + + h, m = divmod(m, 60) + + if not h: + return [m, "分"] + + d, h = divmod(h, 24) + + if not d: + return [h, "时"] + + ten, d = divmod(d, 10) + + if not ten: + return [d, "天"] + + return [10, "天+"] + + +def byte_convert_2_kb(s): + """ + 秒转化为 + :param s: int byte + :return: float kb + """ + return round(float(s) / 1024, 3) + + +def ms_2_s(s): + """ + 毫秒转为秒 + """ + return int(round(float(s) / 1000, 0)) + + +def bytes_to_tb(bytes: int) -> Union[int, float]: + tb = bytes / (1024**4) + tb_rounded = round(tb, 2) + if tb_rounded.is_integer(): + return int(tb_rounded) + else: + return tb_rounded + + +class lazyproperty: + """惰性属性""" + + def __init__(self, fun): + self.fun = fun + + def __get__(self, instance, owner): + if instance is None: + return self + value = self.fun(instance) + setattr(instance, self.fun.__name__, value) + return value + + +def ping_connect(ip, retry=1): + """ping测试""" + system = platform.system() + cmd = f'ping -n 1 -w 1 {ip}' if system == 'Windows' else f'ping -c 3 {ip}' + return_code = -1 + for i in range(retry): + with subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE) as p: + p.wait() + if p.returncode == 0: + return_code = p.returncode + break + time.sleep(1) + return return_code == 0 + + +def Singleton(cls): + """单例模式""" + _instance = {} + + def inner(): + if cls not in _instance: + _instance[cls] = cls() + return _instance[cls] + + return inner + + +class SingletonArguments(type): + _instances = {} + + def __call__(cls, *args, **kwargs): + if cls not in cls._instances: + cls._instances[cls] = super(SingletonArguments, cls).__call__(*args, **kwargs) + return cls._instances[cls] + + +def str2int(v): + try: + return int(v) + except Exception: + return v + + +def bytes2str(v): + try: + if isinstance(v, bytes): + return v.decode('utf8') + except Exception: + return v + + +def int2timestr(v): + try: + return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(v / 1000)) + except Exception as msg: + return v + + +def int2datetime(v): + try: + return datetime.datetime.fromtimestamp(v / 1000) + except Exception as msg: + return v + + +def str2datetime(v): + try: + return datetime.datetime.strptime(v, '%Y-%m-%s %H:%M:%S') + except Exception as msg: + return datetime.datetime.now() + + +def str2float(v): + try: + return float(v) + except Exception: + return v + + +def upper(v): + try: + return v.upper() + except Exception: + return v + + +def lower(v): + try: + return v.lower() + except Exception: + return v + + +def basename(path): + """获取路径的文件名""" + return ntpath.basename(path) + + +def yanei_decimal(value: float, num: int): + """保留num位有效数字 + + :param value:float: 待处理浮点数 + :param num:int: 有效位数 + """ + try: + if not isinstance(value, float): + return value + s = str(value) + pre, post = s.split('.') + return float(pre + '.' + post[:num]) + except Exception: + return value + + +def seconds_format(time_cost: int): + """ + 耗费时间格式转换 + :param time_cost: + :return: + """ + min_value = 60 + hour = 60 * 60 + day = 60 * 60 * 24 + if not time_cost or time_cost < 0: + return '0秒' + elif time_cost < min_value: + return '%s秒' % int(time_cost) + elif time_cost < hour: + # return '%s分%s秒' % (divmod(time_cost, min_value)) + return '%s分%s秒' % (divmod(int(time_cost), int(min_value))) + elif time_cost < day: + cost_hour, cost_min = divmod(int(time_cost), int(hour)) + if cost_min > min_value: + return '%s时%s' % (int(cost_hour), seconds_format(cost_min)) + else: + return '%s时0分%s' % (int(cost_hour), seconds_format(cost_min)) + else: + cost_day, cost_hour = divmod(int(time_cost), int(day)) + if cost_hour >= hour: + return '%s天%s' % (int(cost_day), seconds_format(cost_hour)) + elif cost_hour >= min_value: + return '%s天0时%s' % (int(cost_day), seconds_format(cost_hour)) + else: + return '%s天0时0分%s' % (int(cost_day), seconds_format(cost_hour)) + + +def calculate_rto(end, start, ceil=True): + """其中end, start都是ms级别的时间戳, ceil表示是否向上取整""" + total_miscro = float(end - start) + if ceil: + total = math.ceil(total_miscro / 1000) + else: + if total_miscro < 1000: + total = math.ceil(total_miscro / 1000) + else: + total = int(total_miscro / 1000) + return seconds_format(total) + + +def transform_duration_str_to_seconds(duration_str: str): + """ + 将时长字符串转换为秒 + :param duration_str: 时长字符串 eg: 1天2时3分4秒 + :return: + """ + if not duration_str: + return None + + try: + day, hour, minute, second = 0, 0, 0, 0 + if "天" in duration_str: + _values = duration_str.split("天", 1) + day = int(_values[0]) + duration_str = _values[-1] + + if "时" in duration_str: + _values = duration_str.split("时", 1) + hour = int(_values[0]) + duration_str = _values[-1] + + if "分" in duration_str: + _values = duration_str.split("分", 1) + minute = int(_values[0]) + duration_str = _values[-1] + + if "秒" in duration_str: + _values = duration_str.split("秒", 1) + second = int(duration_str.split("秒")[0]) + + return day * 24 * 60 * 60 + hour * 60 * 60 + minute * 60 + second + except Exception: + return None + + +def extra_script_data_after_call(resp): + """解析内容部门返回的响应数据并拼接成字符串""" + resp_data = '' + try: + overall = resp.get('overall') + _ts = resp.get('data', []) + resp_data = f'状态码:{overall} 其他信息:' + if isinstance(_ts, list): + for _t in _ts: + resp_data += f'{_t}\n' + except Exception: + try: + resp_data = f'{resp}' + except: + pass + return resp_data + + +def time_diff_int(str_time1, str_time2): + """计算差值,返回秒数""" + # TODO 修改类型 + if str(str_time2) == '2038-01-01 00:00:00': + raise Exception() + timearray = time.strptime(str_time1, "%Y-%m-%d %H:%M:%S") + int_time1 = int(time.mktime(timearray)) + timearray = time.strptime(str_time2, "%Y-%m-%d %H:%M:%S") + int_time2 = int(time.mktime(timearray)) + return int_time1 - int_time2 + + +def timed_lru_cache(seconds: int, maxsize: int = 128): + def wrapper_cache(func): + func = lru_cache(maxsize=maxsize)(func) + func.lifetime = datetime.timedelta(seconds=seconds) + func.expiration = datetime.datetime.now() + func.lifetime + + @wraps(func) + def wrapped_func(*args, **kwargs): + if datetime.datetime.now() >= func.expiration: + func.cache_clear() + func.expiration = datetime.datetime.now() + func.lifetime + + return func(*args, **kwargs) + + return wrapped_func + + return wrapper_cache + + +def is_illegal_name(name): + illegal_list = ['+', "-", "=", "@"] + for i in illegal_list: + if i in name: + return True + + return False + + +def get_local_ip(): + try: + # 获取本机主机名 + hostname = socket.gethostname() + # 获取本机IP地址 + ip_address = socket.gethostbyname(hostname) + return ip_address + except socket.error: + return "Unable to get local IP" diff --git a/helper/create_barcode.py b/helper/create_barcode.py new file mode 100644 index 0000000..280e67f --- /dev/null +++ b/helper/create_barcode.py @@ -0,0 +1,338 @@ +""" +此文件是用来生成Code128条形码 并实现打印功能 +""" + +import os +import uuid +import unicodedata +from PyQt5.QtCore import * +from PyQt5.QtGui import * +from PyQt5.QtPrintSupport import QPrinter +from PyQt5.QtWidgets import QApplication +from pystrich.code128 import Code128Encoder +from conf import setting + + +class CreateBarcode: + """ + Code128Encoder(options={}) options参数 + * ttf_font:用于呈现标签的truetype字体文件的绝对路径 + * ttf_fontsize:绘制标签的字体大小 + * label_border:条形码和标签之间的像素空间数 + * bottom_border:标签和底部边框之间的像素空间数 + * height:图像的高度(以像素为单位) + * show_label:是否在条形码下面显示标签(默认为True)“” + """ + + code_path = os.path.join(os.getcwd(), "img") + if not os.path.exists(code_path): + os.umask(0) + os.makedirs(code_path) + + def __init__(self): + self.archive_name = "" + + def create_Code128_img(self, barcode, name='', manufacturer=''): + show_barcode_only = False + if self.archive_name == "耗材" or (self.archive_name == "普通试剂" and setting.BARCODE_COMMON_SHOW_ONLY): + show_barcode_only = True + + a = Code128Encoder( + barcode, + options={ + 'ttf_font': os.path.join(os.getcwd(), 'arial.ttf'), + 'label_border': 0, + 'height': 140, + 'bottom_border': 14, + 'ttf_fontsize': 46 if show_barcode_only else 20, + }, + ) + # bar_width 条码宽度尺寸 + file_name = str(uuid.uuid4()) + '.png' + a.save(file_name, bar_width=3) + if show_barcode_only: + self.printer_only_code(file_name) + else: + self.printer_code(file_name, name, manufacturer) + + def printer_code(self, file_name, name, manufacturer): + + a = QApplication([]) + document = QTextDocument() + document.setDocumentMargin(0) + fontId = QFontDatabase.addApplicationFont(os.path.join(os.getcwd(), 'simsun.ttf')) + if len(name) >= 6: + name = name[:6] + '..' + if manufacturer and len(manufacturer) >= 6: + manufacturer = manufacturer[:6] + '..' + table_html = """ +
  
+
+   试剂名称:{1}
+   生产厂家:{2}
+   开封时间:
+   开封人: + """.format( + file_name, name, manufacturer + ) + html = """ + + Report + + + + {} + + """.format( + table_html + ) + document.setHtml(html) + printer = QPrinter() + printer.setPageSize(QPagedPaintDevice.Custom) + printer.setPaperSize(QSizeF(45.0, 30.0), QPrinter.Millimeter) + # 设置纸张到条码的边距 左上下右 + printer.setPageMargins(5, 3, 0, 0, QPrinter.Millimeter) + document.setPageSize(QSizeF(400.0, 320.0)) + print(document.pageSize(), printer.resolution(), printer.pageRect()) + print('正在打印中。。。。') + document.print_(printer) + print('打印完成。。') + os.remove(file_name) + + def create_drug_lobel_code(self, **kwargs): + a = Code128Encoder( + # kwargs.get("code_number"), + "111", + options={ + 'ttf_font': os.path.join(os.getcwd(), 'arial.ttf'), + 'label_border': 0, + 'height': 15, + 'bottom_border': 0, + 'ttf_fontsize': 0, + }, + ) + # bar_width 条码宽度尺寸 + + file_name = os.path.join(self.code_path, str(uuid.uuid4()) + '.png') + print(file_name) + a.save(file_name, bar_width=1) + kwargs["file_path"] = file_name + self.printer_drug_label(**kwargs) + + def zhsy_create_drug_label_code(self, **kwargs): + """ + 珠海食药打印配液标签 + :param kwargs: + :return: + """ + a = Code128Encoder( + kwargs.get("code_number"), + options={ + 'ttf_font': os.path.join(os.getcwd(), 'arial.ttf'), + 'label_border': 0, + 'height': 15, + 'bottom_border': 0, + 'ttf_fontsize': 0, + }, + ) + file_name = os.path.join(self.code_path, str(uuid.uuid4()) + '.png') + a.save(file_name, bar_width=1) + kwargs["file_path"] = file_name + self.zhsy_printer_drug_label(**kwargs) + + def printer_drug_label(self, **kwargs): + a = QApplication([]) + document = QTextDocument() + #

+ html = """ + + Report + + + +

+

+ 华润三九制药
+ 试剂名称: {}
+ 级别: {}
+ 批号: {}
+ 编号: {}
+ 货位号: {} {}
+ +

+
+ + """.format( + kwargs.get("name"), + kwargs.get("purity"), + kwargs.get("standard_code"), + kwargs.get("remark12"), + kwargs.get("client_name"), + kwargs.get("flow_position_code"), + kwargs.get("file_path"), + ) + document.setHtml(html) + printer = QPrinter() + printer.setPageSize(QPagedPaintDevice.Custom) + # printer.setPaperSize(QSizeF(60.0,40.0),QPrinter.Millimeter) + printer.setPaperSize(QSizeF(30.0, 18.0), QPrinter.Millimeter) + # printer.setPaperSize(QSizeF(30.0,50.0),QPrinter.Millimeter) + # 设置纸张到条码的边距 左上下右 + printer.setPageMargins(6, 3, 0, 0, QPrinter.Millimeter) + # printer.setPageMargins(20, 20, 0, 0, QPrinter.Millimeter) + + document.setPageSize(QSizeF(printer.pageRect().size())) + # document.setPageSize(QSizeF(50.0,30.0)) + + print('正在打印中。。。。') + document.print_(printer) + print('打印完成。。') + os.remove(kwargs.get("file_path")) + + def zhsy_printer_drug_label(self, **kwargs): + print(kwargs.get("file_path"), 55555555) + print('sss:', kwargs) + a = QApplication([]) + document = QTextDocument() + html = """ + + Report + + + +
+

+ 珠海市食品药品检验所
+ 名称: {}
+ 编号: {}
+ 浓度: {}   溶剂:{}
+ 配置日期: {}
+ 有 效 期:{}
+ 配 置 人:{}
+

+ +
+ + """.format( + kwargs.get("name")[:12], + kwargs.get("code_number"), + kwargs.get("purity")[:10], + kwargs.get("solvent"), + kwargs.get("start_time")[:10], + kwargs.get("end_time")[:10], + kwargs.get("user_name"), + kwargs.get("file_path"), + ) + print(html) + document.setHtml(html) + printer = QPrinter() + printer.setPageSize(QPagedPaintDevice.Custom) + printer.setPaperSize(QSizeF(60.0, 40.0), QPrinter.Millimeter) + # printer.setPaperSize(QSizeF(30.0,50.0),QPrinter.Millimeter) + # 设置纸张到条码的边距 左上下右 + printer.setPageMargins(6, 2, 0, 0, QPrinter.Millimeter) + + document.setPageSize(QSizeF(printer.pageRect().size())) + print('正在打印中。。。。') + document.print_(printer) + print('打印完成。。') + + def printer_only_code(self, file_name): + + a = QApplication([]) + document = QTextDocument() + document.setDocumentMargin(0) + fontId = QFontDatabase.addApplicationFont(os.path.join(os.getcwd(), 'simsun.ttf')) + + table_html = """ +
  
+
+ """.format( + file_name + ) + html = """ + + Report + + + + {} + + """.format( + table_html + ) + document.setHtml(html) + printer = QPrinter() + printer.setPageSize(QPagedPaintDevice.Custom) + printer.setPaperSize(QSizeF(30.0, 18.0), QPrinter.Millimeter) + # 设置纸张到条码的边距 左上下右 + printer.setPageMargins(0, 5, 0, 0, QPrinter.Millimeter) + document.setPageSize(QSizeF(400.0, 230.0)) + print(document.pageSize(), printer.resolution(), printer.pageRect()) + print('正在打印中。。。。') + document.print_(printer) + print('打印完成。。') + os.remove(file_name) + + +if __name__ == '__main__': + kwS = { + "code_number": "200001", + "medicament_name": "1111111", + "purity": "12", + "solvent": "2313", + "start_time": "2022-10-15", + "end_time": "2022-10-16", + "user_name": "测试账户", + } + # CreateBarcode().create_drug_lobel_code(**kwS) + CreateBarcode().create_Code128_img('123456', '硫酸', '研一') + # CreateBarcode().create_Code128_img("200000001") diff --git a/helper/exception.py b/helper/exception.py new file mode 100644 index 0000000..aa92771 --- /dev/null +++ b/helper/exception.py @@ -0,0 +1,11 @@ +class FaceNotFoundException(Exception): + pass + +class FaceMoreFoundException(Exception): + pass + +class ArcSoftCallException(Exception): + pass + +class NonLivingException(Exception): + pass \ No newline at end of file diff --git a/helper/export/__init__.py b/helper/export/__init__.py new file mode 100644 index 0000000..54c9626 --- /dev/null +++ b/helper/export/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/9/26 11:05 +# @Author : tx +# @File : __init__.py +# @Description : + +from .report_inventory import ReportInventory +from .report_drug_logs import ReportDrugUseLogs +from .report_use import ReportUse +from .report_slack import ReportSlack +from .report_put_in import ReportPutIn +from .report_remain import ReportRemain +from .report_dosage import ReportDosage +from .report_put_in1 import ReportPutIn1 +from .report_reagent_expiration import ReportReagentExpiration +from .report_environmental import ReportEnvironmental +from .report_take_out import ReportTakeOut +from .report_put_in_detail import ReportPutInDetail +from .report_take_out_detail import ReportTakeOutDetail diff --git a/helper/export/base.py b/helper/export/base.py new file mode 100644 index 0000000..7f3644d --- /dev/null +++ b/helper/export/base.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/9/26 10:58 +# @Author : tx +# @File : base.py +# @Description : +import string +from io import BytesIO +import urllib.parse + +from openpyxl.workbook import Workbook +from openpyxl.styles import Alignment, Side, Border, Font, PatternFill + +from helper.utils import timezone_now +from helper import usb +from datetime import datetime, timezone + + +class ReportExport: + def __init__(self): + self.title = "" + self.stateDict = {} + + # 新建一个workbook + self.wb = Workbook() + # 定义设置样式 + self.a = 0 + + self.styleTrue = '\u2611' # 打勾 + self.styleFalse = '\u25A1' # 空框 + + def set_style(self, title='sheet1', ty=1): + # 如果是第一次调用则删除默认创建的sheet表 + if self.a == 0: + self.wb.remove(self.wb['Sheet']) + self.a += 1 + # 创建第几个sheet + self.ws = self.wb.create_sheet() + # 设置sheet的标题 + self.ws.title = title + # 设置1 2 3 行固定的高度 + self.ws.row_dimensions[1].height = 35 + self.ws.row_dimensions[2].height = 25 + self.ws.row_dimensions[3].height = 28 + # 水平对齐, 居中对齐 + self.alignment_style2 = Alignment( + horizontal='left', vertical='top', wrapText=True) + self.alignment_style = Alignment( + horizontal='center', vertical='center', wrapText=True) + # 定义Border边框样式 + left, right, top, bottom = [Side(style='thin', color='000000')] * 4 + self.border_style = Border( + left=left, right=right, top=top, bottom=bottom) + # 设置列宽 + # 生成前14个大写字母 ascii_uppercase生成所有大写字母 + self.upper_string = string.ascii_uppercase[:15] + # 定义字体 + self.font_size = Font(size=9) + width_list = [20, 15, 10, 15, 15, 22, 15, 22, 15, 15] + for col in self.upper_string: + try: + width = width_list[self.upper_string.index(col)] + except: + width = 20 + self.ws.column_dimensions[col].width = width + + def editor_state(self, col_): + """ + 状态解析 + :param col_: + :return: + """ + self.max_lines = self.ws.max_row + for row_ in range(2, self.max_lines + 1): + b = self.ws[col_ + str(row_)].value + if "状态" in str(b): + continue + self.ws[col_ + str(row_)].value = self.stateDict.get(b) + + def create_row(self, data_list,title=''): + if title: + n=2 + self.ws.merge_cells(start_row=1, end_row=1, + start_column=1, end_column=len(data_list)) + # 写入值 + self.ws.cell(row=1, column=1).value = title + self.ws['A1'].alignment = self.alignment_style + self.ws['A1'].font = Font(size=16, bold=True) + else: + n=1 + for data in range(n, len(data_list) + n): + if n ==2: + data =data-1 + # 第一行写入值 + self.ws.cell(row=n, column=data).value = data_list[data - 1] + # 设置文本水平居中, 垂直居中 + self.ws.cell(row=n, column=data).alignment = self.alignment_style + # 设置字体加粗 + self.ws.cell(row=n, column=data).font = Font(bold=True, size=9) + # 边框样式 + self.ws.cell(row=n, column=data).border = self.border_style + + + def create_multiple_rows(self, start_row, data_list, keys_list): + # 从第2行创建 + for row_number in range(start_row, len(data_list) + start_row): + # 设置每一行的行高 + self.ws.row_dimensions[row_number].height = 60 + # 遍历每一个对象的长度 + col_ = 1 + for key_ in keys_list: + # 写入值 + try: + if key_ =='ysr': + value ='李世光' + elif key_ =='info': + value=f'{self.styleTrue}标识清晰\r\n{self.styleTrue}外观完整、无破损\r\n{self.styleTrue}形状无明显改变\r\n{self.styleTrue}符合申购要求\r\n{self.styleFalse}其他' + elif key_ =='jg': + value=f'{self.styleTrue}合格\r\n{self.styleFalse}不合格' + else: + value = data_list[row_number - start_row][key_] + if isinstance(value, datetime) and value.tzinfo is not None: + # 如果是带有时区信息的datetime对象,则移除时区信息 + value = value.replace(tzinfo=None) + except: + value = "/" + self.ws.cell(row=row_number, column=col_).value = value + # 设置文本水平居中, 垂直居中 + if key_ == "acceptace" or key_ == "info" or key_ == "jg": + self.ws.cell(row=row_number, column=col_).alignment = self.alignment_style2 + else: + self.ws.cell(row=row_number, column=col_).alignment = self.alignment_style + # 设置边框样式 + self.ws.cell(row=row_number, column=col_).border = self.border_style + # 设置字体大小 + self.ws.cell(row=row_number, column=col_).font = Font(size=9) + col_ += 1 + + def build_file(self,title='', **kwargs): + self.set_style(title="Sheet") + # 数据条目 + data_list = kwargs.pop("data_list") + # 英文列名 + key_list = kwargs.pop("key_list") + # 中文列名 + finds_name = kwargs.pop("finds_list") + # 创建标题 + self.create_row(finds_name,title) + # 创建数据 + if title: + n=3 + else: + n=2 + self.create_multiple_rows(n, data_list, key_list) + for i in key_list: + if "state" in i: + # 如果行超过25行异常处理 + if key_list.index(i) <= 25: + self.editor_state(string.ascii_uppercase[key_list.index(i)]) + else: + # AA AB AC AD... + divisor = key_list.index(i) // 25 - 1 + string_divisor = string.ascii_uppercase[divisor] + remainder = key_list.index(i) % 25 - 1 + string_remainder = string.ascii_uppercase[remainder] + self.editor_state(f"{string_divisor}{string_remainder}") + + def export(self, data, download_type='usb'): + """ + 导出 + :param data: + :param download_type: + :return: + """ + binary = BytesIO() + self.wb.save(binary) + self.wb.close() + binary.seek(0) + filename = f"{timezone_now().strftime('%Y%m%d%H%M%S')}-{self.title}-报表.xlsx" + if download_type == 'usb': + try: + usb.put_in(filename, binary) + return 200, "导出成功" + except usb.DeviceNotFound: + return 404, "U盘未找到" + encoded_filename = urllib.parse.quote(filename) + + return binary, encoded_filename diff --git a/helper/export/report_dosage.py b/helper/export/report_dosage.py new file mode 100644 index 0000000..4e658b4 --- /dev/null +++ b/helper/export/report_dosage.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/9/28 16:06 +# @Author : tx +# @File : report_dosage.py +# @Description : 药剂用量 + +from helper.export.base import ReportExport +from helper.report import report_dosage_drug + + +class ReportDosage(ReportExport): + def __init__(self, request, keyword): + super().__init__() + self.title = "使用信息汇总" + self.stateDict = {0: "待入库", 1: "在库", 2: "出库", 3: "空瓶", 4: "报废"} + self.request = request + self.keyword = keyword + + async def parse_data(self, request, keyword): + keyword.page_no = 0 + result = await report_dosage_drug(request, keyword) + self.drug_attribute_key_list = result.get("attribute_key") + return result.get("data") + + def parse_key_finds_list(self): + """ + 解析键列名 + :return: + """ + key_list = ["cas_code", "total_use_weight", "use_num"] + finds_list = ["CAS码", "用量", "使用次数"] + self.key_list = self.drug_attribute_key_list + key_list + self.finds_list = self.drug_attribute_key_list + finds_list + + async def main(self): + """ + 主函数 + :param kwargs: + :return: + """ + self.data_list = await self.parse_data(self.request, self.keyword) + self.parse_key_finds_list() diff --git a/helper/export/report_drug_logs.py b/helper/export/report_drug_logs.py new file mode 100644 index 0000000..bd0753c --- /dev/null +++ b/helper/export/report_drug_logs.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/9/26 11:02 +# @Author : tx +# @File : report_drug_logs.py +# @Description : + +from fastapi.encoders import jsonable_encoder +from tortoise.queryset import QuerySet + +from conf import setting +# from models.drug_use_log import DrugUseLog, DrugUseStateEnum +from helper.export.base import ReportExport +from helper.drug import milligram_to_gram +from helper.report import report_drug_logs + + +class ReportDrugUseLogs(ReportExport): + def __init__(self, request, keyword): + super().__init__() + self.title = "流转日志" + self.stateDict = {0: "入库", 1: "归还", 2: "领用", 3: "空瓶", 4: "报废"} + self.request = request + self.keyword = keyword + + async def parse_data(self, request, keyword): + """ + 解析数据 + :return: + """ + keyword.page_no = 0 + result = await report_drug_logs(request, keyword) + self.drug_attribute_key_list = result.get("attribute_key") + return result.get("data") + + def parse_key_finds_list(self): + """ + 解析键列名 + :return: + """ + key_list = ["state", "position", "users", "weight", "use_weight", "created_at"] + finds_list = ["流转类型", "位置", "用户", "余量", "用量", "创建时间"] + self.key_list = self.drug_attribute_key_list + key_list + self.finds_list = self.drug_attribute_key_list + finds_list + + async def main(self): + """ + 主函数 + :param kwargs: + :return: + """ + self.data_list = await self.parse_data(self.request, self.keyword) + self.parse_key_finds_list() diff --git a/helper/export/report_environmental.py b/helper/export/report_environmental.py new file mode 100644 index 0000000..db3884a --- /dev/null +++ b/helper/export/report_environmental.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/10/25 16:31 +# @Author : tx +# @File : report_environmental.py +# @Description : 环境记录报表导出 + +from helper.export.base import ReportExport +from helper.report import report_cabinet_environmental + + +class ReportEnvironmental(ReportExport): + def __init__(self, request, keyword): + super().__init__() + self.title = "环境温度记录" + self.stateDict = {0: "待入库", 1: "在库", 2: "出库", 3: "空瓶", 4: "报废"} + self.request = request + self.keyword = keyword + + async def parse_data(self, request, keyword): + keyword.page_no = 0 + keyword = keyword.dict() + result = await report_cabinet_environmental(request, keyword) + self.drug_attribute_key_list = result.get("attribute_key") + return result.get("data") + + def parse_key_finds_list(self): + """ + 解析键列名 + :return: + """ + key_list = ["cabinet_label", "temperature", "created_at"] + finds_list = ["柜体", "温度℃", "记录时间"] + self.key_list = self.drug_attribute_key_list + key_list + self.finds_list = self.drug_attribute_key_list + finds_list + + async def main(self): + """ + 主函数 + :param kwargs: + :return: + """ + self.data_list = await self.parse_data(self.request, self.keyword) + self.parse_key_finds_list() diff --git a/helper/export/report_inventory.py b/helper/export/report_inventory.py new file mode 100644 index 0000000..94b1263 --- /dev/null +++ b/helper/export/report_inventory.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/9/26 11:03 +# @Author : tx +# @File : report_inventory.py +# @Description : + + +from helper.export.base import ReportExport +from helper.report import report_inventory_drug + + +class ReportInventory(ReportExport): + def __init__(self, request, keyword): + super().__init__() + self.title = "库存信息记录" + self.stateDict = {0: "待入库", 1: "在库", 2: "出库", 3: "空瓶", 4: "报废"} + self.request = request + self.keyword = keyword + + async def parse_data(self, request, keyword): + keyword.page_no = 0 + result = await report_inventory_drug(request, keyword) + self.drug_attribute_key_list = result.get("attribute_key") + return result.get("data") + + def parse_key_finds_list(self): + """ + 解析键列名 + :return: + """ + key_list = ["rfid", "cas_code", "batch_no", "manufacturer", "net_weight", "remain_gross_weight", "position", "expired_at", "storage_at", "last_user"] + finds_list = ["RFID", "CAS码", "批号", "生产厂家", "净重", "余量", "位置", "有效日期", "入库时间", "最后使用人"] + self.key_list = self.drug_attribute_key_list + key_list + self.finds_list = self.drug_attribute_key_list + finds_list + + async def main(self): + """ + 主函数 + :param kwargs: + :return: + """ + self.data_list = await self.parse_data(self.request, self.keyword) + self.parse_key_finds_list() diff --git a/helper/export/report_put_in.py b/helper/export/report_put_in.py new file mode 100644 index 0000000..a5a9720 --- /dev/null +++ b/helper/export/report_put_in.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/9/27 19:13 +# @Author : tx +# @File : report_put_in.py +# @Description : 入库统计报表导出 + + +from helper.export.base import ReportExport +from helper.report import report_put_in_drug + +class ReportPutIn(ReportExport): + def __init__(self, request, keyword): + super().__init__() + self.title = "入库信息汇总" + self.stateDict = {0: "待入库", 1: "在库", 2: "出库", 3: "空瓶", 4: "报废"} + self.request = request + self.keyword = keyword + + async def parse_data(self, request, keyword): + keyword.page_no = 0 + result = await report_put_in_drug(request, keyword) + self.drug_attribute_key_list = result.get("attribute_key") if result.get("attribute_key") else [] + return result.get("data") + + def parse_key_finds_list(self): + """ + 解析键列名 + :return: + """ + key_list = ["cas_code", "count"] + finds_list = ["CAS码", "入库量"] + self.key_list = self.drug_attribute_key_list + key_list + self.finds_list = self.drug_attribute_key_list + finds_list + + async def main(self): + """ + 主函数 + :param kwargs: + :return: + """ + self.data_list = await self.parse_data(self.request, self.keyword) + self.parse_key_finds_list() diff --git a/helper/export/report_put_in1.py b/helper/export/report_put_in1.py new file mode 100644 index 0000000..eacaa46 --- /dev/null +++ b/helper/export/report_put_in1.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/9/27 19:13 +# @Author : tx +# @File : report_put_in.py +# @Description : 入库统计报表导出 + + +from helper.export.base import ReportExport +from helper.report import report_put_in_drug + +class ReportPutIn1(ReportExport): + def __init__(self, request, keyword): + super().__init__() + self.title = "入库验收记录" + self.stateDict = {0: "待入库", 1: "在库", 2: "出库", 3: "空瓶", 4: "报废"} + self.request = request + self.keyword = keyword + + async def parse_data(self, request, keyword): + keyword.page_no = 0 + result = await report_put_in_drug(request, keyword) + print("result", result) + self.drug_attribute_key_list = result.get("attribute_key") if result.get("attribute_key") else [] + + return result.get("data") + + def parse_key_finds_list(self): + """ + 解析键列名 + :return: + """ + key_list = ["expired_at","manufacturer","distribution","total_count","info","ysr","storage_at","jg"] + finds_list = ["有效期","生产厂商","经销单位","数量","验收记录","验收人","日期", "结果"] + self.key_list = self.drug_attribute_key_list + key_list + self.finds_list = self.drug_attribute_key_list + finds_list + + async def main(self): + """ + 主函数 + :param kwargs: + :return: + """ + self.data_list = await self.parse_data(self.request, self.keyword) + self.parse_key_finds_list() diff --git a/helper/export/report_put_in_detail.py b/helper/export/report_put_in_detail.py new file mode 100644 index 0000000..b257917 --- /dev/null +++ b/helper/export/report_put_in_detail.py @@ -0,0 +1,42 @@ +# -*- coding:utf-8 -*- +""" +@Created on : 2023/12/24 10:03 +@Author: bzy +@Des: +""" +from helper.export.base import ReportExport +from helper.report import report_put_in_detail_drug + + +class ReportPutInDetail(ReportExport): + def __init__(self, request, keyword): + super().__init__() + self.title = "入库信息记录" + self.stateDict = {0: "待入库", 1: "在库", 2: "出库", 3: "空瓶", 4: "报废"} + self.request = request + self.keyword = keyword + + async def parse_data(self, request, keyword): + keyword.page_no = 0 + result = await report_put_in_detail_drug(request, keyword) + self.drug_attribute_key_list = result.get("attribute_key") + return result.get("data") + + def parse_key_finds_list(self): + """ + 解析键列名 + :return: + """ + key_list = ["rfid", "cas_code", "batch_no", "manufacturer", "net_weight", "remain_gross_weight", "position", "expired_at", "storage_at", "storage_user", "state"] + finds_list = ["RFID", "CAS码", "批号", "生产厂家", "净重", "余量", "位置", "有效日期", "入库时间", "入库人", "当前状态"] + self.key_list = self.drug_attribute_key_list + key_list + self.finds_list = self.drug_attribute_key_list + finds_list + + async def main(self): + """ + 主函数 + :param kwargs: + :return: + """ + self.data_list = await self.parse_data(self.request, self.keyword) + self.parse_key_finds_list() diff --git a/helper/export/report_reagent_expiration.py b/helper/export/report_reagent_expiration.py new file mode 100644 index 0000000..0762e4b --- /dev/null +++ b/helper/export/report_reagent_expiration.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/10/17 17:04 +# @Author : tx +# @File : report_reagent_expiration.py +# @Description : 试剂过期导出 + +from helper.export.base import ReportExport +from helper.report import report_expire_drug + + +class ReportReagentExpiration(ReportExport): + def __init__(self, request, keyword): + super().__init__() + self.title = "试剂过期统计" + self.stateDict = {0: "待入库", 1: "在库", 2: "出库", 3: "空瓶", 4: "报废"} + self.request = request + self.keyword = keyword + + async def parse_data(self, request, keyword): + keyword.page_no = 0 + result = await report_expire_drug(request, keyword) + self.drug_attribute_key_list = result.get("attribute_key") + return result.get("data") + + def parse_key_finds_list(self): + """ + 解析键列名 + :return: + """ + key_list = ["expired_at", "last_user", "remain_gross_weight", "state", "position"] + finds_list = ["过期日期", "最后使用人", "余量", "状态", "位置"] + self.key_list = self.drug_attribute_key_list + key_list + self.finds_list = self.drug_attribute_key_list + finds_list + + async def main(self): + """ + 主函数 + :param kwargs: + :return: + """ + self.data_list = await self.parse_data(self.request, self.keyword) + self.parse_key_finds_list() diff --git a/helper/export/report_remain.py b/helper/export/report_remain.py new file mode 100644 index 0000000..be2ef48 --- /dev/null +++ b/helper/export/report_remain.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/9/27 13:13 +# @Author : tx +# @File : report_use.py +# @Description : 库存量统计报表导出 + +from helper.export.base import ReportExport +from helper.report import report_remain_drug + + +class ReportRemain(ReportExport): + def __init__(self, request, keyword): + super().__init__() + self.title = "库存信息汇总" + self.stateDict = {0: "待入库", 1: "在库", 2: "出库", 3: "空瓶", 4: "报废"} + self.request = request + self.keyword = keyword + + async def parse_data(self, request, keyword): + keyword.page_no = 0 + result = await report_remain_drug(request, keyword) + self.drug_attribute_key_list = result.get("attribute_key") + return result.get("data") + + def parse_key_finds_list(self): + """ + 解析键列名 + :return: + """ + key_list = ["cas_code", "count"] + finds_list = ["CAS码", "在库数量"] + self.key_list = self.drug_attribute_key_list + key_list + self.finds_list = self.drug_attribute_key_list + finds_list + + async def main(self): + """ + 主函数 + :param kwargs: + :return: + """ + self.data_list = await self.parse_data(self.request, self.keyword) + self.parse_key_finds_list() diff --git a/helper/export/report_slack.py b/helper/export/report_slack.py new file mode 100644 index 0000000..563fa07 --- /dev/null +++ b/helper/export/report_slack.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/9/27 13:58 +# @Author : tx +# @File : report_slack.py +# @Description : 呆滞物料报表导出 + + +from helper.export.base import ReportExport +from helper.report import report_slack_drug + + +class ReportSlack(ReportExport): + def __init__(self, request, keyword): + super().__init__() + self.title = "呆滞物料" + self.stateDict = {0: "待入库", 1: "在库", 2: "出库", 3: "空瓶", 4: "报废"} + self.request = request + self.keyword = keyword + + async def parse_data(self, request, keyword): + keyword.page_no = 0 + keyword = keyword.dict() + print("keyword", keyword) + result = await report_slack_drug(request, keyword) + self.drug_attribute_key_list = result.get("attribute_key") if result.get("attribute_key") else [] + return result.get("data") + + def parse_key_finds_list(self): + """ + 解析键列名 + :return: + """ + key_list = ["expired_at", "last_use_at", "last_user", "remain_gross_weight", "now_slack_days"] + finds_list = ["过期日期", "最后使用时间", "最后使用人", "余量", "呆滞天数"] + self.key_list = self.drug_attribute_key_list + key_list + self.finds_list = self.drug_attribute_key_list + finds_list + + async def main(self): + """ + 主函数 + :param kwargs: + :return: + """ + self.data_list = await self.parse_data(self.request, self.keyword) + self.parse_key_finds_list() diff --git a/helper/export/report_take_out.py b/helper/export/report_take_out.py new file mode 100644 index 0000000..d4ea00f --- /dev/null +++ b/helper/export/report_take_out.py @@ -0,0 +1,42 @@ +# -*- coding:utf-8 -*- +""" +@Created on : 2023/12/23 14:07 +@Author: bzy +@Des: +""" +from helper.export.base import ReportExport +from helper.report import report_take_out_drug + + +class ReportTakeOut(ReportExport): + def __init__(self, request, keyword): + super().__init__() + self.title = "出库信息汇总" + self.stateDict = {0: "待入库", 1: "在库", 2: "出库", 3: "空瓶", 4: "报废"} + self.request = request + self.keyword = keyword + + async def parse_data(self, request, keyword): + keyword.page_no = 0 + result = await report_take_out_drug(request, keyword) + self.drug_attribute_key_list = result.get("attribute_key") if result.get("attribute_key") else [] + return result.get("data") + + def parse_key_finds_list(self): + """ + 解析键列名 + :return: + """ + key_list = ["cas_code", "count"] + finds_list = ["CAS码", "出库数量"] + self.key_list = self.drug_attribute_key_list + key_list + self.finds_list = self.drug_attribute_key_list + finds_list + + async def main(self): + """ + 主函数 + :param kwargs: + :return: + """ + self.data_list = await self.parse_data(self.request, self.keyword) + self.parse_key_finds_list() diff --git a/helper/export/report_take_out_detail.py b/helper/export/report_take_out_detail.py new file mode 100644 index 0000000..9d200bc --- /dev/null +++ b/helper/export/report_take_out_detail.py @@ -0,0 +1,42 @@ +# -*- coding:utf-8 -*- +""" +@Created on : 2023/12/24 10:08 +@Author: bzy +@Des: +""" +from helper.export.base import ReportExport +from helper.report import report_take_out_detail_drug + + +class ReportTakeOutDetail(ReportExport): + def __init__(self, request, keyword): + super().__init__() + self.title = "出库信息记录" + self.stateDict = {0: "待入库", 1: "在库", 2: "出库", 3: "空瓶", 4: "报废"} + self.request = request + self.keyword = keyword + + async def parse_data(self, request, keyword): + keyword.page_no = 0 + result = await report_take_out_detail_drug(request, keyword) + self.drug_attribute_key_list = result.get("attribute_key") + return result.get("data") + + def parse_key_finds_list(self): + """ + 解析键列名 + :return: + """ + key_list = ["rfid", "cas_code", "batch_no", "manufacturer", "net_weight", "remain_gross_weight", "receive_user", "receive_position", "receive_at", "expired_at"] + finds_list = ["RFID", "CAS码", "批号", "生产厂家", "净重", "余量", "领用人", "领用位置", "领用时间", "有效日期"] + self.key_list = self.drug_attribute_key_list + key_list + self.finds_list = self.drug_attribute_key_list + finds_list + + async def main(self): + """ + 主函数 + :param kwargs: + :return: + """ + self.data_list = await self.parse_data(self.request, self.keyword) + self.parse_key_finds_list() diff --git a/helper/export/report_use.py b/helper/export/report_use.py new file mode 100644 index 0000000..646bf22 --- /dev/null +++ b/helper/export/report_use.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# @Time : 2023/9/27 13:13 +# @Author : tx +# @File : report_use.py +# @Description : + +from helper.export.base import ReportExport +from helper.report import report_use_drug + + +class ReportUse(ReportExport): + def __init__(self, request, keyword): + super().__init__() + self.title = "使用明细记录" + self.stateDict = {0: "待入库", 1: "在库", 2: "出库", 3: "空瓶", 4: "报废"} + self.request = request + self.keyword = keyword + + async def parse_data(self, request, keyword): + keyword.page_no = 0 + result = await report_use_drug(request, keyword) + self.drug_attribute_key_list = result.get("attribute_key") + return result.get("data") + + def parse_key_finds_list(self): + """ + 解析键列名 + :return: + """ + key_list = ["rfid", "cas_code", "batch_no", "manufacturer", "net_weight", "use_weight", "receive_users", "receive_position", "receive_at"] + finds_list = ["RFID", "CAS码", "批号", "生产厂家", "净重", "用量", "操作人员", "位置", "操作时间"] + self.key_list = self.drug_attribute_key_list + key_list + self.finds_list = self.drug_attribute_key_list + finds_list + + async def main(self): + """ + 主函数 + :param kwargs: + :return: + """ + self.data_list = await self.parse_data(self.request, self.keyword) + self.parse_key_finds_list() diff --git a/helper/face_camera_engine.py b/helper/face_camera_engine.py new file mode 100644 index 0000000..d20723c --- /dev/null +++ b/helper/face_camera_engine.py @@ -0,0 +1,98 @@ +import cv2 +from arcsoft.engine import * +from .exception import FaceNotFoundException, FaceMoreFoundException, ArcSoftCallException, NonLivingException +from conf import setting + +class FaceCameraEngine: + def __init__(self): + self.app_id = bytes(setting.ARCSOFT_APP_ID, 'utf-8') + self.sdk_key = bytes(setting.ARCSOFT_SDK_KEY, 'utf-8') + + def __enter__(self): + face_engine = ArcFace() + face_engine.ASFInitEngine(ASF_DETECT_MODE_IMAGE, ASF_OP_0_ONLY, 30, 5, ASF_FACE_DETECT | ASF_FACERECOGNITION | ASF_LIVENESS) + self.face_engine = face_engine + return self + + # 虹软库激活 + def activation(self) -> bool: + ack = ASFOnlineActivation(self.app_id, self.sdk_key) + return (MOK == ack or MERR_ASF_ALREADY_ACTIVATED == ack) + + # 查找相似特征人员 + def match(self, frame, features: dict) -> str: + frame = self.do_resize(frame) + + ack, detect_faces = self.face_engine.ASFDetectFaces(frame) + if MOK != ack: + raise ArcSoftCallException() + + if detect_faces.faceNum == 0: + raise FaceNotFoundException() + + if detect_faces.faceNum > 1: + raise FaceMoreFoundException() + + ack = self.face_engine.ASFProcess(frame, detect_faces, ASF_LIVENESS) + if MOK != ack: + raise ArcSoftCallException() + + ack, liveness = self.face_engine.ASFGetLivenessScore() + if MOK != ack: + raise ArcSoftCallException() + + # TODO: 是否活体(还需进一步测试) + if liveness.isLive[0] != 1: + raise NonLivingException() + + # 特征值 + single_face_info = ASF_SingleFaceInfo() + single_face_info.faceRect = detect_faces.faceRect[0] + single_face_info.faceOrient = detect_faces.faceOrient[0] + ack, feature = self.face_engine.ASFFaceFeatureExtract(frame, single_face_info) + if MOK != ack: + raise ArcSoftCallException() + + for (key, value) in features.items(): + ack, score = self.face_engine.ASFFaceFeatureCompare(feature, value) + if MOK != ack: + continue + if score >= setting.FACE_RECOGNITION_THRESHOLD: + return key + + # 捕获图像人脸特征值 + def capture(self, frame) -> List[bytes]: + frame = self.do_resize(frame) + + ack, detect_faces = self.face_engine.ASFDetectFaces(frame) + if MOK != ack: + raise ArcSoftCallException() + + data = [] + + for i in range(0, detect_faces.faceNum): + single_face_info = ASF_SingleFaceInfo() + single_face_info.faceRect = detect_faces.faceRect[i] + single_face_info.faceOrient = detect_faces.faceOrient[i] + ack, feature = self.face_engine.ASFFaceFeatureExtract(frame, single_face_info) + if MOK == ack: + data.append(feature.get_feature_bytes()) + + return data if len(data) > 0 else None + + # 二进制转特征值 + @staticmethod + def b2f(bin: bytes) -> ASF_FaceFeature: + face_feature = ASF_FaceFeature() + face_feature.featureSize = bin.__len__() + face_feature.feature = lib_func.malloc(face_feature.featureSize) + lib_func.memcpy(face_feature.feature, bin, face_feature.featureSize) + return face_feature + + def __exit__(self, type, value, trace): + self.face_engine.ASFUninitEngine() + + # 处理图片宽域 + def do_resize(self, frame): + sp = frame.shape + return cv2.resize(frame, (sp[1] // 4 * 4, sp[0])) \ No newline at end of file diff --git a/helper/gen_num.py b/helper/gen_num.py new file mode 100644 index 0000000..c4eddb4 --- /dev/null +++ b/helper/gen_num.py @@ -0,0 +1,7 @@ + +import random +import string + +def genRandomStr(instance, N = 4): + res = ''.join(random.choices(string.ascii_uppercase + string.digits, k=N)) + return res \ No newline at end of file diff --git a/helper/log.py b/helper/log.py new file mode 100644 index 0000000..0e50291 --- /dev/null +++ b/helper/log.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# encoding: utf-8 +""" +@author: tx +@file: log.py +@time: 2023/6/6 17:03 +@desc: +""" +import json + +from fastapi import Request +from functools import wraps +import inspect + +from helper.logger import logger +from models.log import Log + +def _parse_func_doc(info): + delimiter = ":param" if ":param" in info else ":return" + result = info.split(delimiter)[0].strip() + return result + +def logger_wrapper(func): + @wraps(func) + async def wrapper(*args, **kwargs): + request = kwargs.get("request") + if not request or not hasattr(request.state, "current_user"): + interface_user_name, interface_user_id = "", None + else: + current_user = request.state.current_user + interface_user_name, interface_user_id = current_user.name, current_user.id + + interface_desc = _parse_func_doc(func.__doc__) + + response = await func(*args, **kwargs) + response_data = json.loads(response.body) + + response_msg = response_data.get("desc") + record = { + "user_id": interface_user_id, + "users": interface_user_name, + "kind": interface_desc, + "comment": f"访问:{interface_desc}, 返回信息:{response_msg}" + } + await record_log(**record) + + return response + + return wrapper + + +async def record_log(**kwargs): + await Log.create(**kwargs) + + logger.info(f"{kwargs.get('users')} {kwargs.get('comment')}") + diff --git a/helper/logger.py b/helper/logger.py new file mode 100644 index 0000000..57d64c4 --- /dev/null +++ b/helper/logger.py @@ -0,0 +1,366 @@ +""" 日志指定目录扫描以及切割, 有效文件名格式如下: + 包含一个.的日志文件: [^.]+.log + 其他忽略 +""" + +import os +import fnmatch +import shutil +import time +import datetime +import logging +import warnings +import requests + +from logging import FileHandler, StreamHandler +from conf import setting + +from .common import Singleton, str2int + + +MAXSIZE = 200 * 1024 * 1024 + + +def check_and_rotate_logfile(dirname, logger, maxsize=10 * 1024 * 1024, maxtime=30 * 24 * 3600): + """每周定时切换日志 + NOTE: 如果日志文件大小小于10MB, 则不进行切换操作 + """ + if not os.path.exists(dirname): + logger.error(f'日志检查: 日志目录({dirname})不存在, 请检查') + return + + # 1. 切割日志 + now = datetime.datetime.now() + date_str = '{:04d}{:02d}{:02d}{:02d}{:02d}{:02d}'.format(now.year, now.month, now.day, now.hour, now.minute, now.second) + for filename in os.listdir(dirname): + # a. 拷贝 + filepath = dirname + os.sep + filename + if not os.path.isfile(filepath): + continue + if filename.count('.') > 1: + continue + if not fnmatch.fnmatchcase(filename, '*.log'): + continue + filesize = os.path.getsize(filepath) + if filesize <= maxsize: # 10MB以下文件不切割 + continue + new_filepath = dirname + os.sep + filename.rsplit('.log', 1)[0] + '.' + date_str + '.log' + shutil.copyfile(filepath, new_filepath) + # b. 清空 + with open(filepath, 'r+', encoding='utf8') as fd: + fd.seek(0) + fd.truncate() + + # 2. 删除老日志 + bretime = time.time() - maxtime # 30天前 + for filename in os.listdir(dirname): + filepath = dirname + os.sep + filename + if not os.path.isfile(filepath): + continue + if fnmatch.fnmatchcase(filename, '*.tar.gz'): # 清理压缩包 + os.remove(filepath) + continue + if not fnmatch.fnmatchcase(filename, '*.log'): + continue + if filename.count('.') <= 1: + continue + try: + if os.path.getmtime(filepath) <= bretime: + os.remove(filepath) + logger.exception(f'日志检查: 删除日志{filepath}成功') + except Exception as e: + logger.exception(f'日志检查: 删除日志{filepath}失败, msg: {e}') + + +def join_multiple_files(logger, filepaths, destfilepath, needsize, date_str): + """join_multiple_files: 从filepaths中提取needsize大小的数据并append到destfilepath中 + + :param filepaths: 按照时间从老到新的有序文件路径列表 + :param destfilepath: 目标文件 + :param needsize: 需要提取的数据大小 + :param date_str: 时间字符串 + """ + _min_size = 1024 * 1024 + for filepath in reversed(filepaths): + # 获取源文件(截取拷贝等操作) + need_delete = False + filesize = os.path.getsize(filepath) + if filesize == needsize: + _tmp_filepath = filepath + needsize = 0 + elif filesize < needsize: + _tmp_filepath = filepath + needsize = int((needsize - filesize) / _min_size) + else: + _tmp_filepath = filepath + f'.tmp{date_str}' + skip_block = int((filesize - needsize) / _min_size) # 跳过块, 没块大小1MB + cmd = f'dd if={filepath} of={_tmp_filepath} skip={skip_block} ibs={_min_size} obs={_min_size}' + needsize = 0 + val = os.system(cmd) + if val: + cmd = f'dd if={filepath} of={_tmp_filepath} skip={skip_block} ibs=1M obs=1M' + val = os.system(cmd) + if val: + msg = f'执行命令:{cmd}失败, code: {val}' + logger.error(msg) + return False, msg + need_delete = True + + # 拼接和删除临时文件 + try: + cmd = f'cat {_tmp_filepath} >> {destfilepath}' + val = os.system(cmd) + if val: + msg = f'执行命令:{cmd}失败, code: {val}' + logger.error(msg) + return False, msg + finally: + if need_delete: + os.remove(_tmp_filepath) + + # 判断needsize大小 + logger.info(f'成功拷贝文件:{filepath}中的内容到:{destfilepath}中') + if needsize <= 0: + break + + return True, destfilepath + + +def zip_and_transform_logfile(dirname, logger, fileprefix='yanei', minsize=MAXSIZE, tarfile=None): + """zip_and_transform_logfile, 打包获取日志文件中最近20MB(传入参数)的数据并返回 + + :param dirname: 日志目录 + :param logger: 日志记录对象 + :param fileprefix: 日志文件前缀 + :param minsize: 截取的日志文件大小 + :param tarfile: 如果该值存在, 则会 + + NOTE: 该函数仅仅适用于Linux系统 + """ + if not os.path.exists(dirname): + msg = f'日志检查: 日志目录({dirname})不存在, 请检查' + logger.error(msg) + return False, msg + + # 获取当前所有server.log日志以及自动备份的日志: yanei.20211026160000.log + can_cutout_filepaths = [] + current_filename = f'{fileprefix}.log' # 当前正在记录日志的文件 + current_filepath = dirname + os.sep + current_filename + for filename in os.listdir(dirname): + filepath = dirname + os.sep + filename + if not os.path.isfile(filepath): + continue + if fnmatch.fnmatchcase(filename, '*.tar.gz'): + continue + if not fnmatch.fnmatchcase(filename, f'{fileprefix}*'): + continue + if filename == current_filename: + continue + can_cutout_filepaths.append(filepath) + _can_cutout_filepaths = [] + + for filepath in can_cutout_filepaths: + try: + x = str2int(filepath.split('.')[1:2][0]) + if not isinstance(x, int): + api_logger.error(f'发现一个未知格式的文件:{filepath}, 忽略该文件') + continue + except Exception as msg: + api_logger.error(f'发现一个未知格式的文件:{filepath}, 忽略该文件') + else: + _can_cutout_filepaths.append(filepath) + can_cutout_filepaths = _can_cutout_filepaths + can_cutout_filepaths = sorted(can_cutout_filepaths, key=lambda x: str2int(x.split('.')[1:2][0])) + + # 截取并拼接文件 + current_filesize = os.path.getsize(current_filepath) + _min_size = 1024 * 1024 + now = datetime.datetime.now() + date_str = '{:04d}{:02d}{:02d}{:02d}{:02d}{:02d}'.format(now.year, now.month, now.day, now.hour, now.minute, now.second) + save_filename = f'server.log.tmp.{date_str}' + save_filepath = dirname + os.sep + save_filename + if current_filesize > minsize: + skip_block = int((current_filesize - minsize) / _min_size) # 跳过块, 没块大小1MB + cmd = f'dd if={current_filepath} of={save_filepath} skip={skip_block} ibs={_min_size} obs={_min_size}' + val = os.system(cmd) + if val: + cmd = f'dd if={current_filepath} of={save_filepath} skip={skip_block} ibs=1M obs=1M' + val = os.system(cmd) + if val: + msg = f'执行命令:{cmd}失败, code: {val}' + logger.error(msg) + return False, msg + logger.info(f'日志文件足够大, 截取:{current_filepath}到{save_filepath}中成功') + elif current_filesize == minsize: + save_filepath = shutil.copyfile(current_filepath, save_filepath) + logger.info(f'日志文件大小刚好, 拷贝:{current_filepath}到{save_filepath}中成功') + else: + save_filepath = shutil.copyfile(current_filepath, save_filepath) + logger.info(f'日志文件大小较小, 先拷贝:{current_filepath}到{save_filepath}中成功') + needsize = int((minsize - current_filesize) / _min_size) + succ, msg = join_multiple_files(logger, can_cutout_filepaths, save_filepath, needsize, date_str) + if not succ: + return False, msg + + # 打包, 其中压缩包文件会在定时任务函数中清理 + tar_filename = f'YANEILog_.{date_str}.tar.gz' if not tarfile else tarfile + tar_filepath = [dirname, tar_filename] + try: + cmd = f'cd {dirname} && tar zcf {tar_filename} {save_filename}' + val = os.system(cmd) + if val: + msg = f'执行命令:{cmd}失败, code: {val}' + logger.error(msg) + return False, msg + finally: + os.remove(save_filepath) + + return True, tar_filepath + + +class YaneFilter(logging.Filter): + """自定义logging过滤器, 通过redis或者mysql获取当前记录的最新日志level, 根据 + 该level决定是否记录某一条日志 + """ + + def filter(self, record): + return super(YaneFilter, self).filter(record) + + +class YaneiErrorFilter(logging.Filter): + """自由控制: 记录error以上的日志""" + + def filter(self, record): + try: + return record.levelno >= logging.ERROR + except Exception: + return super(YaneiErrorFilter, self).filter(record) + + +@Singleton +class YaneiLoggerGenerator: + DEFAULT_LOG_FILE = 'server.log' # 默认日志存储文件 + DEFAULT_ERROR_LOG_FILE = 'error.log' + + """ 日志logger生成工具 + NOTE: 非多进程安全, 并发量高的情况下可能造成日志丢失 + + @默认格式: 日志等级 时间 日志所在文件名:日志所在行号 - 固定前缀 - 日志信息 + @生成logger或获取某一个logger: + from app.common.log import g_logger_generator + + # 表示获取api日志对象, 日志会记录到api.log, 固定前缀为API + app.apiLogger = g_logger_generator.get_logger('api', 'api.log', prefix='API') + + # 表示获取切换日志对象, 日志会记录到默认文件server.log, 固定前缀为SWITCH + app.celeryLogger = g_logger_generator.get_logger('switch', prefix='SWITCH') + + # 表示获取文件同步日志对象, 日志会记录到默认文件server.log, 固定前缀为DELAY + app.delayLogger = g_logger_generator.get_logger('delay', prefix='DELAY') + """ + + def __init__(self): + self._loggers = {} # 当前进程中logger集: name -> logger + self.common_fmt_prefix = '%(levelname)s %(process)d:%(thread)d %(asctime)s %(filename)s:%(lineno)s' + + # 关闭requests, urllib3告警日志 + requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning) + warnings.simplefilter('ignore', ResourceWarning) + + def _formatter(self, fmt, prefix): + return logging.Formatter(fmt + f' - {prefix} - ' + '%(message)s') + + def update_file_mode(self, filepath, mode=0o666): + if os.path.exists(filepath): + status = os.stat(filepath) + if (status.st_mode & mode) == mode: + return + + old_umask = os.umask(0) + with open(filepath, 'w', encoding='utf8') as fd: + fd.write('.') + os.chmod(filepath, mode) + os.umask(old_umask) + + def get_logger(self, name, filename=None, level=logging.INFO, prefix='COMMON', stream=True): + """get_logger, 获取name指定的日志句柄对象 + + :param name:logger名称 + :param filename:日志存储的文件名 + :param level:日志记录等级 + :param prefix:日志记录前缀, 用以区分同一个日志文件中的不同日志信息 + :param stream:是否日志打印到stdout, 其中celery异步任务中的日志会自动重定向到标准输出中, 不需要重新设置 + 如果本地开启调试, 则可以传递该值为True, 确保在本地开发时将日志打印到stdout + + @return: 返回一个{name}Logger的logger对象 + """ + + if name in self._loggers: + return self._loggers[name] + + if not os.path.exists('logs'): + os.makedirs('logs') + filename = self.DEFAULT_LOG_FILE if not filename else filename + + my_formatter = self._formatter(self.common_fmt_prefix, prefix) + streamHandler = StreamHandler() + streamHandler.setFormatter(my_formatter) + _filename = f'logs{os.sep}{filename}' + self.update_file_mode(os.path.join(setting.BASE_DIR, _filename)) + + logHandler = FileHandler(_filename) + logHandler.suffix = '%Y-%m-%d.%M-%S.log' + logHandler.setFormatter(my_formatter) + + my_logger = logging.getLogger(name) + if not my_logger.handlers: + my_logger.addHandler(logHandler) + my_logger.setLevel(level) + if stream: + my_logger.addHandler(streamHandler) + + self._loggers[name] = my_logger + return my_logger + + def update_default_logger(self, logger, filename=None, level=logging.INFO, prefix='COMMON', clear=False, stream=True): + """更新某一个logger的日志配置, 参数参考get_logger""" + if logger.name not in self._loggers: + self._loggers[logger.name] = logger + + filename = self.DEFAULT_LOG_FILE if not filename else filename + + # NOTE: 删除logger默认的handler, 如果需要则删除下面功能 + if clear: + for old_handler in logger.handlers: + logger.removeHandler(old_handler) + + my_formatter = self._formatter(self.common_fmt_prefix, prefix) + streamHandler = StreamHandler() + streamHandler.setFormatter(my_formatter) + if stream: + logger.addHandler(streamHandler) + + _filename = f'logs{os.sep}{filename}' + self.update_file_mode(os.path.join(setting.BASE_DIR, _filename)) + logHandler = FileHandler(_filename) + logHandler.suffix = '%Y-%m-%d.%M-%S.log' + logHandler.setFormatter(my_formatter) + logger.addHandler(logHandler) + + # 增加一个单独记录错误日志的handler(不需要重定向到标准输出) + _filename = f'logs{os.sep}{self.DEFAULT_ERROR_LOG_FILE}' + self.update_file_mode(os.path.join(setting.BASE_DIR, _filename)) + errorhandler = FileHandler(_filename) + errorhandler.suffix = '%Y-%m-%d.%M-%S.log' + errorhandler.setFormatter(my_formatter) + errorhandler.addFilter(YaneiErrorFilter()) + logger.addHandler(errorhandler) + + logger.setLevel(logging.INFO) + + return logger + + +__g_logger_generator = YaneiLoggerGenerator() +api_logger = __g_logger_generator.get_logger('api', filename='api.log', level=setting.YANEI_LOG_LEVEL, prefix='API', stream=True) +logger = __g_logger_generator.get_logger('server', level=setting.YANEI_LOG_LEVEL, prefix='SERVER', stream=True) diff --git a/helper/mask.py b/helper/mask.py new file mode 100644 index 0000000..df4c130 --- /dev/null +++ b/helper/mask.py @@ -0,0 +1,8 @@ +from enum import IntEnum + +class Mask(IntEnum): + READ = 1 + CREATE = 3 + UPDATE = 5 + DELETE = 9 + CRUD = 15 \ No newline at end of file diff --git a/helper/module.py b/helper/module.py new file mode 100644 index 0000000..cdfeb85 --- /dev/null +++ b/helper/module.py @@ -0,0 +1,9 @@ +from enum import Enum +from collections import namedtuple + +ModuleItem = namedtuple('ModuleItem', ('id', 'name', 'rank', 'options')) + +class Module(Enum): + USER = ModuleItem(1001, '用户管理', 1, {'下载': 1, '编辑': 9}) + ROLE = ModuleItem(1002, '角色管理', 2, {}) + TEMPLATE = ModuleItem(1003, '模板管理', 3, {}) \ No newline at end of file diff --git a/helper/outside_uhf_reader.py b/helper/outside_uhf_reader.py new file mode 100644 index 0000000..1a84baf --- /dev/null +++ b/helper/outside_uhf_reader.py @@ -0,0 +1,75 @@ +from helper.SerialPort import * +from collections import Counter +import time +import threading +import binascii + +# 移植来自RMS3.0的超高频SimpleRFIDReader方法中的超高频(UHF)部分 +class OutSideUHFReader: + def __init__(self, com="/dev/ttyUSB0"): + self.seria = None + self.rfidData='' + self.taskKillFlag=True + self.noCount=0 + self.bqz='' + try: + self.seria = SerialPort(com, 115200,0.1) + print('小型FID初始化成功') + except Exception as ex: + print('小型FID初始化失败:' + str(ex)) + + # 读取 + def read(self): + self.seria.Write(bytes.fromhex("A0 04 FF 89 01 D3")) + while True: + data = self.seria.Read() + if data == None: + continue + else: + break + val = str(binascii.b2a_hex(data).decode()) + # print('valvalval:',val) + if len(val) == 66: + # 不同的超高频的标签的UID位置可能不一样!(注意) + bq = val[14:38] + self.bqz=bq + self.noCount=0 + else: + self.noCount+=1 + if(self.noCount>=3): + self.bqz = "" + # print(bqz) + return self.bqz + + def getData(self): + return self.rfidData + + def readDataThread(self): + while not(self.taskKillFlag): + try: + dataStr=self.read() + if(dataStr): + self.rfidData=dataStr + else: + self.rfidData='' + time.sleep(0.1) + except Exception as e: + print(str(e)) + + # 启动服务 + def start(self): + self.rfidData='' + self.taskKillFlag=False + if self.seria.ser: + p = threading.Thread(target=self.readDataThread) + p.start() + else: + print("打开串口设备失败!") + + # 停止服务 + def stop(self): + self.taskKillFlag=True + self.rfidData='' + +rfid_reader = OutSideUHFReader() +rfid_reader.start() diff --git a/helper/report_excel.py b/helper/report_excel.py new file mode 100644 index 0000000..9f96e80 --- /dev/null +++ b/helper/report_excel.py @@ -0,0 +1,330 @@ +#!/usr/bin/env python +# -*- encoding: utf-8 -*- +''' +@Date:2022/08/27 11:05:06 +''' +import sys +sys.path.append('.') +import string +import os +import datetime +from openpyxl.workbook import Workbook +from openpyxl.styles import Alignment, Side, Border, Font, PatternFill + +class ReportData: + def __init__(self): + + # 新建一个workbook + self.wb = Workbook() + # 定义设置样式 + self.a = 0 + + def set_style(self, title='sheet1'): + # 如果是第一次调用则删除默认创建的sheet表 + if self.a == 0: + self.wb.remove(self.wb['Sheet']) + self.a += 1 + # 创建第几个sheet + self.ws = self.wb.create_sheet() + # 设置sheet的标题 + self.ws.title = title + # 设置1 2 3 行固定的高度 + self.ws.row_dimensions[1].height = 35 + self.ws.row_dimensions[2].height = 25 + self.ws.row_dimensions[3].height = 28 + # 水平对齐, 居中对齐 + self.alignment_style = Alignment( + horizontal='center', vertical='center') + # 定义Border边框样式 + left, right, top, bottom = [Side(style='thin', color='000000')] * 4 + self.border_style = Border( + left=left, right=right, top=top, bottom=bottom) + # 设置列宽 + # 生成前14个大写字母 ascii_uppercase生成所有大写字母 + self.upper_string = string.ascii_uppercase[:15] + # 定义字体 + self.font_size = Font(size=9) + for col in self.upper_string: + self.ws.column_dimensions[col].width = 20 + + # 创建第一行 + def create_row1(self, value, length): + # 合并第1行1-14列单元格 + self.ws.merge_cells(start_row=1, end_row=1, + start_column=1, end_column=length) + # 写入值 + self.ws.cell(row=1, column=1).value = value + self.ws['A1'].alignment = self.alignment_style + self.ws['A1'].font = Font(size=16, bold=True) + self.create_row2(length) + + def create_row2(self, length): + # 第2行写入值 + now_time = datetime.datetime.now().strftime('%Y%m%d %H%M') + # 格式化时间为中文年月日 + now_time = now_time[:4] + '年' + now_time[4:6] + '月' + \ + now_time[6:8] + '日' + now_time[8:11] + '时' + now_time[11:13] + '分' + self.ws.cell(row=2, column=1).value = '报表导出时间:{}'.format(now_time) + self.ws.cell(row=2, column=3).value = '终端系统版本:3.1' + self.ws.cell(row=2, column=5).value = '' + self.ws.cell(row=2, column=7).value = '报表导出位置:终端' + # 合并单元格 + x = 1 + while x < 6: + self.ws.merge_cells(start_row=2, end_row=2, + start_column=x, end_column=x + 1) + x += 2 + self.ws.merge_cells(start_row=2, end_row=2, + start_column=7, end_column=length) + # 遍历取1, 3, 5, 7为第二行添加样式 + for x in range(1, 8, 2): + self.ws.cell(row=2, column=x).font = Font(size=9) + # 水平对齐 垂直对齐 + self.ws.cell(row=2, column=x).alignment = self.alignment_style + if x < 7: + # 边框样式 + self.ws.cell(row=2, column=x).border = self.border_style + self.ws.cell(row=2, column=x + 1).border = self.border_style + else: + # 因为第2行第7-14列是合并的单元格 所以需要循环添加边框样式 + while x < length + 1: + self.ws.cell(row=2, column=x).border = self.border_style + x += 1 + + # 创建第三行, 需要传入一个列表用来写入单元格的值 + def create_row3(self, data_list): + for data in range(1, len(data_list) + 1): + # 第三行写入值 + self.ws.cell(row=3, column=data).value = data_list[data - 1] + # 设置文本水平居中, 垂直居中 + self.ws.cell(row=3, column=data).alignment = self.alignment_style + # 设置字体加粗 + self.ws.cell(row=3, column=data).font = Font(bold=True, size=9) + # 背景颜色 + self.ws.cell(row=3, column=data).fill = PatternFill( + fill_type='solid', fgColor='EE9A49') + # 边框样式 + self.ws.cell(row=3, column=data).border = self.border_style + + # 创建多行固定样式 start_row从第几行开始有规律 传入一个数据库获取的列表对象的值, 传入一个数据对象keys列表 + def create_multiple_rows(self, start_row, data_list, keys_list): + # 从第4行创建 + for row_number in range(start_row, len(data_list) + start_row): + # 设置每一行的行高 + self.ws.row_dimensions[row_number].height = 18 + # 遍历每一个对象的长度 + col_ = 1 + for key_ in keys_list: + # 写入值 + try: + # 判断是否时datetime 类型, + if isinstance(dict(data_list[row_number - start_row]).get(key_), datetime.datetime): + value = dict(data_list[row_number - start_row]).get(key_).replace(tzinfo=None) + # 判断是否时state + elif key_=='state': + value = dict(data_list[row_number - start_row]).get(key_).value + else: + value = dict(data_list[row_number - start_row]).get(key_) + except : + value = "/" + self.ws.cell(row=row_number, column=col_).value = value + # 设置文本水平居中, 垂直居中 + self.ws.cell(row=row_number, + column=col_).alignment = self.alignment_style + # 设置边框样式 + self.ws.cell(row=row_number, + column=col_).border = self.border_style + # 设置字体大小 + self.ws.cell(row=row_number, column=col_).font = Font(size=9) + col_ += 1 + + # 保存文件 + def save(self, file_path): + self.wb.save('{}.xlsx'.format(file_path)) + self.close() + + # 关闭文件 + def close(self): + self.wb.close() + + # 替换空白 + def replace_space(self, length): + self.max_lines = self.ws.max_row + upper_letter_str = string.ascii_uppercase[:length] + for upper_letter in upper_letter_str: + print(upper_letter) + for row_ in range(1, self.max_lines + 1): + b = self.ws[upper_letter + str(row_)].value + # print(b) + if not b: + self.ws[upper_letter + str(row_)].value = 'null' + + # 编辑excel表格中列药剂状态1 2 3 替换为在库 出库 + def editor_status(self, col_): + self.max_lines = self.ws.max_row + for row_ in range(4, self.max_lines + 1): + b = self.ws[col_ + str(row_)].value + if b == 1: + self.ws[col_ + str(row_)].value = '在库' + elif b == 0: + self.ws[col_ + str(row_)].value = '待入库' + elif b == 2: + self.ws[col_ + str(row_)].value = '出库' + elif b == 3: + self.ws[col_ + str(row_)].value = '空瓶' + elif b == 4: + self.ws[col_ + str(row_)].value = '销毁' + # 编辑 温度报警类型 0,1 替换为正常和异常 + def eidtor_temperature_type(self,col_): + self.max_lines = self.ws.max_row + for row_ in range(4,self.max_lines +1): + b = self.ws[col_ + str(row_)].value + if b==0: + self.ws[col_ + str(row_)].value = "正常" + elif b ==1 : + self.ws[col_ + str(row_)].value = "异常" + # 编辑药剂重点监管列为1 0 替换为是 否 + def editor_isSupervise(self, col_): + self.max_lines = self.ws.max_row + for row_ in range(4, self.max_lines + 1): + b = self.ws[col_ + str(row_)].value + if b == 1: + self.ws[col_ + str(row_)].value = '是' + elif b == 0: + self.ws[col_ + str(row_)].value = '否' + + # 编辑操作类型 1 2 3 入库 领用 归还 + def editor_RecordType(self, col_): + self.max_lines = self.ws.max_row + for row_ in range(4, self.max_lines + 1): + b = self.ws[col_ + str(row_)].value + if b == 1: + self.ws[col_ + str(row_)].value = '入库' + elif b == 2: + self.ws[col_ + str(row_)].value = '领用' + elif b == 3: + self.ws[col_ + str(row_)].value = '归还' + + # 替换sqlAlchemy时间格式 + def editor_time(self, keys_list, params): + self.max_lines = self.ws.max_row + for row_ in range(4, self.max_lines + 1): + col_ = string.ascii_uppercase[keys_list.index(params)] + b = self.ws[col_ + str(row_)].value + # print('sssssssssssssssssssssssssssssssssssssssssassssssssssssssss:::', b,type(b)) + try: + if len(b) == 19: + self.ws[col_ + str(row_)].value = b.replace('T', ' ') + except Exception as e : + pass + # print(e) + + # 编辑用户管理列为1 0 替换为正常 禁用 + def editor_user_status(self, col_): + self.max_lines = self.ws.max_row + for row_ in range(4, self.max_lines + 1): + b = self.ws[col_ + str(row_)].value + if b == 1: + self.ws[col_ + str(row_)].value = '正常' + elif b == 0: + self.ws[col_ + str(row_)].value = '禁用' + + # 构建文件内容 + def build_file(self,**kwargs): + data_list = kwargs.pop("data_list") + key_list = kwargs.pop("key_list") + finds_name = kwargs.pop("finds_name") + self.set_style(title="Sheet") + file_name = kwargs.pop("file_name") + + self.create_row1(file_name, len(finds_name)) + + self.create_row3(finds_name) + + self.create_multiple_rows(4, data_list, key_list) + + for i in key_list: + if "date" in i: + self.editor_time(key_list, i) + if "state" in i: + self.editor_status(string.ascii_uppercase[key_list.index(i)]) + if "temperature_type" in i: + self.eidtor_temperature_type(string.ascii_uppercase[key_list.index(i)]) + if "is_supervise" in i: + self.editor_isSupervise(string.ascii_uppercase[key_list.index(i)]) + if "record_type" == i: + self.editor_RecordType( + string.ascii_uppercase[key_list.index(i)]) + return self + + # 下载文件 + + @staticmethod + def download_excel(filename, chunk_size=512): + try: + f = open(filename, 'rb') + except: + return + while True: + c = f.read(chunk_size) + if c: + yield c + else: + f.close() + os.remove(filename) + break + + + +# # 导出用户数据报表接口 +# @dataReport.route('/downloadUserReport', methods=["GET", "POST"]) +# def downloadUserReport(): +# try: +# uPath = Utils.getUDiskPath() +# if (uPath == ''): +# return jsonify(Utils.resultData(1, '未检测到U盘!')) +# else: +# data_user_list = BllUser().findList().all() +# data_user_list = json.loads( +# Utils.resultAlchemyData(data_user_list)) + +# # 创建一个报表类实例 +# a = ReportData() +# a.set_style(title='Sheet') +# data_list = ['工号', '角色', '姓名', '性别', 'QQ', '手机', '邮箱', +# '条码', '状态', '最后一次登录时间', ] +# a.create_row1('用户角色数据统计表', len(data_list)) +# a.create_row3(data_list) +# keys_list = ['UserCode', 'RoleName', 'RealName', 'Sex', 'QQ', 'Mobile', 'Email', +# 'BarCode', 'IsEnabled', 'LastVisitDate', ] +# a.create_multiple_rows(4, data_user_list, keys_list) +# a.replace_space(len(data_list)) +# a.editor_time(keys_list, 'LastVisitDate') +# # 判断用户角色 +# for row_ in range(4, a.max_lines + 1): +# col_ = string.ascii_uppercase[keys_list.index('UserCode')] +# b = a.ws[col_ + str(row_)].value +# if b == 'admin': +# for col_value in range(1, len(keys_list) + 1): +# a.ws.cell(row=row_, column=col_value).fill = PatternFill( +# fill_type='solid', fgColor='FF0000') +# elif b == 'yanyi': +# for col_value in range(1, len(keys_list) + 1): +# a.ws.cell(row=row_, column=col_value).fill = PatternFill( +# fill_type='solid', fgColor='FFFF00') + +# # 优化用户性别 +# for row_ in range(4, a.max_lines + 1): +# col_ = string.ascii_uppercase[keys_list.index('Sex')] +# b = a.ws[col_ + str(row_)].value +# if b == 0: +# a.ws[col_ + str(row_)].value = '女' +# else: +# a.ws[col_ + str(row_)].value = '男' +# a.editor_user_status('I') +# file_name = '用户角色数据统计表{}'.format(Utils.UUID()) +# a.save(uPath + '/' + file_name) +# returnData = Utils.resultData(0, '导出成功') +# return jsonify(returnData) +# except Exception as e: +# return jsonify(Utils.resultData(2, str(e))) diff --git a/helper/simsun.ttc b/helper/simsun.ttc new file mode 100644 index 0000000..5f22ce3 Binary files /dev/null and b/helper/simsun.ttc differ diff --git a/helper/tool.py b/helper/tool.py new file mode 100644 index 0000000..f2359f8 --- /dev/null +++ b/helper/tool.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +# encoding: utf-8 +""" +@author: tx +@file: tool.py +@time: 2023/7/4 19:37 +@desc: +""" +import os +import socket +import time +from datetime import datetime + +import pygame + + +def parse_datetime(time_str, trans_str='%Y-%m-%d %H:%M:%S'): + """ + 时间日期格式转化 + :param time_str: + :param trans_str: 转义输出格式 + :return: + """ + possible_formats = [ + "%Y-%m-%d %H:%M:%S", + "%Y/%m/%d %H:%M:%S", + "%Y-%m-%d", + "%Y/%m/%d", + "%d-%m-%Y %H:%M:%S", + "%d/%m/%Y %H:%M:%S", + "%d-%m-%Y", + "%d/%m/%Y", + "%Y年%m月%d日", + "%Y年%m月%d日 %H时%M分", + "%Y年%m月%d日 %H时%M分%S秒", + "%Y-%m-%dT%H:%M:%S.%f", + "%Y-%m-%dT%H:%M:%S.%f%z", + "%Y-%m-%dT%H:%M:%S%z", + "%Y-%m-%d %H:%M:%S.%f", + "%Y-%m-%d %H:%M:%S.%f%z", + ] + + datetime_list = [] + for format_str in possible_formats: + try: + dt = datetime.strptime(time_str, format_str) + datetime_list.append(dt) + except ValueError: + pass + if datetime_list: + result = datetime_list[0].strftime(trans_str) + return result + else: + return time_str + + +def get_ip_address(): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(('8.8.8.8', 1)) + ip = s.getsockname()[0] + finally: + s.close() + return ip + +def play_sound(sound_name): + """ + 播放音频文件 + :param sound_name: + :return: + """ + pygame.mixer.init(frequency=15500, size=-16, channels=4) + if os.path.exists(os.path.join("static", "sounds", sound_name)): + track2 = pygame.mixer.Sound(os.path.join(os.getcwd(), "static", "sounds", sound_name)) + track2.play() + time.sleep(2.5) diff --git a/helper/tools.py b/helper/tools.py new file mode 100644 index 0000000..8c6b57e --- /dev/null +++ b/helper/tools.py @@ -0,0 +1,12 @@ +import random +import string + +def genRandomStr(N = 7): + res = ''.join(random.choices(string.ascii_uppercase + string.digits, k=N)) + return res + + +async def genNum(instance): + instance.num = genRandomStr(4) + instance.tag = "123" + await instance.save() \ No newline at end of file diff --git a/helper/usb.py b/helper/usb.py new file mode 100644 index 0000000..b85e384 --- /dev/null +++ b/helper/usb.py @@ -0,0 +1,46 @@ +import getpass +import glob +import platform +from io import BytesIO +from os import path + +import psutil + +system_name = platform.system() + + +class DeviceNotFound(Exception): + pass + + +def fetch(): + """获取U盘中的Excel文件""" + if system_name == 'Windows': + usbs = [disk.device for disk in psutil.disk_partitions() if disk.opts == 'rw,removable'] + if not usbs: + raise DeviceNotFound() + usb = usbs[0] + files = glob.glob(f'{usb}/*.xlsx') + glob.glob(f'{usb}/*.xls') + return [{"name": path.basename(f), "file": f} for f in files] + + usbs = [item for item in psutil.disk_partitions() if item.mountpoint.startswith(f'/media/{getpass.getuser()}')] + if not usbs: + raise DeviceNotFound() + usb = usbs[0] + files = glob.glob(f'{usb.mountpoint}/*.xlsx') + glob.glob(f'{usb.mountpoint}/*.xls') + return [{"name": path.basename(f), "file": f} for f in files] + + +def put_in(filename: str, bin: BytesIO): + if system_name == 'Windows': + usbs = [disk.device for disk in psutil.disk_partitions() if disk.opts == 'rw,removable'] + if not usbs: + raise DeviceNotFound() + path_file = path.join(usbs[0], filename) + else: + usbs = [item for item in psutil.disk_partitions() if item.mountpoint.startswith(f'/media/{getpass.getuser()}')] + if not usbs: + raise DeviceNotFound() + path_file = path.join(usbs[0].mountpoint, filename) + with open(path_file, 'wb') as f: + f.write(bin.getbuffer()) diff --git a/helper/utils.py b/helper/utils.py new file mode 100644 index 0000000..d1705e8 --- /dev/null +++ b/helper/utils.py @@ -0,0 +1,109 @@ +# -*- coding:utf-8 -*- +""" +@Created on : 2023/7/24 10:25 +@Author: hxl +@Des: +""" +import hashlib +import os +import platform +from collections import namedtuple +from datetime import datetime + +import psutil +from pytz import timezone + +from conf import setting + + +def ostruct(kv: dict): + return namedtuple('OpenStruct', ' '.join(kv.keys()))(**kv) + + +def isBlank(term): + return not (term and term.strip()) + + +def timezone_now(): + zone = timezone(setting.TIMEZONE) + return datetime.now(zone) + + +def rfid_reverse(rfid): + """ + 读卡器反转 + """ + if setting.RFID_REVERSE: + rfid = "".join(reversed([rfid[i:i + 2] for i in range(0, len(rfid), 2)])) + rfid = rfid.upper() + return rfid + + +def encrypt_md5(text): + # 创建一个 MD5 对象 + md5 = hashlib.md5() + + # 更新哈希对象的输入值,需要将字符串编码为字节型再进行更新 + md5.update(text.encode('utf-8')) + + # 计算并返回哈希值的十六进制表示 + return md5.hexdigest() + + +# 判断当前系统是linux还是windows +system_name = platform.system() + + +# 获取当前插入U盘路径 +def get_UDisk_path(): + if system_name == 'Windows': + disk_list = psutil.disk_partitions() + # 获取U盘路径 + u_path = [disk.device for disk in disk_list if disk.opts == 'rw,removable'] + if u_path: + return u_path[0] + elif system_name == "Linux": + r = os.popen('ls -a /media/yanyi') + text = r.read() + r.close() + udisklist = text.splitlines() + if (len(udisklist) >= 3): + return '/media/yanyi/' + udisklist[2] + return "" + + +# 创建文件夹 +def mkdir(path): + folder = os.path.exists(path) + if not folder: + os.makedirs(path) + + +# 获取配液按天自增编号 + +def get_seq_no(number): + """ + 储备液编号 = 打印标签 + 格式如:CB230728001 + """ + # zone = timezone(setting.TIMEZONE) # 时区 + # today = datetime.now(zone).strftime('%y%m%d') + today = datetime.now().strftime('%y%m%d') + + return "CB%s%03d" % (today, number) + + +# 获取数字和单位 +def number_and_unit(input_str): + """ + 获取数量和单位 + 支持如下的常见格式:12.23g/ml,12g,12.3ml等 + """ + pattern = r"(\d+\.?\d+)([a-zA-Z/]+)" # 匹配数字和单位的正则表达式 + match = re.match(pattern, input_str) + if match: + weight = match.group(1) # 获取数字部分 + unit = match.group(2) # 获取单位部分 + return weight, unit + else: + return None, None diff --git a/helper/websocket_manage.py b/helper/websocket_manage.py new file mode 100644 index 0000000..8386b75 --- /dev/null +++ b/helper/websocket_manage.py @@ -0,0 +1,83 @@ +import asyncio +import time +import uuid +from typing import List, Dict + +from fastapi.websockets import WebSocket +from starlette.websockets import WebSocketState +from websockets.exceptions import ConnectionClosedOK, ConnectionClosedError + + +class ConnectionManager: + def __init__(self): + self.active_connections: List[WebSocket] = [] + self.unacknowledged_messages: Dict[str, Dict[str, any]] = {} + async def connect(self, websocket: WebSocket): + await websocket.accept() + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + if websocket in self.active_connections: + self.active_connections.remove(websocket) + + async def broadcast_stream(self, data: bytes): + for connection in self.active_connections: + try: + await connection.send_bytes(data) + except (ConnectionClosedError, ConnectionClosedOK): + self.disconnect(connection) + + async def broadcast_json(self, event: str, data: str): + for connection in self.active_connections: + try: + await connection.send_json({"event": event, "data": data}) + except (ConnectionClosedError, ConnectionClosedOK): + self.disconnect(connection) + + async def send_message(self, event: str, data: str, message_id:str = None): + if message_id is None: + message_id = str(uuid.uuid4()) + try: + for connection in self.active_connections: + if connection.client_state == WebSocketState.CONNECTED: + await connection.send_json({"event": event, "data": data, "message_id": message_id}) + if message_id not in self.unacknowledged_messages: + self.unacknowledged_messages[message_id] = { + "message": {"event": event, "data": data, "message_id": message_id}, + "timestamp": time.time(), + "websocket": connection + } + except (ConnectionClosedError, ConnectionClosedOK): + self.disconnect(connection) + + async def handle_acknowledgment(self, message_id: str): + if message_id in self.unacknowledged_messages: + del self.unacknowledged_messages[message_id] + + async def check_unacknowledged_messages(self): + while True: + for message_id, message_info in list(self.unacknowledged_messages.items()): + current_time = time.time() + if current_time - message_info["timestamp"] > 5: # 设置超时时间为5秒 + await self.send_message( + message_info["message"]["event"], + message_info["message"]["data"], + message_info["message"]["message_id"] + ) + await asyncio.sleep(3) # 等待一段时间再次检查 + + async def handle_message(self, message:str): + message_id = message + if message_id is not None: + await self.handle_acknowledgment(message_id) + async def start_message_check(self): + self.message_check_task = asyncio.create_task(self.check_unacknowledged_messages()) + + async def stop_message_check(self): + if hasattr(self, 'message_check_task'): + self.message_check_task.cancel() + try: + await self.message_check_task + except asyncio.CancelledError: + pass +manager = ConnectionManager() diff --git a/helper/write_doc.py b/helper/write_doc.py new file mode 100644 index 0000000..d352acf --- /dev/null +++ b/helper/write_doc.py @@ -0,0 +1,157 @@ +from docx import Document +from docx.shared import Pt, Inches +from docx.enum.text import WD_ALIGN_PARAGRAPH,WD_UNDERLINE +from docx import Document +from docx.shared import Inches +from docx.oxml import OxmlElement +from docx.oxml.ns import qn +import time +from io import BytesIO +import os + +#设置表格的边框 +def set_cell_border(cell, **kwargs): + """ + Set cell`s border + Usage: + set_cell_border( + cell, + top={"sz": 12, "val": "single", "color": "#FF0000", "space": "0"}, + bottom={"sz": 12, "color": "#00FF00", "val": "single"}, + left={"sz": 24, "val": "dashed", "shadow": "true"}, + right={"sz": 12, "val": "dashed"}, + ) + """ + tc = cell._tc + tcPr = tc.get_or_add_tcPr() + + # check for tag existnace, if none found, then create one + tcBorders = tcPr.first_child_found_in("w:tcBorders") + if tcBorders is None: + tcBorders = OxmlElement('w:tcBorders') + tcPr.append(tcBorders) + + # list over all available tags + for edge in ('left', 'top', 'right', 'bottom', 'insideH', 'insideV'): + edge_data = kwargs.get(edge) + if edge_data: + tag = 'w:{}'.format(edge) + + # check for tag existnace, if none found, then create one + element = tcBorders.find(qn(tag)) + if element is None: + element = OxmlElement(tag) + tcBorders.append(element) + + # looks like order of attributes is important + for key in ["sz", "val", "color", "space", "shadow"]: + if key in edge_data: + element.set(qn('w:{}'.format(key)), str(edge_data[key])) + +class WriteDoc(object): + + def __init__(self, data, path): + self.data = data + self.path = path + self.d = Document() + section = self.d.sections[0] + header = section.header + paragraph = header.paragraphs[0] + paragraph.paragraph_format.left_indent = 0 + paragraph.paragraph_format.right_indent = 0 + now_date = time.strftime("%Y-%m-%d", time.localtime()) + text=paragraph.add_run('记录格式编号 : 实施日期 :{} 版本号/修改号 :'.format(now_date)) + text.font.size = Pt(6) + paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER + # paragraph.runs[-1].underline = WD_UNDERLINE.THICK#页眉下划线 + + + #添加列表 + def add_table(self): + for i in range(len(self.data)): + table = self.d.add_table(rows=11, cols=2, style="Table Grid") + self.merge_table(table) + self.write_data(table,i) + self.d.add_paragraph("").paragraph_format.space_after = Pt(25) + return self + + + #写入数据 + def write_data(self,table,index): + # for i in range(len(self.data)): + # table =Document.tables[i] + i=self.data[index] + try: + cell = table.cell(0, 0) + cell.text = "标准储备液名称:{}".format(i.name) + cell = table.cell(0, 1) + cell.text = "编号:{}".format(i.seq_no) + cell = table.cell(1, 0) + cell.text = "溶剂:{}".format(i.solvent) + cell = table.cell(2, 0) + cell.text = "配制日期:{}".format(i.configuration_date) + cell = table.cell(2, 1) + cell.text = "有效期:{}".format(i.validity_date) + cell = table.cell(3, 0) + cell.text = "配制浓度:{}".format(i.config_concentration) + cell = table.cell(3, 1) + cell.text = "配制量:{}".format(i.allocation) + cell = table.cell(4, 0) + cell.text = "配制依据:{}".format(i.configuration_basis) + cell = table.cell(5, 0) + cell.text = "单位编号:{}".format(i.barcode_list) + cell = table.cell(6, 0) + cell.text = "配制记录:{}".format(i.configuration_record) + cell = table.cell(10, 0) + set_cell_border( + cell, + top={"sz": 12, "val": "single", "color": "white", "space": "0"}, + right={"sz": 12, "val": "single", "color": "white", "space": "0"}, + ) + cell = table.cell(10, 1) + set_cell_border( + cell, + top={"sz": 12, "val": "single", "color": "white", "space": "0"}, + ) + cell.text = "配制人:{}".format(i.user_name) + except Exception as e: + print('') + + + # 保存文件 + def save(self, file_path): + self.d.save(file_path) + + # 关闭文件 + def close(self): + self.d.close() + + #合并单元格,修改列宽 + def merge_table(self,table): + table.cell(1,0).merge(table.cell(1,1)) + table.cell(4,0).merge(table.cell(4,1)) + table.cell(5,0).merge(table.cell(5,1)) + table.cell(6,0).merge(table.cell(9,1)) + table.cell(0,0).width=Inches(6.5) + + + # 下载文件 + @staticmethod + def download_doc(filename, chunk_size=512): + try: + f = Document(filename) + b=BytesIO() + f.save(b) + b.flush() + b.seek(0) + b_data=b.getvalue() + b.close() + os.remove(filename) + return b_data + except: + return '' + + +if __name__ == '__main__': + pass + # WriteDoc([1,2,3,4,5,6,7,8,9],'').add_table() \ No newline at end of file diff --git a/initPathData.py b/initPathData.py new file mode 100644 index 0000000..8b6e86b --- /dev/null +++ b/initPathData.py @@ -0,0 +1,87 @@ +from tortoise import Tortoise, run_async +import asyncio +import json +import os + + +cwd = os.getcwd() +PATHPaths = os.path.join(cwd, "algorithm", "Paths.json") +PATHWeights = os.path.join(cwd, "algorithm", "Weights.json") + +Paths = None +Weights = None + +with open(PATHPaths, "r") as fp: + Paths = json.load(fp) + +with open(PATHWeights, "r") as fp: + Weights = json.load(fp) + +# print("Paths: ", Paths) +# print("Weights: ", Weights) + +async def init(): + # Here we create a SQLite DB using file "db.sqlite3" + # also specify the app name of "models" + # which contain models from "app.models" + await Tortoise.init( + config={ + "connections": { + "default": "mysql://root:123456@127.0.0.1:3306/agv?charset=utf8mb4" + }, + "apps": { + "models": { + "models": ["aerich.models", "models"], + "default_connection": "default" + } + }, + 'use_tz': False, + # 'timezone': setting.TIMEZONE + } + ) + # Generate the schema + # await Tortoise.generate_schemas() + +# run_async is a helper function to run simple async Tortoise scripts. +run_async(init()) + +from models.path import Path + +async def insertData(): + cntRow = len(Weights) + #exclude itself + cntColumn = len(Weights[0]) - 1 + keyRow = sorted(Paths.keys(), key=lambda x: int(x)) + + print("cntRow: {}, cntColumn: {}".format(cntRow, cntColumn)) + + for idxRow in range(0, cntRow): + keySource = keyRow[idxRow] + print("Path Source: ", keySource) + paths = Paths[keySource] + pathsKey = sorted(paths.keys(), key=lambda x: int(x)) + for idxColumn in range(0, cntColumn): + print("{} -> {}".format(keySource, str(pathsKey[idxColumn]))) + print("Weight: ", Weights[idxRow][idxColumn]) + print("Path: ", Paths[str(keySource)][str(pathsKey[idxColumn])]) + + await Path.create( + start = keySource, + stop = str(pathsKey[idxColumn]), + weight = Weights[idxRow][idxColumn], + paths = Paths[str(keySource)][str(pathsKey[idxColumn])] + ) + + +async def getCnt(): + return await Path.all().count() + +async def main(): + print("cnt: ", await getCnt()) + await insertData() + + +asyncio.run( main() ) + +exit() + diff --git a/installPackage.sh b/installPackage.sh new file mode 100644 index 0000000..58f619c --- /dev/null +++ b/installPackage.sh @@ -0,0 +1 @@ +modbus-tk \ No newline at end of file diff --git a/logic/agv/__init__.py b/logic/agv/__init__.py new file mode 100644 index 0000000..0d607e5 --- /dev/null +++ b/logic/agv/__init__.py @@ -0,0 +1,7 @@ +from fastapi import APIRouter +from .views import agv_router + +router = APIRouter(prefix='/api/scheduler') + + +router.include_router(agv_router, tags=["AGV小车状态"]) diff --git a/logic/agv/__pycache__/__init__.cpython-311.pyc b/logic/agv/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..32c2b7e Binary files /dev/null and b/logic/agv/__pycache__/__init__.cpython-311.pyc differ diff --git a/logic/agv/__pycache__/views.cpython-311.pyc b/logic/agv/__pycache__/views.cpython-311.pyc new file mode 100644 index 0000000..3c8e6d6 Binary files /dev/null and b/logic/agv/__pycache__/views.cpython-311.pyc differ diff --git a/logic/agv/views.py b/logic/agv/views.py new file mode 100644 index 0000000..21703c8 --- /dev/null +++ b/logic/agv/views.py @@ -0,0 +1,112 @@ +from fastapi import APIRouter +from starlette.requests import Request +from tortoise.queryset import QuerySet, Prefetch +from models.agv import Agv +from helper import respond_to +from tortoise.contrib.pydantic import pydantic_model_creator +import requests +from typing import List + +from plc.cmd import( + cmd_type, + setSubPaths, + pass_door, + setLayer, + moveArm, + taskNum, + setTasks, + readStatus +) + +agv_router = APIRouter(prefix='/agv') + +endpoint_getassgin = "http://127.0.0.1:8000/api/scheduler/assign/" +endpoint_done = "http://127.0.0.1:8000/api/scheduler/assign/done/" +endpoint_failed = "http://127.0.0.1:8000/api/scheduler/assign/failed/" + + +# @agv_router.get("/read") +# async def getassgin(): + + + +# return respond_to(data=obj) + +@agv_router.get("/getassgin") +async def getassgin(): + res = requests.get(endpoint_getassgin) + obj = None + if res.status_code < 300: + obj = res.json() + + try: + assignment = obj["data"] + + agvs = await Agv.filter( + agvid = assignment["agvid"], + is_valid = True, + is_active = True, + ) + + agv = None + if len(agvs) > 0: + agv = agvs[0] + agv.curorder = assignment["orderid"] + agv.curitinerary = assignment["itinerary"] + await agv.save() + else: + agv = await Agv.create( + agvid = assignment["agvid"], + curorder = assignment["orderid"], + curitinerary = assignment["itinerary"] + ) + + # for task in assignment["cmd"]: + # task + + + except Exception as e: + print("getassgin: ", e) + + + return respond_to(code=res.status_code, data=obj) + +@agv_router.get("/done/{assignment_id}") +async def get_done(assignment_id: str): + print("assignment_id: ", endpoint_done + assignment_id) + res = requests.put(endpoint_done + assignment_id) + obj = None + if res.status_code < 300: + obj = res.json() + + return respond_to(code=res.status_code, data=obj) + +'http://localhost:8000/api/scheduler/assign/done/e56ea677-4dd1-46e6-8e4f-c7f25ff5893a' +@agv_router.get("/failed/{assignment_id}") +async def get_failed(assignment_id: str): + print("assignment_id: ", endpoint_failed + assignment_id) + res = requests.put(endpoint_failed + assignment_id) + obj = None + if res.status_code < 300: + obj = res.json() + + return respond_to(code=res.status_code, data=obj) + +@agv_router.get('/list', summary="列出所有AGV小车的状态") +async def index(request: Request, page_no: int = 1, page_size: int = 20): + """ + 列出所有AGV小车的状态 + :param page_no: 1 + :param page_size: 20 + :return: + """ + offset = (page_no - 1) * page_size + + query = QuerySet(Agv).filter(is_valid=True, is_active=True) + + count = await query.count() + agvs = await query.limit(page_size).offset(offset) + + return respond_to(code=200, data=dict(count=count, data=agvs)) + + diff --git a/logic/dataSample.json b/logic/dataSample.json new file mode 100644 index 0000000..08fd229 --- /dev/null +++ b/logic/dataSample.json @@ -0,0 +1,106 @@ +{ + "code": 200, + "desc": "成功", + "data": { + "id": "e619a476-4d13-460c-8b5e-65c1a525657b", + "num": "3SYB", + "orderid": "tr43frr", + "sequence": 0, + "ordertime": "2024-08-23 00:00:00+08:00", + "agvid": "001", + "itinerary": [ + "A0", + "B1", + "N2", + "I2", + "Q1", + "A0" + ], + "cmd": [ + { + "con1-A1": { + "orderid": "tr43frr", + "name": "试剂1", + "typetask": 1, + "action": 5, + "quantity": 6, + "coordinates": "B1", + "traynum": "1X01-01" + } + }, + { + "con1-B1": { + "orderid": "tr43frr", + "name": "试剂3", + "typetask": 3, + "action": 5, + "quantity": 2, + "coordinates": "N2", + "traynum": "1R07-08" + } + }, + { + "con1-C1": { + "orderid": "tr43frr", + "name": "试剂2", + "typetask": 9, + "action": 5, + "quantity": 6, + "coordinates": "I2", + "traynum": "1V04" + } + } + ], + "last": "I2", + "loada": 6, + "loadb": 2, + "loadc": 0, + "loadd": 0, + "loade": 6, + "is_canceled": false, + "cancel_time": "None", + "is_processing": false, + "is_failed": false, + "is_done": false, + "is_valid": true, + "updated_at": "2024-09-07 19:50:48.803537+08:00", + "created_at": "2024-09-07 19:50:48.775540+08:00", + "owner": { + "id": "4f7cbe2e-dedf-4a6c-ae19-fb48820dc5fc", + "username": "2346", + "name": "2346", + "is_locked": true, + "phone": "7459344674", + "email": "lsr@126.com" + }, + "subtasks": [ + { + "id": "91a18cc4-2c6f-4277-82db-0ac0c04182d2", + "num": "SC8D", + "name": "试剂2", + "quantity": 6, + "action": 5, + "typetask": 9, + "created_at": "2024-09-07 19:50:48.660618+08:00" + }, + { + "id": "33908c4e-07db-4fae-b2b8-b43f17b68d9d", + "num": "ZEE8", + "name": "试剂1", + "quantity": 6, + "action": 5, + "typetask": 1, + "created_at": "2024-09-07 19:50:48.628290+08:00" + }, + { + "id": "f9d1d905-fdf6-4957-84d2-2aa45df26b50", + "num": "ZUZZ", + "name": "试剂3", + "quantity": 2, + "action": 5, + "typetask": 3, + "created_at": "2024-09-07 19:50:48.710594+08:00" + } + ] + } + } \ No newline at end of file diff --git a/logs/api.log b/logs/api.log new file mode 100644 index 0000000..945c9b4 --- /dev/null +++ b/logs/api.log @@ -0,0 +1 @@ +. \ No newline at end of file diff --git a/logs/server.log b/logs/server.log new file mode 100644 index 0000000..b86c89d --- /dev/null +++ b/logs/server.log @@ -0,0 +1,123 @@ +.INFO 12272:28072 2024-09-06 16:04:38,142 agvtasks.py:273 - SERVER - ׼AGVСɷʱ, ʱ: 12 +2024-09-06 16:04:38,144 - INFO - Application startup complete. +2024-09-06 16:07:15,590 - ERROR - Exception in ASGI application +Traceback (most recent call last): + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\starlette\middleware\base.py", line 78, in call_next + message = await recv_stream.receive() + ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\anyio\streams\memory.py", line 94, in receive + return self.receive_nowait() + ^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\anyio\streams\memory.py", line 87, in receive_nowait + raise EndOfStream +anyio.EndOfStream + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\uvicorn\protocols\http\httptools_impl.py", line 435, in run_asgi + result = await app( # type: ignore[func-returns-value] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\uvicorn\middleware\proxy_headers.py", line 78, in __call__ + return await self.app(scope, receive, send) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\fastapi\applications.py", line 276, in __call__ + await super().__call__(scope, receive, send) + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\starlette\applications.py", line 122, in __call__ + await self.middleware_stack(scope, receive, send) + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\starlette\middleware\errors.py", line 184, in __call__ + raise exc + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\starlette\middleware\errors.py", line 162, in __call__ + await self.app(scope, receive, _send) + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\starlette\middleware\cors.py", line 84, in __call__ + await self.app(scope, receive, send) + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\starlette\middleware\base.py", line 108, in __call__ + response = await self.dispatch_func(request, call_next) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\AutoTaskSchedule\simAgv\main.py", line 54, in process_time_header + response = await next(request) + ^^^^^^^^^^^^^^^^^^^ + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\starlette\middleware\base.py", line 84, in call_next + raise app_exc + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\starlette\middleware\base.py", line 70, in coro + await self.app(scope, receive_or_disconnect, send_no_error) + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\starlette\middleware\exceptions.py", line 79, in __call__ + raise exc + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\starlette\middleware\exceptions.py", line 68, in __call__ + await self.app(scope, receive, sender) + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\fastapi\middleware\asyncexitstack.py", line 21, in __call__ + raise e + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\fastapi\middleware\asyncexitstack.py", line 18, in __call__ + await self.app(scope, receive, send) + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\starlette\routing.py", line 718, in __call__ + await route.handle(scope, receive, send) + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\starlette\routing.py", line 276, in handle + await self.app(scope, receive, send) + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\starlette\routing.py", line 66, in app + response = await func(request) + ^^^^^^^^^^^^^^^^^^^ + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\fastapi\routing.py", line 237, in app + raw_response = await run_endpoint_function( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\fastapi\routing.py", line 163, in run_endpoint_function + return await dependant.call(**values) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\AutoTaskSchedule\simAgv\logic\agv\views.py", line 35, in index + query = QuerySet(Agv).filter(is_valid=True, is_active=True) + ^^^^^^^^^^^^^ + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\tortoise\queryset.py", line 299, in __init__ + super().__init__(model) + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\tortoise\queryset.py", line 96, in __init__ + self.capabilities: Capabilities = model._meta.db.capabilities + ^^^^^^^^^^^^^^ + File "C:\Users\hp\AppData\Local\Programs\Python\Python311\Lib\site-packages\tortoise\models.py", line 285, in db + raise ConfigurationError( +tortoise.exceptions.ConfigurationError: default_connection for the model cannot be None +2024-09-06 16:08:17,538 - INFO - Shutting down +2024-09-06 16:08:17,647 - INFO - Waiting for application shutdown. +2024-09-06 16:08:17,649 - INFO - Application shutdown complete. +2024-09-06 16:08:17,650 - INFO - Finished server process [12272] +INFO 11504:26992 2024-09-06 16:19:14,235 agvtasks.py:273 - SERVER - ׼AGVСɷʱ, ʱ: 12 +2024-09-06 16:19:14,237 - INFO - Application startup complete. +2024-09-06 16:28:08,549 - INFO - Shutting down +2024-09-06 16:28:08,658 - INFO - Waiting for application shutdown. +2024-09-06 16:28:08,661 - INFO - Application shutdown complete. +2024-09-06 16:28:08,662 - INFO - Finished server process [11504] +INFO 14408:36888 2024-09-06 16:28:09,897 agvtasks.py:273 - SERVER - ׼AGVСɷʱ, ʱ: 12 +2024-09-06 16:28:09,899 - INFO - Application startup complete. +2024-09-06 16:28:11,095 - INFO - Shutting down +2024-09-06 16:28:11,205 - INFO - Waiting for application shutdown. +2024-09-06 16:28:11,207 - INFO - Application shutdown complete. +2024-09-06 16:28:11,208 - INFO - Finished server process [14408] +INFO 33012:30064 2024-09-06 17:33:05,949 agvtasks.py:273 - SERVER - ׼AGVСɷʱ, ʱ: 12 +2024-09-06 17:33:05,951 - INFO - Application startup complete. +2024-09-06 17:33:15,443 - INFO - Shutting down +2024-09-06 17:33:15,553 - INFO - Waiting for application shutdown. +2024-09-06 17:33:15,554 - INFO - Application shutdown complete. +2024-09-06 17:33:15,555 - INFO - Finished server process [33012] +INFO 33740:5880 2024-09-06 17:33:22,981 agvtasks.py:273 - SERVER - ׼AGVСɷʱ, ʱ: 12 +2024-09-06 17:33:22,983 - INFO - Application startup complete. +2024-09-06 17:33:26,258 - INFO - Shutting down +2024-09-06 17:33:26,367 - INFO - Waiting for application shutdown. +2024-09-06 17:33:26,367 - INFO - Application shutdown complete. +2024-09-06 17:33:26,367 - INFO - Finished server process [33740] +INFO 35040:17000 2024-09-06 17:34:07,447 agvtasks.py:273 - SERVER - ׼AGVСɷʱ, ʱ: 12 +2024-09-06 17:34:07,449 - INFO - Application startup complete. +2024-09-06 17:49:24,793 - INFO - Shutting down +2024-09-06 17:49:24,901 - INFO - Waiting for application shutdown. +2024-09-06 17:49:24,905 - INFO - Application shutdown complete. +2024-09-06 17:49:24,906 - INFO - Finished server process [35040] +INFO 40716:17136 2024-09-06 17:49:25,709 agvtasks.py:273 - SERVER - ׼AGVСɷʱ, ʱ: 12 +2024-09-06 17:49:25,710 - INFO - Application startup complete. +2024-09-06 17:52:02,164 - INFO - Shutting down +2024-09-06 17:52:02,273 - INFO - Waiting for application shutdown. +2024-09-06 17:52:02,274 - INFO - Application shutdown complete. +2024-09-06 17:52:02,275 - INFO - Finished server process [40716] +INFO 34940:9244 2024-09-06 17:52:03,508 agvtasks.py:273 - SERVER - ׼AGVСɷʱ, ʱ: 12 +2024-09-06 17:52:03,510 - INFO - Application startup complete. +2024-09-06 17:59:04,023 - INFO - Shutting down +2024-09-06 17:59:04,130 - INFO - Waiting for application shutdown. +2024-09-06 17:59:04,132 - INFO - Application shutdown complete. +2024-09-06 17:59:04,132 - INFO - Finished server process [34940] +INFO 36040:23864 2024-09-06 17:59:05,373 agvtasks.py:273 - SERVER - ׼AGVСɷʱ, ʱ: 12 +2024-09-06 17:59:05,375 - INFO - Application startup complete. diff --git a/main.py b/main.py new file mode 100644 index 0000000..29560fa --- /dev/null +++ b/main.py @@ -0,0 +1,79 @@ +import asyncio +import os +import time + +import logging +from tortoise import Tortoise +import uvicorn +from fastapi import FastAPI, Request +from fastapi.exceptions import RequestValidationError +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from tortoise.exceptions import DoesNotExist, DBConnectionError + +from conf import setting, register_mysql +from logic import agv +from agvtask.agvtasks import start_scheduler + +app = FastAPI(debug=setting.DEBUG_MODE) +app.mount('/static', StaticFiles(directory="static"), name='static') + + +def enable_debug_sql(): + import logging, sys + + fmt = logging.Formatter( + fmt="%(asctime)s - %(name)s:%(lineno)d - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", + ) + sh = logging.StreamHandler(sys.stdout) + sh.setLevel(logging.DEBUG) + sh.setFormatter(fmt) + + # will print debug sql + logger_db_client = logging.getLogger("tortoise.db_client") + logger_db_client.setLevel(logging.DEBUG) + + +if setting.SQL_DEBUG_MODE: + enable_debug_sql() + +# 注册连接数据库 +register_mysql(app) +Tortoise.init_models(["models.agv"], "models") + + +@app.on_event("startup") +async def startup(): + await start_scheduler() + +# 执行时间中间件 +@app.middleware('http') +async def process_time_header(request: Request, next): + start_time = time.time() + response = await next(request) + process_time = time.time() - start_time + response.headers['X-Process-Time'] = str(process_time) + return response + + +@app.on_event("startup") +async def startup_log_event(): + logger = logging.getLogger("uvicorn.access") + access_error = logging.getLogger("uvicorn.error") + handler = logging.handlers.TimedRotatingFileHandler('logs/server.log', when='midnight', backupCount=30) + handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) + logger.addHandler(handler) + logger.setLevel(logging.WARNING) + access_error.addHandler(handler) + + +# 跨域中间件 +app.add_middleware( + CORSMiddleware, allow_origins=setting.YANEI_CORS_ORIGINS, allow_credentials=True, allow_methods=['*'], allow_headers=['*'] +) + +app.include_router(agv.router) + +if __name__ == '__main__': + uvicorn.run(app='main:app', host='0.0.0.0', port=setting.PORT, reload=setting.DEBUG_DEV_MODE) diff --git a/migrations/models/0_20240907164554_init.py b/migrations/models/0_20240907164554_init.py new file mode 100644 index 0000000..b5f13ac --- /dev/null +++ b/migrations/models/0_20240907164554_init.py @@ -0,0 +1,58 @@ +from tortoise import BaseDBAsyncClient + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + CREATE TABLE IF NOT EXISTS `aerich` ( + `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT, + `version` VARCHAR(255) NOT NULL, + `app` VARCHAR(100) NOT NULL, + `content` JSON NOT NULL +) CHARACTER SET utf8mb4; +CREATE TABLE IF NOT EXISTS `Agvs` ( + `id` CHAR(36) NOT NULL PRIMARY KEY, + `num` VARCHAR(255) COMMENT '短编号', + `agvid` VARCHAR(255) COMMENT 'AGV小车编号', + `percent` DECIMAL(3,2) NOT NULL COMMENT '小车电量' DEFAULT 1, + `records` JSON COMMENT '记录列表', + `curorder` VARCHAR(255) COMMENT '当前订单号', + `curstop` VARCHAR(255) COMMENT '当前站点', + `curitinerary` JSON COMMENT '订单对应的全局最短路径', + `is_charging` BOOL NOT NULL COMMENT '充电中' DEFAULT 0, + `is_busy` BOOL NOT NULL COMMENT '设备忙' DEFAULT 0, + `is_abnormal` BOOL NOT NULL COMMENT '是否异常' DEFAULT 0, + `abnormal_time` DATETIME(6) COMMENT '异常时间', + `is_done` BOOL NOT NULL COMMENT '是否完成' DEFAULT 0, + `created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `is_valid` BOOL NOT NULL COMMENT '是否有效' DEFAULT 1, + `is_active` BOOL NOT NULL COMMENT '是否删除' DEFAULT 1 +) CHARACTER SET utf8mb4 COMMENT='AGV管理表'; +CREATE INDEX `num` ON `Agvs` (`num`); +CREATE INDEX `agvid` ON `Agvs` (`agvid`); +CREATE INDEX `agvs_is_busy` ON `Agvs` (`is_busy`); +CREATE INDEX `agvs_is_abnormal` ON `Agvs` (`is_abnormal`); +CREATE INDEX `agvs_is_done` ON `Agvs` (`is_done`); +CREATE INDEX `agvs_is_charging` ON `Agvs` (`is_charging`); +CREATE INDEX `is_valididx` ON `Agvs` (`is_valid`); +CREATE INDEX `is_activeidx` ON `Agvs` (`is_active`); +CREATE TABLE IF NOT EXISTS `Paths` ( + `id` CHAR(36) NOT NULL PRIMARY KEY, + `start` VARCHAR(255) COMMENT '起点', + `stop` VARCHAR(255) COMMENT '终点', + `weight` INT NOT NULL COMMENT '距离' DEFAULT 0, + `paths` JSON COMMENT '路径', + `created_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), + `updated_at` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + `is_valid` BOOL NOT NULL COMMENT '是否有效' DEFAULT 1, + `is_active` BOOL NOT NULL COMMENT '是否删除' DEFAULT 1 +) CHARACTER SET utf8mb4 COMMENT='AGV管理表'; +CREATE INDEX `start` ON `Paths` (`start`); +CREATE INDEX `stop` ON `Paths` (`stop`); +CREATE INDEX `is_valididx` ON `Paths` (`is_valid`); +CREATE INDEX `is_activeidx` ON `Paths` (`is_active`);""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + """ diff --git a/migrations/models/1_20240907164558_update.py b/migrations/models/1_20240907164558_update.py new file mode 100644 index 0000000..264fbdf --- /dev/null +++ b/migrations/models/1_20240907164558_update.py @@ -0,0 +1,57 @@ +from tortoise import BaseDBAsyncClient + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + ALTER TABLE `Paths` DROP INDEX `stop`; + ALTER TABLE `Paths` DROP INDEX `is_valididx`; + ALTER TABLE `Paths` DROP INDEX `is_activeidx`; + ALTER TABLE `Paths` DROP INDEX `start`; + ALTER TABLE `Agvs` DROP INDEX `agvs_is_charging`; + ALTER TABLE `Agvs` DROP INDEX `agvs_is_done`; + ALTER TABLE `Agvs` DROP INDEX `is_valididx`; + ALTER TABLE `Agvs` DROP INDEX `num`; + ALTER TABLE `Agvs` DROP INDEX `agvid`; + ALTER TABLE `Agvs` DROP INDEX `is_activeidx`; + ALTER TABLE `Agvs` DROP INDEX `agvs_is_abnormal`; + ALTER TABLE `Agvs` DROP INDEX `agvs_is_busy`; + CREATE INDEX `agvs_is_charging` ON `Agvs` (`is_charging`); + CREATE INDEX `is_valididx` ON `Agvs` (`is_valid`); + CREATE INDEX `agvs_is_busy` ON `Agvs` (`is_busy`); + CREATE INDEX `agvs_is_done` ON `Agvs` (`is_done`); + CREATE INDEX `agvid` ON `Agvs` (`agvid`); + CREATE INDEX `agvs_is_abnormal` ON `Agvs` (`is_abnormal`); + CREATE INDEX `is_activeidx` ON `Agvs` (`is_active`); + CREATE INDEX `num` ON `Agvs` (`num`); + CREATE INDEX `stop` ON `Paths` (`stop`); + CREATE INDEX `start` ON `Paths` (`start`); + CREATE INDEX `is_activeidx` ON `Paths` (`is_active`); + CREATE INDEX `is_valididx` ON `Paths` (`is_valid`);""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + ALTER TABLE `Paths` DROP INDEX `is_valididx`; + ALTER TABLE `Paths` DROP INDEX `is_activeidx`; + ALTER TABLE `Paths` DROP INDEX `start`; + ALTER TABLE `Paths` DROP INDEX `stop`; + ALTER TABLE `Agvs` DROP INDEX `num`; + ALTER TABLE `Agvs` DROP INDEX `is_activeidx`; + ALTER TABLE `Agvs` DROP INDEX `agvs_is_abnormal`; + ALTER TABLE `Agvs` DROP INDEX `agvid`; + ALTER TABLE `Agvs` DROP INDEX `agvs_is_done`; + ALTER TABLE `Agvs` DROP INDEX `agvs_is_busy`; + ALTER TABLE `Agvs` DROP INDEX `is_valididx`; + ALTER TABLE `Agvs` DROP INDEX `agvs_is_charging`; + CREATE INDEX `agvs_is_busy` ON `Agvs` (`is_busy`); + CREATE INDEX `agvs_is_abnormal` ON `Agvs` (`is_abnormal`); + CREATE INDEX `is_activeidx` ON `Agvs` (`is_active`); + CREATE INDEX `agvid` ON `Agvs` (`agvid`); + CREATE INDEX `num` ON `Agvs` (`num`); + CREATE INDEX `is_valididx` ON `Agvs` (`is_valid`); + CREATE INDEX `agvs_is_done` ON `Agvs` (`is_done`); + CREATE INDEX `agvs_is_charging` ON `Agvs` (`is_charging`); + CREATE INDEX `start` ON `Paths` (`start`); + CREATE INDEX `is_activeidx` ON `Paths` (`is_active`); + CREATE INDEX `is_valididx` ON `Paths` (`is_valid`); + CREATE INDEX `stop` ON `Paths` (`stop`);""" diff --git a/migrations/models/__pycache__/1_20240907164558_update.cpython-311.pyc b/migrations/models/__pycache__/1_20240907164558_update.cpython-311.pyc new file mode 100644 index 0000000..876f84d Binary files /dev/null and b/migrations/models/__pycache__/1_20240907164558_update.cpython-311.pyc differ diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..f836f19 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,2 @@ +from .agv import Agv +from .path import Path \ No newline at end of file diff --git a/models/__pycache__/__init__.cpython-311.pyc b/models/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..cb509ed Binary files /dev/null and b/models/__pycache__/__init__.cpython-311.pyc differ diff --git a/models/__pycache__/agv.cpython-311.pyc b/models/__pycache__/agv.cpython-311.pyc new file mode 100644 index 0000000..e415e66 Binary files /dev/null and b/models/__pycache__/agv.cpython-311.pyc differ diff --git a/models/__pycache__/path.cpython-311.pyc b/models/__pycache__/path.cpython-311.pyc new file mode 100644 index 0000000..d49bb77 Binary files /dev/null and b/models/__pycache__/path.cpython-311.pyc differ diff --git a/models/__pycache__/user.cpython-311.pyc b/models/__pycache__/user.cpython-311.pyc new file mode 100644 index 0000000..5040aac Binary files /dev/null and b/models/__pycache__/user.cpython-311.pyc differ diff --git a/models/agv.py b/models/agv.py new file mode 100644 index 0000000..369919a --- /dev/null +++ b/models/agv.py @@ -0,0 +1,85 @@ +from tortoise import fields, models +from tortoise.indexes import Index +from tortoise.contrib.pydantic import pydantic_model_creator +from tortoise.signals import post_save +from typing import Type +from helper.tools import genRandomStr + +# from conf.setting import setting + +class Agv(models.Model): + id = fields.UUIDField(pk=True) + num = fields.CharField(null=True, max_length=255, description='短编号') + + agvid = fields.CharField(null=True, max_length=255, description='AGV小车编号') + + percent = fields.DecimalField(default=1.0, max_digits=3, decimal_places=2, description='小车电量') + + records = fields.JSONField(null=True, description='记录列表') + + curorder = fields.CharField(null=True, max_length=255, description='当前订单号') + curstop = fields.CharField(null=True, max_length=255, description='当前站点') + curitinerary = fields.JSONField(null=True, description='订单对应的全局最短路径') + + is_charging = fields.BooleanField(default=False, description='充电中') + is_busy = fields.BooleanField(default=False, description='设备忙') + + is_abnormal = fields.BooleanField(default=False, description='是否异常') + abnormal_time = fields.DatetimeField(null=True, description='异常时间') + + is_done = fields.BooleanField(default=False, description='是否完成') + + created_at = fields.DatetimeField(auto_now_add=True) + updated_at = fields.DatetimeField(auto_now=True) + is_valid = fields.BooleanField(default=True, description='是否有效') + is_active = fields.BooleanField(default=True, description='是否删除') + + + class Meta: + table = 'Agvs' + table_description = 'AGV管理表' + + indexes = [ + Index(fields={"num"}, name="num"), + Index(fields={"agvid"}, name="agvid"), + Index(fields={"is_busy"}, name="agvs_is_busy"), + Index(fields={"is_abnormal"}, name="agvs_is_abnormal"), + Index(fields={"is_done"}, name="agvs_is_done"), + Index(fields={"is_charging"}, name="agvs_is_charging"), + Index(fields={"is_valid"}, name="is_valididx"), + Index(fields={"is_active"}, name="is_activeidx"), + ] + + def __str__(self): + return f'Agv(id={self.id} num={self.num}, agvid={self.agvid}, created_at={self.created_at})' + + def as_json(self): + return dict(id="{}".format(self.id), num=self.num, agvid=self.agvid, created_at=self.created_at) + +Agv_Pydantic = pydantic_model_creator(Agv, name="Agv") + + +@post_save(Agv) +async def agv_post_save( + sender: "Type[Agv]", + instance: Agv, + created, + using_db, + update_fields, +) -> None: + + if not instance.num or "null" == instance.num: + # print("instance.num: ", instance.num) + # check duplicated num + async def checkNum(): + num = genRandomStr(4) + # print("new num: ", num) + if await Agv.filter(num = num).exists(): + # print("num exist, ", num) + checkNum() + else: + # print("num not exist, ", num) + instance.num = num + await instance.save() + + await checkNum() diff --git a/models/path.py b/models/path.py new file mode 100644 index 0000000..29654fd --- /dev/null +++ b/models/path.py @@ -0,0 +1,41 @@ +from tortoise import fields, models +from tortoise.indexes import Index +from tortoise.contrib.pydantic import pydantic_model_creator +from tortoise.signals import post_save +from typing import Type + + +class Path(models.Model): + id = fields.UUIDField(pk=True) + + start = fields.CharField(null=True, max_length=255, description='起点') + stop = fields.CharField(null=True, max_length=255, description='终点') + weight = fields.IntField(default=0, description='距离') + paths = fields.JSONField(null=True, max_length=255, description='路径') + + created_at = fields.DatetimeField(auto_now_add=True) + updated_at = fields.DatetimeField(auto_now=True) + is_valid = fields.BooleanField(default=True, description='是否有效') + is_active = fields.BooleanField(default=True, description='是否删除') + + + class Meta: + table = 'Paths' + table_description = 'AGV管理表' + + indexes = [ + Index(fields={"start"}, name="start"), + Index(fields={"stop"}, name="stop"), + Index(fields={"is_valid"}, name="is_valididx"), + Index(fields={"is_active"}, name="is_activeidx"), + ] + + def __str__(self): + return f'Path(id={self.id} start={self.start}, stop={self.stop}, created_at={self.created_at})' + + def as_json(self): + return dict(id="{}".format(self.id), start=self.start, stop=self.stop, created_at=self.created_at) + +Path_Pydantic = pydantic_model_creator(Path, name="Path") + + diff --git a/models/warehouse.py b/models/warehouse.py new file mode 100644 index 0000000..04f232b --- /dev/null +++ b/models/warehouse.py @@ -0,0 +1,141 @@ +from tortoise import fields, models +from tortoise.indexes import Index +from tortoise.contrib.pydantic import pydantic_model_creator, pydantic_queryset_creator +from tortoise.signals import post_save +from typing import Type +from helper.tools import genRandomStr + +class Vacancy(models.Model): + id = fields.UUIDField(pk=True) + number = fields.CharField(max_length=255, null=True, description='空位编号') + traynum = fields.CharField(max_length=255, null=True, description='托盘编号') + + orderids = fields.JSONField(null=True, description='托盘编号对应的订单号列表') + + memo = fields.JSONField(null=True, description='备忘录') + + is_shelf = fields.BooleanField(default=False, description='是货架库存') + is_connect = fields.BooleanField(default=False, description='是接驳区库存') + + created_at = fields.DatetimeField(auto_now_add=True) + updated_at = fields.DatetimeField(auto_now=True) + is_valid = fields.BooleanField(default=True, description='是否取消') + is_active = fields.BooleanField(default=True, description='是否删除') + + + class PydanticMeta: + # Let's exclude the created timestamp + exclude = ("created_at",) + + class Meta: + table = 'Vacancys' + table_description = '暂存货架或接驳机空位编号表' + + ordering = ["number", "created_at"] + + indexes = [ + Index(fields={"number"}, name="numberidx"), + Index(fields={"traynum"}, name="traynumidx"), + Index(fields={"is_shelf"}, name="is_shelfidx"), + Index(fields={"is_connect"}, name="is_connectidx"), + Index(fields={"is_valid"}, name="is_valididx"), + Index(fields={"is_active"}, name="is_activeidx"), + ] + + def __str__(self): + return f'Vacancy(id={self.id} number={self.number}, traynum={self.traynum}, created_at={self.created_at})' + + def as_json(self): + return dict(id="{}".format(self.id), number=self.number, traynum=self.traynum, created_at="{}".format(self.created_at)) + + +Vacancy_Pydantic = pydantic_model_creator(Vacancy, name="Vacancy") +Vacancy_Pydantic_List = pydantic_queryset_creator(Vacancy) + + +class Schelve(models.Model): + id = fields.UUIDField(pk=True) + num = fields.CharField(null=True, max_length=255, description='短编号') + name = fields.CharField(null=True, max_length=255, description='暂存货架编号') + vacancies = fields.ManyToManyField('models.Vacancy', related_name='schelvevacancies') + + memo = fields.JSONField(null=True, description='备忘录') + + created_at = fields.DatetimeField(auto_now_add=True) + updated_at = fields.DatetimeField(auto_now=True) + is_valid = fields.BooleanField(default=True, description='是否取消') + is_active = fields.BooleanField(default=True, description='是否删除') + + + class PydanticMeta: + # Let's exclude the created timestamp + exclude = ("created_at",) + allow_cycles = True + max_recursion = 4 + + class Meta: + table = 'Schelves' + table_description = '暂存货架库存表' + + ordering = ["name", "created_at"] + + indexes = [ + Index(fields={"name"}, name="nameidx"), + Index(fields={"num"}, name="numidx"), + Index(fields={"is_valid"}, name="is_valididx"), + Index(fields={"is_active"}, name="is_activeidx"), + ] + + def __str__(self): + return f'Schelve(id={self.id} num={self.num}, created_at={self.created_at})' + + def as_json(self): + return dict( + id="{}".format(self.id), + num=self.num, + name =self.name, + memo =self.memo, + is_valid =self.is_valid, + is_active =self.is_active, + updated_at="{}".format(self.updated_at), + created_at="{}".format(self.created_at)) + +Schelve_Pydantic = pydantic_model_creator(Schelve, name="Schelve") + +@post_save(Schelve) +async def schelve_post_save( + sender: "Type[Schelve]", + instance: Schelve, + created, + using_db, + update_fields, +) -> None: + + if not instance.num or "null" == instance.num: + # print("instance.num: ", instance.num) + # check duplicated num + async def checkNum(): + num = genRandomStr(4) + # print("new num: ", num) + if await Schelve.filter(num = num).exists(): + # print("num exist, ", num) + checkNum() + else: + # print("num not exist, ", num) + instance.num = num + await instance.save() + + await checkNum() + + + + + + + + + + + + + diff --git a/plc/cmd.py b/plc/cmd.py new file mode 100644 index 0000000..9fedb08 --- /dev/null +++ b/plc/cmd.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python +# -*- coding: utf_8 -*- + +import modbus_tk.defines as cst +from plc.tools import bytes_to_int_list, get_bit_bool, int_list_to_bytes +from plc.modbus import ModbusConnection +from conf.setting import AGVPLC +from models.path import Path +import time + + +class TaskAddr: + + # 起始地址 + TASKTYPE_ADDR_BEGIN = 15000 + + # AGV路径点集合 + PATH_ADDR_BEGIN = 15001 + # PATH_ADDR_LENGTH = 42 + + #是否经过玻璃门 + PASS_DOOR_ADDR = 15051 + + #货架第几层 + LAYER_ADDR = 15052 + + #机械臂是否动作 + ARM_ADDR = 15053 + + #任务数量 + TASK_NUM = 15060 + + #任务取放开始地址 15061, 15062 -> 15079, 15080 + T1_ADDR_GET_ADDR = 15061 + T1_ADDR_RETURN_ADDR = 15062 + + #任务截至地址 + T10_ADDR_GET_ADDR = 15079 + T10_ADDR_RETURN_ADDR = 15080 + + # + + ###### + # Read Registers + ###### + #任务是否完成 + TASK_DONE_ADDR = 16000 + #任务消息接收 + TASK_RECV_ADDR = 16001 + #AGV所在站点 + AGV_STOP_ADDR = 16002 + #AGV当前电量 + AGV_POWER_ADDR = 16003 + #AGV充电状态 + AGV_CHARGE_ADDR = 16004 + #设备忙 + BUSY_ADDR = 16005 + #设备异常 + ABNORM_ADDR = 16006 + #报警信息 + ALARM_ADDR = 16007 + + #状态寄存器长度 + # 连续读取8字节,单字节长度为16位 + STATUS_ADDR_LENGTH = 8 + + +# +def cmd_type(cmdType: int): + try: + with ModbusConnection( + # 试剂amr机器人 + AGVPLC.HOST, + AGVPLC.PORT + ) as master: + res = master.execute( + 1, + cst.WRITE_SINGLE_REGISTER, + starting_address=TaskAddr.TASKTYPE_ADDR_BEGIN, + output_value=cmdType + ) + print('cmd_type: ',res) + if res: + task_status = False + + except Exception as e: + print('cmd_type信息指令异常:' + str(e)) + time.sleep(2) + +async def setSubPaths(start: str, stop: str): + path = await Path.filter(start = start, stop = stop, is_valid = True, is_active = True) + + subPaths = [] + + if path and len(path) > 0: + subPaths = path.paths + + try: + with ModbusConnection( + # 试剂amr机器人 + AGVPLC.HOST, + AGVPLC.PORT + ) as master: + res = master.execute( + 1, + cst.WRITE_MULTIPLE_REGISTERS, + starting_address=TaskAddr.TASKTYPE_ADDR_BEGIN, + output_value=subPaths + ) + print('cmd_type: ',res) + if res: + task_status = False + + except Exception as e: + print('cmd_type信息指令异常:' + str(e)) + time.sleep(2) + + + +# +def pass_door(passDoor: int): + try: + with ModbusConnection( + # 试剂amr机器人 + AGVPLC.HOST, + AGVPLC.PORT + ) as master: + res = master.execute( + 1, + cst.WRITE_SINGLE_REGISTER, + starting_address=TaskAddr.PASS_DOOR_ADDR, + output_value=passDoor + ) + print('cmd_type: ',res) + if res: + task_status = False + + except Exception as e: + print('cmd_type信息指令异常:' + str(e)) + time.sleep(2) + +# +def setLayer(layer: int): + try: + with ModbusConnection( + # 试剂amr机器人 + AGVPLC.HOST, + AGVPLC.PORT + ) as master: + res = master.execute( + 1, + cst.WRITE_SINGLE_REGISTER, + starting_address=TaskAddr.LAYER_ADDR, + output_value=layer + ) + print('cmd_type: ',res) + if res: + task_status = False + + except Exception as e: + print('cmd_type信息指令异常:' + str(e)) + time.sleep(2) + +# +def moveArm(move: int): + try: + with ModbusConnection( + # 试剂amr机器人 + AGVPLC.HOST, + AGVPLC.PORT + ) as master: + res = master.execute( + 1, + cst.WRITE_SINGLE_REGISTER, + starting_address=TaskAddr.ARM_ADDR, + output_value=move + ) + print('cmd_type: ',res) + if res: + task_status = False + + except Exception as e: + print('cmd_type信息指令异常:' + str(e)) + time.sleep(2) + +# +def taskNum(num: int): + try: + with ModbusConnection( + # 试剂amr机器人 + AGVPLC.HOST, + AGVPLC.PORT + ) as master: + res = master.execute( + 1, + cst.WRITE_SINGLE_REGISTER, + starting_address=TaskAddr.TASK_NUM, + output_value=num + ) + print('cmd_type: ',res) + if res: + task_status = False + + except Exception as e: + print('cmd_type信息指令异常:' + str(e)) + time.sleep(2) + + +def setTasks(tasks = []): + + try: + with ModbusConnection( + # 试剂amr机器人 + AGVPLC.HOST, + AGVPLC.PORT + ) as master: + res = master.execute( + 1, + cst.WRITE_MULTIPLE_REGISTERS, + starting_address=TaskAddr.T1_ADDR_GET_ADDR, + output_value=tasks + ) + print('cmd_type: ',res) + if res: + task_status = False + + except Exception as e: + print('cmd_type信息指令异常:' + str(e)) + time.sleep(2) + + +def readStatus(): + + with ModbusConnection( + AGVPLC.HOST, + AGVPLC.PORT + ) as master: + status = master.execute( + 1, + cst.READ_HOLDING_REGISTERS, + TaskAddr.TASK_DONE_ADDR, + TaskAddr.STATUS_ADDR_LENGTH + ) + print('readStatus: ',status) + return status diff --git a/plc/modbus.py b/plc/modbus.py new file mode 100644 index 0000000..25d226e --- /dev/null +++ b/plc/modbus.py @@ -0,0 +1,35 @@ +import traceback +import click +from modbus_tk import modbus_tcp + + +class ModbusConnection: + ''' + Modbus连接管理器 + ''' + + def __init__(self, host: str, port: int) -> None: + self.host = host + self.port = port + self.master = None + + def __enter__(self) -> modbus_tcp.TcpMaster: + self.master = modbus_tcp.TcpMaster(self.host, self.port) + self.master.set_timeout(5.0) + return self.master + + def __exit__(self, exc_type, exc_value, _traceback) -> None: + try: + if self.master: + self.master.close() + + if exc_type is not None: + click.echo("发生异常:") + click.echo(f"类型: {exc_type}") + click.echo(f"值: {exc_value}") + click.echo("调用栈:") + traceback.print_tb(_traceback) + except Exception as e : + print('PLC通信异常:::',str(e)) + + diff --git a/plc/test.py b/plc/test.py new file mode 100644 index 0000000..409e19d --- /dev/null +++ b/plc/test.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python +# -*- coding: utf_8 -*- +import struct +import modbus_tk.defines as cst +from plc.tools import bytes_to_int_list, get_bit_bool, int_list_to_bytes +from plc.modbus import ModbusConnection +from conf.setting import AGVPLC +from models.path import Path +import time + + +''' +智能柜地址定义 +''' +ADDR_DRAWER = 6051 +ADDR_CABINET = 6052 +ADDR_OPEN_DRAWER = 6053 +ADDR_CLOSE_DRAWER = 6054 + + +class Cabinet: + ''' + 智能柜配置 + ''' + HOST = '192.168.193.211' + PORT = 6000 + +class Worktop: + ''' + 操作台配置 + ''' + HOST = '192.168.193.212' + PORT = 502 + +class AmrDrug: + ''' + 药剂AMR配置PLC + ''' + HOST = '192.168.193.202' + PORT = 502 +class AmrInstrument: + ''' + 仪器AMR配置PLC + ''' + HOST = '192.168.193.203' + PORT = 502 + + +""" +操作台地址定义 +""" +class WorktopAddr: + ADDR_POWER_ONE = 6080 + ADDR_POWER_TWO = 6081 + ADDR_TRANSFER_TRAY = 6082 + ADDR_POWER_ONE_STATUS = 6083 + ADDR_POWER_TWO_STATUS = 6084 + ADDR_TANSFER_TRAY_STATUS = 6085 + ADDR_WORKTOP_ERR = 6086 + TASK_ADDR_BEGIN = 6080 # 发送任务的起始未知 + STATUS_ADDR_BEGIN = 6083 # 状态的起始状态 + STATUS_ADDR_LENGTH = 4 + +class AmrAddr: + + # 起始地址 + TASK_ADDR_BEGIN = 6001 + + # 门状态 + CABIENT_STATUS_ADDR_BEGIN = 6061 + + # 抓试剂点位的起始地址 + GRAB_ADDR_BEGIN = 7000 + # 抓取校验点的起始地址 + GRAB_MARK_ADDR_BEGIN = 7600 + + # 放试剂点位的起始地址 + DEPOSIT_ADDR_BEGIN= 9000 + # 松抓校验点的起始地址 + DEPOSIT_MARK_ADDR_BEGIN= 7900 + + + # 状态起始地址及长度 + STATUS_ADDR_BEGIN = 6100 + STATUS_ADDR_LENGTH = 42 + + # 取货起始地址及长度 + LOAD_LOCATION_BEGIN = 7000 + LOAD_LOCATION_COUNT = 25 + + # 卸货起始地址及长度 + UNLOAD_LOCATION_BEGIN = 7300 + UNLOAD_LOCATION_COUNT = 25 + + TASK_STATUS = 6100 # 状态显示1 + AMR_STATUS = 6101 # 状态显示2 + INSTRUMENT_STATUS_1 = 6102 # 状态显示3 + D6103 = 6103 + D6104 = 6104 + INSTRUMENT_STATUS_2 = 6105 + D6105 = 6105 + D6106 = 6106 + D6107 = 6107 + D6108 = 6108 + D6109 = 6109 + INSTRUMENT_CAM_HIGH = 6110 + CUR_LOAD_LAYER_COMPLETE_NUM = 6112 # 当前层已取试剂数量 + CUR_LOAD_LAYER_COMPLETE = 6113 # 当前层试剂已取完 + CUR_LOAD_LAYER_ID = 6114 # 当前在取层数 + LOAD_NUMBER = 6115 # 合计一共已取试剂数量 + D6116 = 6116 + D6117 = 6117 + D6118 = 6118 + D6119 = 6119 + ROBOT_CUR_NAV_LOCATION = 6120 # 机器人当前导航站点 + ROBOT_LOCATING_STATUS = 6121 # 机器人定位状态 + ROBOT_CUR_NAV_STATUS = 6122 # 机器人当前导航状态 + ROBOT_CUR_NAV_TYPE = 6123 # 机器人当前导航类型 + ROBOT_CONFIDENCE_LEVEL = 6124 # 机器人定位置信度 + BATTERY_STATUS = 6126 # 电池电量 + BATTERY_TEM = 6128 # 电池温度 + BATTERY_VOLTAGE = 6130 # 电池电压 + ROBOT_CUR_LOCATION = 6132 # 机器人当前所在站点 + ROBOT_PRE_LOCATION = 6133 # 机器人上一个所在站点 + ROBOT_NXT_LOCATION = 6134 # 机器人下一个要经过的站点 + BLOCK_REASON = 6135 # 被阻挡的原因 + DECELERATION_REASON = 6136 # 机器人减速原因 + ROBOT_CUR_STATUS = 6137 # 状态指示 + + + +def get_worktop_status(): + """ + 获取操作台状态[0,0,88,0] + """ + with ModbusConnection( + Worktop.HOST, + Worktop.PORT + ) as master: + worktop_list = master.execute( + 1, + cst.READ_HOLDING_REGISTERS, + WorktopAddr.STATUS_ADDR_BEGIN, + WorktopAddr.STATUS_ADDR_LENGTH, + ) + print('获取旋转托盘状态:::',worktop_list) + # return parser_worktop_status(worktop_list) + + + +def get_drawer_status(): + ''' + 解析抽屉状态 + ''' + with ModbusConnection( + Cabinet.HOST, + Cabinet.PORT + ) as master: + drawer_short_list = master.execute( + 1, cst.READ_HOLDING_REGISTERS, ADDR_DRAWER, 1) + print('get_drawer_status:::', drawer_short_list) + # return parser_drawer_status(drawer_short_list[0]) + + +def get_cabinet_task_status(): + ''' + 解析任务状态 + ''' + with ModbusConnection( + Cabinet.HOST, + Cabinet.PORT + ) as master: + task_status_data_list = master.execute( + 1, cst.READ_HOLDING_REGISTERS, ADDR_CABINET, 1) + print('get_cabinet_task_status:::', task_status_data_list) + # return parser_task_status(task_status_data_list[0]) + + +def get_instrument_amr_status(): + ''' + 获取试剂amr 并解析AMR状态 + ''' + try: + with ModbusConnection( + AmrInstrument.HOST, + AmrInstrument.PORT + ) as master: + drawer_short_list = master.execute( + 1, + cst.READ_HOLDING_REGISTERS, + AmrAddr.STATUS_ADDR_BEGIN, + AmrAddr.STATUS_ADDR_LENGTH + ) + print('get_instrument_amr_status::',drawer_short_list) + # return parser_ams_status(int_list_to_bytes(drawer_short_list)) + except Exception as e : + # amr_status = AmrStatus() + # return amr_status + + print("get_instrument_amr_status Exception: ", e) + + +def get_drug_amr_status(): + ''' + 获取试剂amr 并解析AMR状态 + ''' + + with ModbusConnection( + AmrDrug.HOST, + AmrDrug.PORT + ) as master: + drawer_short_list = master.execute( + 1, + cst.READ_HOLDING_REGISTERS, + AmrAddr.STATUS_ADDR_BEGIN, + AmrAddr.STATUS_ADDR_LENGTH + ) + print('get_drug_amr_status::',drawer_short_list) + # return parser_ams_status(int_list_to_bytes(drawer_short_list)) + + + + +get_worktop_status() +get_drawer_status() +get_cabinet_task_status() +get_instrument_amr_status() +get_drug_amr_status() \ No newline at end of file diff --git a/plc/tools.py b/plc/tools.py new file mode 100644 index 0000000..fc236cc --- /dev/null +++ b/plc/tools.py @@ -0,0 +1,55 @@ +import struct + + +def int_list_to_bytes(int_list): + ''' + int列表转换为字节流 + ''' + byte_stream = b'' + for num in int_list: + byte_stream += struct.pack('>H', num) + return byte_stream + + +def bytes_to_int_list(byte_stream): + ''' + 字节流转换为int列表 + ''' + int_list = [] + for i in range(0, byte_stream.__len__(), 2): + int_list.append(struct.unpack('>H', byte_stream[i:i+2])[0]) + + return int_list + + +def get_bit(data: int, bit: int) -> int: + ''' + 获取某比特的值 + ''' + if data & (1 << bit): + return 1 + else: + return 0 + + +def get_bit_bool(data: int, bit: int) -> bool: + ''' + 获取某比特的值,并转换为布尔值 + ''' + return bool(get_bit(data, bit)) + + +def set_bit(number: int, pos: int, value: int): + ''' + 修改某比特的值,并返回新值 + ''' + + new_number = None + + if value == 1: + new_number = number | (1 << pos) + elif value == 0: + new_number = number & ~(1 << pos) + + return new_number + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..abf7e22 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[tool.aerich] +tortoise_orm = "settings.TORTOISE_ORM" +location = "./migrations" +src_folder = "./." diff --git a/run.sh b/run.sh new file mode 100644 index 0000000..7fee4f2 --- /dev/null +++ b/run.sh @@ -0,0 +1,2 @@ +#python11 ENV +python main.py --host 0.0.0.0 --reload \ No newline at end of file diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..c30972a --- /dev/null +++ b/settings.py @@ -0,0 +1,13 @@ +TORTOISE_ORM = { + # "connections": {"default": "mysql://root:123456@127.0.0.1:3306/shceduler?charset=utf8mb4"}, # MySQL + # "connections": {"default": "sqlite://db.sqlite3"}, # sqlite + "connections": {"default": "mysql://root:123456@127.0.0.1:3306/agv?charset=utf8mb4"}, # MySQL + "apps": { + # 模型分组名字,当需要使用此app下的模型的时候,需使用 此名字.模型名称 + "models": { + # 须添加"aerich.models", 此时,会在数据库中生成一个名为aerich的表用于存模型信息,以便以后做脚本迁移 + "models": ["aerich.models", "models"], # 模型所在的py文件 + "default_connection": "default" + } + } +} \ No newline at end of file