You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
538 lines
22 KiB
538 lines
22 KiB
import requests |
|
import time |
|
import os |
|
import platform |
|
import shutil |
|
import json |
|
import shlex |
|
import subprocess |
|
from concurrent.futures import ThreadPoolExecutor, as_completed |
|
from .oss_redis import ossClient |
|
from .logs import log |
|
from .oss_func import download_file_with_check, checkFileExists |
|
from .changeFiles import changeObjFile, changeMtlFile |
|
from .small_machine_transform import transform_save |
|
|
|
ENV = 'prod' |
|
url = 'https://mp.api.suwa3d.com' |
|
if ENV == 'dev': |
|
url = 'http://mp.api.dev.com' |
|
elif ENV == 'prod': |
|
url = 'https://mp.api.suwa3d.com' |
|
|
|
|
|
# 根据打印ID 获取下载目录 |
|
def getDownloadDirByPrintId(printIds): |
|
# 调用接口获取下载的路径 |
|
api_url = f"{url}/api/printOrder/getInfoByPrintIds" |
|
res = requests.post(api_url, json={"print_ids": printIds}) |
|
res = res.json() |
|
print(f"根据打印ID 获取下载目录, res={res}") |
|
if res["code"] == 1000: |
|
return res["data"] |
|
else: |
|
return False |
|
|
|
|
|
# 根据批次id,进行切片中的状态变更 |
|
def requestApiToUpdateSliceStatus(versionId, downloadCounts): |
|
api_url = f"{url}/api/printTypeSettingOrder/updateBatchSliceing?batch_id={versionId}&download_counts=" + str(downloadCounts) |
|
log(f'发起状态变更请求url={api_url}, versionId={versionId}') |
|
try: |
|
# 添加超时参数,防止请求时间过长 |
|
res = requests.post(api_url, timeout=60) # 60秒超时 |
|
if res.status_code != 200: |
|
log(f'状态变更请求失败, res={res.text}') |
|
return False |
|
log(f'状态变更请求成功, res={res.text}') |
|
return True |
|
except requests.exceptions.Timeout: |
|
log(f'状态变更请求超时, url={api_url}') |
|
return False |
|
except requests.exceptions.RequestException as e: |
|
log(f'状态变更请求异常, error={str(e)}') |
|
return False |
|
|
|
|
|
# 判断是否上传了 JSON 文件 |
|
def checkJsonFileExists(versionId): |
|
log(f"检测文件和图片是否存在, versionId={versionId}") |
|
jsonFilePath = f'batchPrint/{versionId}/{versionId}.json' |
|
# 判断oss 上是否存在 |
|
jpgFilePath = f'batchPrint/{versionId}/{versionId}.jpg' |
|
|
|
if not ossClient().object_exists(jsonFilePath): |
|
log(f'JSON文件不存在: {jsonFilePath}') |
|
return False, False |
|
|
|
if not ossClient().object_exists(jpgFilePath): |
|
log(f'JPG文件不存在: {jpgFilePath}') |
|
return False, False |
|
log(f"文件和图片检测成功,存在, versionId={versionId}") |
|
return jsonFilePath, jpgFilePath |
|
|
|
|
|
# 检测本地文件是否存在 |
|
def checkLocalFileExists(localFilePath): |
|
if not os.path.exists(localFilePath): |
|
return False |
|
return True |
|
|
|
|
|
# 读取JSON文件内容 |
|
def readJsonFile(localFilePath): |
|
with open(localFilePath, 'r', encoding='utf-8') as f: |
|
jsonData = json.load(f) |
|
return jsonData |
|
|
|
|
|
# 根据批次ID,获取批次信息 |
|
def getBatchInfo(versionId): |
|
url1 = f"{url}/api/printTypeSettingOrder/getBatchInfoAndPrintMachineInfoByBatchId?batch_id={versionId}" |
|
res = requests.get(url1) |
|
res = res.json() |
|
log(f"获取批次信息和打印机信息, url={url1}, res={res}") |
|
if res["code"] == 1000: |
|
return res["data"] |
|
else: |
|
return False |
|
|
|
|
|
# 下载文件,读取文件内容,并且文件迁移至正确的目录里,不再放在临时目录里 |
|
def downloadJsonAndJpgFileAndMoveToCorrectDir(versionId, currentDir): |
|
jsonFilePath, jpgFilePath = checkJsonFileExists(versionId) |
|
if jsonFilePath == False or jpgFilePath == False: |
|
return False, False |
|
|
|
# 将文件下载到临时目录,待会要判断是否什么类型机型 |
|
tempDir = os.path.join(currentDir, 'batchPrint', 'temp', versionId) |
|
if not os.path.exists(tempDir): |
|
os.makedirs(tempDir) |
|
localFilePath = os.path.join(tempDir, f'{versionId}.json') |
|
# 使用带完整性校验的下载方法下载JSON文件 |
|
ok = download_file_with_check(jsonFilePath, localFilePath) |
|
if not ok: |
|
log(f"JSON 文件下载失败或不完整, versionId={versionId}") |
|
return False, False |
|
# 下载JPG文件 |
|
localJpgFilePath = os.path.join(tempDir, f'{versionId}.jpg') |
|
ok = download_file_with_check(jpgFilePath, localJpgFilePath) |
|
if not ok: |
|
log(f"JPG 文件下载失败或不完整, versionId={versionId}") |
|
return False, False |
|
# 根据批次ID,获取批次信息和打印机信息 |
|
batchMachineInfo = getBatchInfo(versionId) |
|
if not batchMachineInfo: |
|
log(f"获取批次信息和打印机信息失败, versionId={versionId}") |
|
return False, False |
|
# "batch_info": batchInfo, |
|
# "print_machine_info": printMachineInfo, |
|
machineInfo = batchMachineInfo["print_machine_info"] |
|
print(f"machineInfo={machineInfo['id']}") |
|
dirNewName = "" |
|
if str(machineInfo["machine_type"]) == '1': |
|
dirNewName = os.path.join(currentDir, 'batchPrint', versionId + '_small_No' + str(machineInfo['id'])) |
|
else: |
|
dirNewName = os.path.join(currentDir, 'batchPrint', versionId + '_big_No' + str(machineInfo['id'])) |
|
|
|
# 判断目录是否存在,存在就删除 |
|
if os.path.exists(dirNewName): |
|
shutil.rmtree(dirNewName) |
|
# 创建目录 |
|
os.makedirs(dirNewName) |
|
|
|
# 创建json子目录 |
|
jsonSubDir = os.path.join(dirNewName, 'json') |
|
if not os.path.exists(jsonSubDir): |
|
os.makedirs(jsonSubDir) |
|
|
|
# 将数据移动过来 |
|
shutil.move(localFilePath, os.path.join(jsonSubDir, '3DPrintLayout.json')) |
|
shutil.move(localJpgFilePath, os.path.join(jsonSubDir, f'{versionId}.jpg')) |
|
# 检测文件是否移动成功 |
|
if not os.path.exists(os.path.join(jsonSubDir, '3DPrintLayout.json')): |
|
log(f"JSON文件不存在, versionId={versionId}") |
|
return False, False |
|
if not os.path.exists(os.path.join(jsonSubDir, f'{versionId}.jpg')): |
|
log(f"JPG文件不存在, versionId={versionId}") |
|
return False, False |
|
log(f"文件移动成功, versionId={versionId}") |
|
# 返回目录路径 |
|
return dirNewName, machineInfo |
|
|
|
|
|
# 整合json文件,读取对应的数据,返回数据结构 |
|
def getJsonData(dirNewName): |
|
jsonFilePath = os.path.join(dirNewName, 'json', '3DPrintLayout.json') |
|
if not os.path.exists(jsonFilePath): |
|
log(f"JSON文件不存在, dirNewName={dirNewName}") |
|
return False |
|
# 读取JSON文件内容 |
|
jsonData = readJsonFile(jsonFilePath) |
|
# 读取models |
|
models = jsonData.get('models', []) |
|
if not models: |
|
log(f"models不存在, dirNewName={dirNewName}") |
|
return False |
|
|
|
listData = [] |
|
# 遍历models |
|
for model in models: |
|
file_name = model.get('file_name', '') |
|
# 分割数据 |
|
arrFileName = file_name.split('_') |
|
orderId = arrFileName[0] |
|
pid = arrFileName[1] |
|
printId = arrFileName[2].replace("P", "") |
|
size = arrFileName[3] |
|
counts = arrFileName[4].replace("x", "").replace(".obj", "") |
|
|
|
# 检测这些数据 |
|
if not orderId or not pid or not printId or not size or not counts: |
|
log(f"数据不完整, orderId={orderId}, pid={pid}, printId={printId}, size={size}, counts={counts}") |
|
return False |
|
|
|
# 创建数据结构 |
|
modelInfo = { |
|
"orderId": orderId, |
|
"printId": printId, |
|
"pid": pid, |
|
"size": size, |
|
"counts": counts, |
|
"file_name": file_name, |
|
} |
|
listData.append(modelInfo) |
|
|
|
# 检测数据长度 |
|
if len(listData) == 0: |
|
log(f"数据长度为0, dirNewName={dirNewName}") |
|
return False |
|
# 返回数据结构 |
|
return listData |
|
|
|
|
|
# 读取 json 文件,获取 homo_matrix 数据,根据 file_name 获取 homo_matrix 数据 |
|
def getHomoMatrixByFileName(dirNewName, fileName): |
|
jsonFilePath = os.path.join(dirNewName, 'json', '3DPrintLayout.json') |
|
if not os.path.exists(jsonFilePath): |
|
log(f"JSON文件不存在, dirNewName={dirNewName}") |
|
return False |
|
# 读取JSON文件内容 |
|
jsonData = readJsonFile(jsonFilePath) |
|
if not jsonData: |
|
log(f"读取JSON文件内容失败, dirNewName={dirNewName}") |
|
return False |
|
|
|
for model in jsonData["models"]: |
|
log(f"jsonData={model['file_name']} == {fileName}") |
|
if model["file_name"] == fileName: |
|
log(f"model={model['transform']}") |
|
return model["transform"]["homo_matrix"] |
|
return False |
|
|
|
|
|
# 处理单个数据项:下载文件、修改关联路径、转换数据(如果是小机台) |
|
def _process_single_item(v, arrDownloadPath, dirNewName, dirPath, isSmallMachine): |
|
""" |
|
处理单个数据项的函数,用于多线程处理 |
|
参数: |
|
v: 单个数据项 |
|
arrDownloadPath: 下载路径数组 |
|
dirNewName: 目录名称 |
|
dirPath: 数据目录路径 |
|
isSmallMachine: 是否是小机台 |
|
返回: |
|
(success: bool, error_msg: str) |
|
""" |
|
try: |
|
# 查找匹配的下载路径信息 |
|
info = {} |
|
for tempv in arrDownloadPath: |
|
if str(tempv["print_order_id"]) == str(v["printId"]): |
|
info = tempv |
|
break |
|
|
|
if not info: |
|
return False, f"未找到匹配的下载路径信息, printId={v['printId']}" |
|
|
|
filePath = info["path"] |
|
pid = info["pid"] |
|
orderId = info["order_id"] |
|
printId = info["print_order_id"] |
|
size = info["real_size"] |
|
counts = info["quantity"] |
|
fileName = v["file_name"] |
|
|
|
# 判断文件是否存在 |
|
ossJpgFilePath = f"{filePath}/printId_{printId}Tex1.jpg" |
|
if not checkFileExists(ossJpgFilePath): |
|
ossJpgFilePath = f"{filePath}/{pid}Tex1.jpg" |
|
|
|
localJpgName = os.path.join(f"{orderId}_{pid}Tex1.jpg") |
|
loaclMtlName = os.path.join(f"{orderId}_{pid}.mtl") |
|
localObjName = fileName |
|
arrDownloadFiles = [ |
|
{"ossPath": ossJpgFilePath, "localPath": os.path.join(dirPath, localJpgName)}, |
|
{"ossPath": f"{filePath}/{pid}.obj", "localPath": os.path.join(dirPath, localObjName)}, |
|
{"ossPath": f"{filePath}/{pid}.mtl", "localPath": os.path.join(dirPath, loaclMtlName)}, |
|
] |
|
|
|
# 遍历下载文件 |
|
beginTime = time.time() |
|
objsLocal = "" |
|
for objFiles in arrDownloadFiles: |
|
downloadBeginTime = time.time() |
|
# 判断 mtl 和 jpg 文件是否存在,存在就不在下载了 |
|
if "mtl" in objFiles["localPath"] or "jpg" in objFiles["localPath"]: |
|
if os.path.exists(objFiles["localPath"]): |
|
continue |
|
|
|
downloadOk = download_file_with_check(objFiles["ossPath"], objFiles["localPath"]) |
|
if not downloadOk: |
|
error_msg = f"下载文件失败, ossPath={objFiles['ossPath']}, localPath={objFiles['localPath']}" |
|
log(error_msg) |
|
return False, error_msg |
|
|
|
log(f"下载文件耗时: {time.time() - downloadBeginTime}秒 - {objFiles['localPath']}") |
|
# 下载成功之后要修改文件之间的关联路径 |
|
if objFiles["localPath"].endswith(".obj"): |
|
beginChangeTime = time.time() |
|
objsLocal = objFiles["localPath"] |
|
changeObjFile(objFiles["localPath"], f"{orderId}_{pid}.mtl") |
|
log(f"修改obj关联耗时: {time.time() - beginChangeTime}秒 - {objFiles['localPath']}") |
|
if objFiles["localPath"].endswith(".mtl"): |
|
changeMtlFile(objFiles["localPath"], f"{orderId}_{pid}Tex1.jpg") |
|
|
|
endTime = time.time() |
|
log(f"下载文件和修改文件之间的关联路径 : 耗时{endTime - beginTime}秒 - {fileName}") |
|
|
|
# 如果是小机台,则要转换数据 |
|
if isSmallMachine: |
|
timeBegin = time.time() |
|
homo_matrix = getHomoMatrixByFileName(dirNewName, localObjName) |
|
if not homo_matrix: |
|
error_msg = f"获取homo_matrix失败, dirNewName={dirNewName}, objsLocal={objsLocal}" |
|
log(error_msg) |
|
return False, error_msg |
|
|
|
# 通过blender 调用执行 python 文件 |
|
blender_bin_path = findBpyModule() |
|
# 获取 small_machine_transform.py 的绝对路径 |
|
script_dir = os.path.dirname(os.path.abspath(__file__)) |
|
transform_script_path = os.path.join(script_dir, 'small_machine_transform.py') |
|
|
|
# 将 homo_matrix 转换为 JSON 字符串 |
|
homo_matrix_json = json.dumps(homo_matrix) |
|
|
|
# 检查必要文件是否存在 |
|
if not blender_bin_path or not os.path.exists(blender_bin_path): |
|
error_msg = f"Blender 可执行文件不存在: {blender_bin_path}" |
|
log(error_msg) |
|
return False, error_msg |
|
|
|
if not os.path.exists(transform_script_path): |
|
error_msg = f"转换脚本文件不存在: {transform_script_path}" |
|
log(error_msg) |
|
return False, error_msg |
|
|
|
if not os.path.exists(objsLocal): |
|
error_msg = f"OBJ 文件不存在: {objsLocal}" |
|
log(error_msg) |
|
return False, error_msg |
|
|
|
# 构建命令参数列表(使用列表形式,避免 shell 转义问题) |
|
cmd = [ |
|
blender_bin_path, |
|
'--background', |
|
'--python', |
|
transform_script_path, |
|
'--', |
|
f'--objPathName={objsLocal}', |
|
f'--trans={homo_matrix_json}' |
|
] |
|
log(f"准备执行 Blender 命令") |
|
log(f" Blender路径: {blender_bin_path}") |
|
log(f" 脚本路径: {transform_script_path}") |
|
log(f" OBJ文件: {objsLocal}") |
|
log(f" 完整命令: {' '.join(cmd[:4])} ... (参数已隐藏)") |
|
|
|
try: |
|
log(f"开始执行 Blender 转换命令...") |
|
# 使用 subprocess 执行命令,捕获输出和错误 |
|
# 在 Windows 上,Blender 输出可能是 UTF-8,需要指定编码并处理错误 |
|
result = subprocess.run( |
|
cmd, |
|
capture_output=True, |
|
text=True, |
|
encoding='utf-8', # 指定 UTF-8 编码 |
|
errors='replace', # 遇到无法解码的字符时替换为占位符,而不是抛出异常 |
|
timeout=300, # 5分钟超时 |
|
check=False # 不自动抛出异常,手动检查返回码 |
|
) |
|
|
|
log(f"Blender 命令执行完成, 返回码: {result.returncode}") |
|
|
|
# 无论成功失败都输出详细信息 |
|
if result.stdout: |
|
log(f"Blender stdout 输出: {result.stdout}") |
|
if result.stderr: |
|
log(f"Blender stderr 输出: {result.stderr}") |
|
|
|
if result.returncode != 0: |
|
error_output = result.stderr if result.stderr else result.stdout |
|
error_msg = f"调用blender 执行 python 文件失败, 返回码={result.returncode}, fileName={fileName}" |
|
if error_output: |
|
error_msg += f", 错误信息: {error_output[:10000000]}" # 增加错误信息长度限制 |
|
log(error_msg) |
|
return False, error_msg |
|
|
|
log(f"调用blender 执行 python 文件成功, fileName={fileName}") |
|
|
|
except subprocess.TimeoutExpired: |
|
error_msg = f"调用blender 执行超时(超过5分钟), fileName={fileName}" |
|
log(error_msg) |
|
return False, error_msg |
|
except Exception as e: |
|
error_msg = f"调用blender 执行时发生异常: {str(e)}, fileName={fileName}" |
|
log(error_msg) |
|
return False, error_msg |
|
|
|
timeEnd = time.time() |
|
log(f"转换数据时间: 耗时{timeEnd - timeBegin}秒 - {objsLocal}") |
|
|
|
return True, "" |
|
except Exception as e: |
|
error_msg = f"处理数据项时发生异常, fileName={v.get('file_name', 'unknown')}, error={str(e)}" |
|
log(error_msg) |
|
return False, error_msg |
|
|
|
|
|
# json文件进行下载对应的数据 和转换数据,传递目录路径 |
|
def downloadDataByOssAndTransformSave(dirNewName, isSmallMachine=False, max_workers=1): |
|
listData = getJsonData(dirNewName) |
|
if not listData: |
|
log(f"获取数据失败, dirNewName={dirNewName}") |
|
return False |
|
# 遍历数据 |
|
arrPrintId = [] |
|
arrPrintDataInfo = [] |
|
for modelInfo in listData: |
|
arrPrintId.append(modelInfo.get('printId')) |
|
arrPrintDataInfo.append({ |
|
"printId": modelInfo.get('printId'), |
|
"file_name": modelInfo.get('file_name'), |
|
}) |
|
|
|
# 调用接口获取下载的路径 |
|
arrDownloadPath = getDownloadDirByPrintId(arrPrintId) |
|
if not arrDownloadPath: |
|
log(f"获取下载路径失败, arrPrintId={arrPrintId}") |
|
return False |
|
|
|
dirPath = os.path.join(dirNewName, 'data') |
|
if not os.path.exists(dirPath): |
|
os.makedirs(dirPath) |
|
|
|
# 使用多线程并发处理数据 |
|
log(f"开始多线程处理数据, 共{len(listData)}个项目, 线程数={max_workers}") |
|
beginTime = time.time() |
|
|
|
# 使用线程池并发处理 |
|
try: |
|
with ThreadPoolExecutor(max_workers=max_workers) as executor: |
|
# 提交所有任务 |
|
future_to_item = { |
|
executor.submit(_process_single_item, v, arrDownloadPath, dirNewName, dirPath, isSmallMachine): v |
|
for v in listData |
|
} |
|
|
|
# 收集结果 |
|
success_count = 0 |
|
fail_count = 0 |
|
try: |
|
for future in as_completed(future_to_item): |
|
v = future_to_item[future] |
|
try: |
|
success, error_msg = future.result() |
|
if success: |
|
success_count += 1 |
|
log(f"处理成功: {v.get('file_name', 'unknown')} ({success_count}/{len(listData)})") |
|
else: |
|
fail_count += 1 |
|
log(f"处理失败: {v.get('file_name', 'unknown')}, 错误: {error_msg} ({fail_count}/{len(listData)})") |
|
# 如果任何一个任务失败,记录错误但继续处理其他任务 |
|
except KeyboardInterrupt: |
|
# 收到中断信号,取消所有未完成的任务 |
|
log(f"收到中断信号,正在取消未完成的任务...") |
|
for f in future_to_item: |
|
f.cancel() |
|
raise # 重新抛出,让外层捕获 |
|
except Exception as e: |
|
fail_count += 1 |
|
error_msg = f"处理数据项时发生异常: {str(e)}" |
|
log(f"处理异常: {v.get('file_name', 'unknown')}, 错误: {error_msg} ({fail_count}/{len(listData)})") |
|
except KeyboardInterrupt: |
|
# 收到中断信号,取消所有未完成的任务 |
|
log(f"收到中断信号,正在取消未完成的任务...") |
|
for f in future_to_item: |
|
f.cancel() |
|
# 尝试等待正在执行的任务完成(非阻塞方式) |
|
import concurrent.futures |
|
for f in list(future_to_item.keys()): |
|
if not f.done(): |
|
try: |
|
# 尝试获取结果,如果任务还在执行则立即返回 |
|
f.result(timeout=0.1) |
|
except (concurrent.futures.TimeoutError, concurrent.futures.CancelledError): |
|
pass |
|
except: |
|
pass |
|
raise # 重新抛出,让外层捕获 |
|
|
|
endTime = time.time() |
|
log(f"多线程处理完成, 总耗时{endTime - beginTime}秒, 成功:{success_count}, 失败:{fail_count}, 总计:{len(listData)}") |
|
|
|
# 如果有任何失败,返回 False |
|
if fail_count > 0: |
|
log(f"部分任务处理失败,共{fail_count}个失败") |
|
return False |
|
|
|
return True |
|
except KeyboardInterrupt: |
|
log(f"多线程处理被中断") |
|
raise # 重新抛出,让调用者处理 |
|
|
|
|
|
def findBpyModule(): |
|
# 返回 Blender 可执行文件路径 |
|
blender_bin_path = '/Applications/Blender.app/Contents/MacOS/Blender' |
|
# 判断当前是 windows 还是 macOS |
|
if platform.system() == 'Windows': |
|
# Windows 上常见的 Blender 安装路径 |
|
possible_paths = [ |
|
'C:\\Program Files\\Blender Foundation\\Blender 5.0\\blender.exe', |
|
'C:\\Program Files\\Blender Foundation\\Blender 4.4\\blender.exe', |
|
'C:\\Program Files\\Blender Foundation\\Blender 4.3\\blender.exe', |
|
'C:\\Program Files\\Blender Foundation\\Blender 4.2\\blender.exe', |
|
'C:\\Program Files\\Blender Foundation\\Blender 4.1\\blender.exe', |
|
'C:\\Program Files\\Blender Foundation\\Blender 4.0\\blender.exe', |
|
] |
|
# 查找存在的路径 |
|
for path in possible_paths: |
|
if os.path.exists(path): |
|
blender_bin_path = path |
|
break |
|
else: |
|
# 如果都没找到,使用默认路径 |
|
blender_bin_path = 'C:\\Program Files\\Blender Foundation\\Blender 5.0\\blender.exe' |
|
log(f"警告: 未找到 Blender 可执行文件,使用默认路径: {blender_bin_path}") |
|
else: |
|
blender_bin_path = '/Applications/Blender.app/Contents/MacOS/Blender' |
|
|
|
# 检查路径是否存在 |
|
if not os.path.exists(blender_bin_path): |
|
error_msg = f"Blender 可执行文件不存在: {blender_bin_path}" |
|
log(error_msg) |
|
raise FileNotFoundError(error_msg) |
|
|
|
return blender_bin_path
|
|
|