Compare commits

...
77 Commits
Author SHA1 Message Date
dongchangxi 0f962e407e v4 2025-12-19 17:28:09 +08:00
dongchangxi 6cbd7e1c1e v4 2025-12-19 17:23:59 +08:00
dongchangxi 3f08bac785 v3 2025-12-19 16:55:17 +08:00
dongchangxi 0ee8888a68 v3 2025-12-19 16:51:28 +08:00
dongchangxi 9231460ccc v2 2025-12-19 16:19:14 +08:00
dongchangxi 36bf02b1da v2 2025-12-19 16:15:31 +08:00
dongchangxi 296dc35880 v1 2025-12-19 16:07:41 +08:00
dongchangxi db129b7f2c 已存在直接切屁 2025-12-19 16:03:54 +08:00
dongchangxi 2ce2000147 新版切片软件 2025-12-19 13:32:26 +08:00
dongchangxi feb8cd380e 删除再移除该目录 2025-12-19 13:28:44 +08:00
dongchangxi 5236717598 关闭切片软件 2025-12-19 13:18:18 +08:00
dongchangxi 7053969422 内网服务 2025-12-19 11:29:26 +08:00
dongchangxi f70705724f 000 2025-12-16 19:17:53 +08:00
dongchangxi 7b19e55e74 8 2025-12-16 19:11:04 +08:00
dongchangxi 24e525ab70 exe7 2025-12-16 19:03:37 +08:00
dongchangxi 56d915aa6d exe6 2025-12-16 18:53:38 +08:00
dongchangxi 4d3f3cd6fd exe5 2025-12-16 18:45:39 +08:00
dongchangxi c34a7f6054 exe4 2025-12-16 18:38:29 +08:00
dongchangxi 37e44846ca exe3 2025-12-16 18:32:07 +08:00
dongchangxi 9b5cdac560 exe2 2025-12-16 18:27:59 +08:00
dongchangxi 05fcc26450 exe 2025-12-16 18:13:45 +08:00
dongchangxi ba469c48e2 打包问题处理 2025-12-16 18:08:53 +08:00
dongchangxi e66617d0c9 version1-0-0-1 2025-12-16 17:52:18 +08:00
dongchangxi 47bad0c0cb 最新版 2025-12-16 17:02:04 +08:00
dongchangxi b8f5091514 判断是否切片完成 2025-12-15 11:30:50 +08:00
dongchangxi 0b83a6bd6d 关闭切片程序 2025-12-12 10:25:06 +08:00
dongchangxi 37b78bfdf8 新版本2 2025-12-12 09:48:32 +08:00
dongchangxi 9efa8ddf4d 222 2025-12-12 09:43:13 +08:00
dongchangxi e67251153e 新版本 2025-12-12 09:18:56 +08:00
dongchangxi 160a06bdb6 4567789 2025-12-11 18:11:55 +08:00
dongchangxi bac04def8e 234 2025-12-11 17:30:35 +08:00
dongchangxi 881d0b8719 2222 2025-12-11 17:04:22 +08:00
dongchangxi b7843917ff 111111 2025-12-10 17:14:34 +08:00
dongchangxi 549c2b8f55 new exe 2025-12-10 16:21:22 +08:00
dongchangxi 3c4881c0e9 bug 2025-12-10 16:18:33 +08:00
dongchangxi 53877e254f 新的程序 2025-12-10 14:34:47 +08:00
dongchangxi bd09b33337 修改处理 2025-12-10 14:30:45 +08:00
dongchangxi e255cb3ccb 456 2025-12-10 14:11:11 +08:00
dongchangxi d9a691ea9a 22 2025-12-10 11:51:32 +08:00
dongchangxi 05a7fff1f2 1111 2025-12-10 11:42:09 +08:00
dongchangxi 968189dee9 合并程序 2025-12-10 11:39:10 +08:00
dongchangxi cc15c76e62 目录迁移 2025-12-09 16:34:35 +08:00
dongchangxi 2ea782ed93 1 2025-12-09 16:29:48 +08:00
dongchangxi 4e948f7df1 数据文件构建处理 2025-12-09 14:27:55 +08:00
dongchangxi a978fd89b8 1 2025-12-09 11:10:50 +08:00
dongchangxi dcf9eb6f23 111 2025-12-09 11:09:11 +08:00
dongchangxi 5134ece73b 1 2025-12-09 11:08:42 +08:00
dongchangxi 6cfa8a7c98 676767676767 2025-12-05 14:38:19 +08:00
dongchangxi 4549c06846 1111 2025-12-05 14:34:35 +08:00
dongchangxi d944502849 00000 2025-12-05 11:28:28 +08:00
dongchangxi c9867a4ddb 数据处理 2025-12-05 11:23:59 +08:00
dongchangxi e5187a1444 无效日志处理 2025-12-05 11:07:51 +08:00
dongchangxi 5743aa3059 12345678 2025-12-05 10:55:39 +08:00
dongchangxi df797bc906 bug修复 2025-12-05 10:52:13 +08:00
dongchangxi 9d15bdd3c3 新版本 2025-12-05 10:39:58 +08:00
dongchangxi 6578f4732f 多线程终止处理 2025-12-05 10:37:17 +08:00
dongchangxi fd1de8c3d0 切片下载数据处理 2025-12-05 10:27:43 +08:00
dongchangxi 8375d4d11f 切片新版本 2025-12-04 20:47:00 +08:00
dongchangxi d43a50e14d Refactor main.py to enable API call for updating slice status and enhance argument parsing. Update funcs.py for improved code readability and error handling in Blender execution. 2025-12-04 20:41:37 +08:00
dongchangxi 7ea8b2d199 222 2025-12-04 16:24:02 +08:00
dongchangxi 827f9847f8 更新下载 2025-12-04 15:39:48 +08:00
dongchangxi baa883ca7e 1 2025-12-04 11:26:47 +08:00
dongchangxi 9230c4c7bc 1 2025-12-04 11:24:57 +08:00
dongchangxi 0b70dc7c0c 切片下载V2 2025-12-04 11:24:15 +08:00
dongchangxi 829e9ed22d 56789 2025-11-28 18:33:53 +08:00
dongchangxi 914c442524 111 2025-11-28 18:23:17 +08:00
dongchangxi 6ba4546368 数据处理更新版本 2025-11-27 21:11:28 +08:00
dongchangxi 1262b65412 bug处理 2025-11-27 21:05:10 +08:00
dongchangxi 824a918659 更新批次程序 2025-11-27 15:18:47 +08:00
dongchangxi 5b7197a2ab 1 2025-11-27 14:11:10 +08:00
dongchangxi d014a633fd 10000 2025-11-26 14:21:40 +08:00
dongchangxi eae8ef5e24 1111 2025-11-26 14:17:13 +08:00
dongchangxi dc3130a747 9999 2025-11-26 13:56:49 +08:00
dongchangxi 77f9bc8948 jiaobe 2025-11-26 13:52:03 +08:00
dongchangxi 253d28f39f 88899 2025-11-26 13:34:11 +08:00
dongchangxi d9eeb4b6cd Merge branch 'master' of https://git.suwa3d.com/dongchangxi/make2 2025-11-26 13:26:13 +08:00
dongchangxi 718388a09d 44455 2025-11-26 13:25:41 +08:00
54 changed files with 35415 additions and 12270 deletions

No files matched your search

+28 -3
View File
@@ -15,8 +15,7 @@ test/test1.py
timer/29.zip
yj_local_build/main_step1.spec
yj_local_build/建模source_code.zip
apps/__pycache__/fix_up_color_two_a.cpython-310.pyc
.gitignore
apps/__pycache__/
apps/__pycache__/ps_image_shadow_up_ag_two_a.cpython-310.pyc
apps/__pycache__/white_purification_v3.cpython-310.pyc
apps/__pycache__/white_purification_v4.cpython-310.pyc
@@ -59,4 +58,30 @@ yj_local_build/xml.zip
openapi.zip
apps/__pycache__/
venv/
script/batchPrint/
script/batchPrint/
script/download_obj/
script/factory_sliceing/README_BUILD.md
script/factory_sliceing/batchPrint/
script/factory_sliceing_v2/tempData/
.github/workflows/build-windows-exe.yml
script/factory_sliceing/.DS_Store
script/factory_sliceing/__pycache__/download_print_out.cpython-311.pyc
script/factory_sliceing_v2/tempData/
script/factory_sliceing_v2/utils/__pycache__/
.DS_Store
script/auto_sliceing_operate/__pycache__/
script/test.py
script/test1.py
factory_sliceing/auto_sliceing_operate/__pycache__/
factory_sliceing/download_batch_data/utils/__pycache__/
factory_sliceing/auto_sliceing_operate/utils/__pycache__/
script/factory_sliceing/build/build_exe/
script/factory_sliceing_v2/build/build_exe/
factory_sliceing/download_batch_data/__pycache__/
factory_sliceing/build/factory_sliceing/localpycs/
factory_sliceing/build/factory_sliceing/
factory_sliceing/utils/__pycache__/__init__.cpython-311.pyc
factory_sliceing/utils/__pycache__/
factory_sliceing/auto_sliceing_operate/utils/__pycache__/
factory_sliceing/@AutomationLog.txt
factory_sliceing/auto_sliceing_operate/utils/__pycache__/logs.cpython-311.pyc
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+362 -3
View File
@@ -33,7 +33,7 @@ def main(pid,orderId,styleNumber):
print("弹出版本选择按钮已成功点击")
time.sleep(3)
# 使用XPath定位包含特定文本的div元素
target_text = "深度图批处理"
target_text = "3.小鱼AI(灰度图)图片浮雕流程-V3"
button = WebDriverWait(driver, 10).until(
EC.element_to_be_clickable((By.XPATH, f'//div[contains(text(), "{target_text}")]'))
)
@@ -177,7 +177,7 @@ if __name__ == '__main__':
orderId = 0
styleNumber = 0
downloadPath = "D:\downloadPhotos"
inputFolder = "c:\deep" #"C:\StableDiffusion_xiaoyu_V3\ComfyUI-aki-v1.6-XY\ComfyUI\input"
inputFolder = "C:\StableDiffusion_xiaoyu_V3\ComfyUI-aki-v1.6-XY\ComfyUI\input" #"c:\deep" #"C:\StableDiffusion_xiaoyu_V3\ComfyUI-aki-v1.6-XY\ComfyUI\input"
outputFolder = "C:\StableDiffusion_xiaoyu_V3\ComfyUI-aki-v1.6-XY\ComfyUI\output"
jsonFilePath = "C:\StableDiffusion_xiaoyu_V3\ComfyUI-aki-v1.6-XY\ComfyUI\pysssss-workflows"
# 设置浏览器驱动路径(以Chrome为例)
@@ -232,7 +232,8 @@ if __name__ == '__main__':
print(f"Redis数据格式错误,缺少必要字段: {info}")
utils.notify(f"Redis数据格式错误,缺少必要字段: {info}")
continue
print(info)
exit
pid = int(info['pid'])
orderId = int(info['order_id'])
styleNumber = int(info['style_number'])
@@ -265,3 +266,361 @@ if __name__ == '__main__':
r = utils.create_redis_connection()
continue
# from selenium import webdriver
# from selenium.webdriver.common.by import By
# from selenium.webdriver.common.keys import Keys
# from selenium.webdriver.chrome.service import Service
# from selenium.webdriver.support.ui import WebDriverWait
# from selenium.webdriver.support import expected_conditions as EC
# import time
# import os
# import sys
# import oss2
# import json
# import shutil
# import traceback
# import datetime
# from util import utils
# import requests
# # ===================== 核心修复:Chrome配置 =====================
# def init_chrome_driver(driver_path, chrome_binary_path=None):
# """初始化Chrome驱动,解决找不到Chrome二进制文件问题"""
# from selenium.webdriver.chrome.options import Options
# chrome_options = Options()
# # 1. 指定Chrome二进制文件路径(关键修复)
# if chrome_binary_path:
# chrome_options.binary_location = chrome_binary_path
# # 2. 添加通用优化参数
# chrome_options.add_argument("--no-sandbox") # 解决沙箱问题
# chrome_options.add_argument("--disable-dev-shm-usage") # 解决内存不足
# chrome_options.add_argument("--disable-gpu") # 禁用GPU加速(兼容老系统)
# chrome_options.add_argument("--window-size=1920,1080") # 设置窗口大小
# chrome_options.add_experimental_option("excludeSwitches", ["enable-logging"]) # 屏蔽无用日志
# # 3. 初始化驱动
# try:
# service = Service(executable_path=driver_path)
# driver = webdriver.Chrome(service=service, options=chrome_options)
# driver.set_page_load_timeout(60) # 设置页面加载超时
# return driver
# except Exception as e:
# raise Exception(f"Chrome驱动初始化失败:{str(e)}\n堆栈信息:{traceback.format_exc()}")
# # ===================== 原有业务函数修改 =====================
# def main(pid, orderId, styleNumber):
# driver = None
# try:
# # 进行一些初始化的处理
# flag = init(pid, orderId, styleNumber)
# if flag == False:
# err_msg = f"初始化步骤失败 pid:{pid} orderId:{orderId} styleNumber:{styleNumber}"
# print(err_msg)
# utils.notify(err_msg)
# return
# # 初始化浏览器(修复核心:指定Chrome二进制路径)
# chrome_binary_path = r"C:\Program Files\Google\Chrome\Application\chrome.exe" # 根据实际路径修改
# driver = init_chrome_driver(driver_path, chrome_binary_path)
# # 打开目标网页
# driver.get('http://127.0.0.1:8188') # 替换为你的目标网页地址
# # 等待页面加载完成
# time.sleep(5) # 等待2秒,确保页面加载完成
# # 定位按钮并点击
# # 1. 点击弹出版本选择按钮
# button = WebDriverWait(driver, 10).until(
# EC.element_to_be_clickable((By.CSS_SELECTOR, '#comfy-load-button .pysssss-workflow-arrow'))
# )
# button.click()
# print("弹出版本选择按钮已成功点击")
# time.sleep(3)
# # 2. 点击"深度图批处理"
# target_text = "深度图批处理"
# button = WebDriverWait(driver, 10).until(
# EC.element_to_be_clickable((By.XPATH, f'//div[contains(text(), "{target_text}")]'))
# )
# button.click()
# print("选择版本按钮已成功点击")
# time.sleep(3)
# # 3. 点击"Extra options"
# target_text = "Extra options"
# button = WebDriverWait(driver, 10).until(
# EC.element_to_be_clickable((By.XPATH, f'//label[contains(text(), "{target_text}")]'))
# )
# button.click()
# time.sleep(5)
# # 4. 点击队列按钮
# button = WebDriverWait(driver, 10).until(
# EC.element_to_be_clickable((By.ID, 'queue-button'))
# )
# button.click()
# print("队列按钮点击成功")
# # 获取当前时间戳
# timestamp = int(time.time())
# # 检测是否完成数据,一个死循环
# while True:
# boolV = checkIsFinish(driver, pid, orderId, styleNumber)
# if boolV:
# break
# else:
# # 计算时间戳是否超过5分钟了
# now = int(time.time())
# if now - timestamp >= 300:
# err_msg = f"pid:{pid} orderId:{orderId} 超过5分钟,任务还没结束,强行停止执行下一个任务"
# print(err_msg)
# utils.notify(err_msg)
# break
# time.sleep(5)
# continue
# except Exception as e:
# err_msg = f"pid:{pid} orderId:{orderId} styleNumber:{styleNumber} 执行错误:{str(e)}\n堆栈信息:{traceback.format_exc()}"
# print(err_msg)
# utils.notify(err_msg)
# finally:
# # 确保浏览器关闭
# if driver:
# try:
# driver.quit()
# except:
# pass
# def down_obj_from_oss(fileName, pid, styleNumber):
# """从OSS下载文件"""
# path = os.path.join(inputFolder, fileName)
# # 根据前缀获取文件列表
# prefix = f'photos/{pid}/cartoon/cartoon_image'
# filelist = oss2.ObjectIteratorV2(utils.oss(), prefix=prefix)
# print(f"filelist:{filelist} prefix{prefix}")
# isDownload = False
# for file in filelist:
# if file and hasattr(file, 'key') and f"{styleNumber}.png" in file.key:
# utils.oss().get_object_to_file(file.key, path)
# isDownload = True
# print(f"pid:{pid} styleNumber:{styleNumber} 文件下载成功")
# break
# return isDownload
# def init(pid, orderId, styleNumber):
# """初始化:清空文件夹、下载OSS文件、修改配置文件"""
# try:
# # 清除输入输出里的一些文件
# clearFolder(outputFolder)
# clearFolder(inputFolder)
# # 下载oss的文件到指定目录
# imageName = f"{pid}_{orderId}_{styleNumber}.png"
# downloadRes = down_obj_from_oss(imageName, pid, styleNumber)
# if downloadRes == False:
# print(f"pid:{pid} styleNumber:{styleNumber} OSS文件下载失败")
# return False
# # 修改配置文件:替换LoadImage的inputs[0]的value
# json_file = "3.小鱼AI(灰度图)图片浮雕流程-V3.json"
# json_origin_path = os.path.join(jsonFilePath, json_file) # 修正路径拼接
# with open(json_origin_path, 'r', encoding='utf-8') as f:
# data = json.load(f)
# # 遍历节点修改值
# for item in data.get('nodes', []):
# if item.get('id') == 277:
# if len(item.get('widgets_values', [])) > 0:
# item['widgets_values'][0] = imageName
# else:
# print(f"pid:{pid} 节点277的widgets_values为空,无法修改")
# return False
# # 写入修改后的配置文件
# with open(json_origin_path, 'w', encoding='utf-8') as f:
# json.dump(data, f, ensure_ascii=False, indent=4)
# print(f"pid:{pid} orderId:{orderId} styleNumber:{styleNumber} 配置文件修改成功")
# return True
# except Exception as e:
# print(f"初始化失败:{str(e)}")
# utils.notify(f"pid:{pid} orderId:{orderId} 初始化失败:{str(e)}")
# return False
# def checkIsFinish(driver, pid, orderId, styleNumber):
# """检测任务是否完成:检查输出文件夹、上传OSS"""
# try:
# folder = outputFolder
# # 检查文件夹是否存在
# if not os.path.exists(folder):
# print(f"输出文件夹不存在:{folder}")
# return False
# file_list = os.listdir(folder)
# fileLength = len(file_list)
# uploadCounts = 0
# files = ""
# if fileLength == 2:
# # 上传文件到指定的目录
# for file in file_list:
# files += file + ","
# file_path = os.path.join(folder, file)
# if not os.path.exists(file_path):
# continue
# # 上传TIFF文件
# if "tiff" in file.lower(): # 忽略大小写
# ossPath = f"photos/{pid}/cartoon_deep/{styleNumber}/{styleNumber}.tiff"
# time.sleep(5)
# utils.oss().put_object_from_file(ossPath, file_path)
# uploadCounts += 1
# # 上传PNG文件
# elif "png" in file.lower():
# ossPath = f"photos/{pid}/cartoon_deep/{styleNumber}/{styleNumber}.png"
# time.sleep(5)
# utils.oss().put_object_from_file(ossPath, file_path)
# uploadCounts += 1
# if uploadCounts == 2:
# success_msg = f"pid={pid},orderid={orderId},style_number:{styleNumber} 任务处理完成,深度图已上传到OSS"
# print(success_msg)
# # 推入到队列中
# r = utils.create_redis_connection()
# if pid > 1000:
# r.lpush('model:badge_cartoon_build',
# json.dumps({'pid': pid, 'order_id': orderId, 'style_number': styleNumber}))
# return True
# else:
# err_msg = f"pid={pid},orderid={orderId},style_number:{styleNumber} 任务处理失败,当前文件数据为{files},仅上传{uploadCounts}个文件"
# print(err_msg)
# utils.notify(err_msg)
# return False
# except Exception as e:
# err_msg = f"检测任务完成状态失败:{str(e)}"
# print(err_msg)
# utils.notify(err_msg)
# return False
# def clearFolder(folder):
# """清除指定文件夹下的所有文件(增强容错)"""
# if not os.path.exists(folder):
# os.makedirs(folder)
# return
# for file in os.listdir(folder):
# file_path = os.path.join(folder, file)
# try:
# if os.path.isdir(file_path):
# shutil.rmtree(file_path)
# else:
# os.remove(file_path)
# except Exception as e:
# print(f"删除文件/文件夹失败 {file_path}:{str(e)}")
# def checkInputFolder():
# """检测输入目录里是否有输入的文件存在"""
# folder = inputFolder
# if not os.path.exists(folder):
# return False
# fileLength = os.listdir(folder)
# return len(fileLength) == 1
# # ===================== 主程序入口修改 =====================
# if __name__ == '__main__':
# # 全局变量定义(修正路径格式,使用原始字符串避免转义)
# pid = 0
# orderId = 0
# styleNumber = 0
# downloadPath = r"D:\downloadPhotos"
# inputFolder = r"C:\deep" # 实际路径请确认
# outputFolder = r"C:\StableDiffusion_xiaoyu_V3\ComfyUI-aki-v1.6-XY\ComfyUI\output"
# jsonFilePath = r"C:\StableDiffusion_xiaoyu_V3\ComfyUI-aki-v1.6-XY\ComfyUI\pysssss-workflows"
# driver_path = r"D:\chromedriver-win64\chromedriver.exe" # ChromeDriver路径
# # 检查并创建文件夹
# for folder in [inputFolder, outputFolder, jsonFilePath, downloadPath]:
# if not os.path.exists(folder):
# os.makedirs(folder)
# print(f"创建文件夹:{folder}")
# # 处理命令行参数
# arrArgs = sys.argv
# if len(arrArgs) == 4:
# pid = arrArgs[1]
# orderId = arrArgs[2]
# styleNumber = arrArgs[3]
# # 判断是否是数字
# if pid.isdigit() and orderId.isdigit() and styleNumber.isdigit():
# main(int(pid), int(orderId), int(styleNumber)) # 转为int避免字符串比较问题
# else:
# print("输入的参数不是数字")
# else:
# # 从Redis消费任务
# while True:
# try:
# r = utils.create_redis_connection()
# info = r.spop('model:badge_cartoon')
# if info is None:
# print('队列为空,等待10秒')
# time.sleep(10)
# continue
# # 确保info是字符串类型
# if isinstance(info, bytes):
# info = info.decode('utf-8')
# # 解析JSON
# info_dict = json.loads(info)
# print(f"从Redis获取任务:{info_dict}")
# # 检查必要的字段是否存在
# if not all(key in info_dict for key in ['pid', 'order_id', 'style_number']):
# err_msg = f"Redis数据格式错误,缺少必要字段: {info_dict}"
# print(err_msg)
# utils.notify(err_msg)
# continue
# # 转换为整数
# pid = int(info_dict['pid'])
# orderId = int(info_dict['order_id'])
# styleNumber = int(info_dict['style_number'])
# print(f"开始处理任务 - pid:{pid} orderId:{orderId} style_number:{styleNumber}")
# # 创建临时文件标记
# tempFile = os.path.join(downloadPath, f"{pid}_{orderId}_{styleNumber}.txt")
# with open(tempFile, 'w', encoding='utf-8') as f:
# f.write(f"任务开始时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
# # 执行主任务
# main(pid, orderId, styleNumber)
# except json.JSONDecodeError as e:
# err_msg = f'JSON解析错误:{str(e)}\n原始数据:{info if "info" in locals() else "未知"}\n堆栈:{traceback.format_exc()}'
# print(err_msg)
# utils.notify(err_msg)
# # 写入错误标记文件
# if 'pid' in locals() and 'orderId' in locals() and 'styleNumber' in locals():
# tempFile = os.path.join(downloadPath, f"{pid}_{orderId}_{styleNumber}_解析错误.txt")
# with open(tempFile, 'w', encoding='utf-8') as f:
# f.write(err_msg)
# time.sleep(10)
# continue
# except Exception as e:
# err_msg = f'执行任务异常:{str(e)}\n堆栈:{traceback.format_exc()}'
# print(err_msg)
# utils.notify(err_msg)
# # 修复:异常对象转字符串后写入文件
# if 'pid' in locals() and 'orderId' in locals() and 'styleNumber' in locals():
# tempFile = os.path.join(downloadPath, f"{pid}_{orderId}_{styleNumber}_异常错误.txt")
# with open(tempFile, 'w', encoding='utf-8') as f:
# f.write(err_msg)
# time.sleep(10)
# continue
+47
View File
@@ -0,0 +1,47 @@
# Factory Sliceing 项目
## 安装依赖
在运行项目之前,需要先安装依赖:
```bash
# 使用 pip 安装
pip install -r requirements.txt
# 或者使用 pip3
pip3 install -r requirements.txt
# 或者使用 python -m pip
python -m pip install -r requirements.txt
```
### 主要依赖
- `toml` - 配置文件解析(必需)
- `redis` - Redis 客户端
- `oss2` - 阿里云 OSS 客户端
- `requests` - HTTP 请求库
- `uiautomation` - UI 自动化(Windows)
- `pywin32` - Windows API(仅 Windows 系统需要)
- `minio` - MinIO 客户端(如果使用)
## 快速开始
1. **安装依赖**
```bash
pip install -r requirements.txt
```
2. **配置配置文件**
- 复制 `config.example.toml` 为 `config.toml`
- 根据实际情况修改配置
3. **运行程序**
```bash
python main.py <command> [work_dir]
```
## 打包为 exe
详见 [README_BUILD.md](README_BUILD.md)
+139
View File
@@ -0,0 +1,139 @@
# 打包说明
## 方法一:使用 Python 脚本打包(推荐)
### Windows
```bash
python build_exe.py
```
### 打包后清理临时文件
```bash
python build_exe.py --clean
```
### 仅清理临时文件
```bash
python build_exe.py --clean-only
```
## 方法二:使用批处理文件(Windows)
直接双击运行 `build_exe.bat` 或在命令行执行:
```bash
build_exe.bat
```
## 方法三:手动使用 PyInstaller
```bash
# 安装 PyInstaller(如果未安装)
pip install pyinstaller
# 打包
pyinstaller --name=factory_sliceing --onefile --console --clean main.py
```
## 使用打包后的 exe
打包完成后,exe 文件位于 `dist/factory_sliceing.exe`
### 配置文件
**重要**: 打包后的 exe 需要配置文件 `config.toml` 才能正常运行。
- **配置文件位置**: 必须放在 **exe 同级目录**(与 `factory_sliceing.exe` 同一目录)
- **自动复制**: 打包脚本会自动将 `config.toml` 复制到 `dist/` 目录
- **如果配置文件不存在**: 打包脚本会尝试复制 `config.example.toml` 作为模板,你需要根据实际情况修改
配置文件示例:
```toml
[redis]
host = "your-redis-host"
password = "your-redis-password"
port = 6379
db = 6
[oss]
access_key_id = "your-access-key-id"
access_key_secret = "your-access-key-secret"
endpoint = "oss-cn-shanghai.aliyuncs.com"
bucket = "your-bucket-name"
[api]
base_url = "https://mp.api.suwa3d.com"
env = "prod"
[work]
default_dir = "C:\\work" # 默认工作目录
```
### 使用方法
```bash
factory_sliceing.exe <command> [work_dir]
```
可用命令:
- `batch_download` - 批量下载
- `begin_sliceing` - 开始切片
- `download_zip` - 下载压缩包
参数说明:
- `command`: 必需,要执行的命令
- `work_dir`: 可选,工作目录。如果未提供,将从配置文件读取 `work.default_dir`,如果配置文件中也没有,则使用 exe 所在目录
示例:
```bash
# 使用命令行参数指定工作目录
factory_sliceing.exe begin_sliceing C:\work\data
# 使用配置文件中的默认工作目录
factory_sliceing.exe begin_sliceing
```
## 注意事项
1. **依赖项**: 确保所有依赖已安装(redis, oss2, requests, uiautomation, pywin32 等)
2. **隐藏导入**: 打包脚本已自动包含常见模块(uiautomation, win32gui 等),如果运行时仍提示模块未找到,可以在 `build_exe.py` 中添加 `--hidden-import` 参数
3. **数据文件**:
- `small_machine_transform.py` 已自动包含在打包脚本中(作为数据文件)
- 如果项目需要其他配置文件或数据文件,需要添加 `--add-data` 参数
4. **文件大小**: 打包后的 exe 文件可能较大(通常 50-200MB),这是正常的,因为包含了 Python 解释器和所有依赖
5. **uiautomation**: 已自动包含在打包脚本中,使用 `--collect-all=uiautomation` 确保完整打包
6. **small_machine_transform.py**: 已自动包含在打包脚本中,会被 Blender 作为外部脚本调用
## 故障排除
### 问题:打包后 exe 无法运行
- 检查是否所有依赖都已正确安装
- 尝试在命令行运行 exe 查看错误信息
- 检查是否有隐藏导入的模块未添加
### 问题:模块未找到错误(如 uiautomation, win32gui, minio)
- **已修复**:
- `uiautomation` 模块已添加到打包脚本中,使用 `--collect-all=uiautomation`
- `win32gui` 等 pywin32 模块已添加,使用 `--collect-all=pywin32` 确保包含所有 DLL 文件
- `minio` 模块已添加,使用 `--collect-all=minio` 确保包含所有子模块
- 如果遇到其他模块未找到,在 `build_exe.py` 中添加对应的 `--hidden-import` 参数
- 或者使用 `--collect-all=<模块名>` 参数收集所有子模块和 DLL 文件
- 常见问题模块:`uiautomation`, `win32gui`, `win32con`, `win32api`, `win32process`, `psutil`, `minio`
- **注意**:
- pywin32 包含很多 DLL 文件,必须使用 `--collect-all=pywin32` 才能正确打包
- minio 有多个子模块,使用 `--collect-all=minio` 确保完整打包
### 问题:文件过大
- 这是正常的,PyInstaller 会打包 Python 解释器和所有依赖
- 可以使用 `--exclude-module` 排除不需要的模块来减小体积
### 问题:转换脚本文件不存在(small_machine_transform.py)
- **已修复**: `small_machine_transform.py` 已自动包含在打包脚本中
- 如果仍然提示文件不存在,检查 `build_exe.py` 中的 `--add-data` 参数是否正确
- 文件会被放在 `_MEIPASS/utils/` 和 `_MEIPASS/download_batch_data/utils/` 目录下
### 问题:配置文件不存在或读取失败
- **配置文件位置**: 必须放在 exe 同级目录(与 `factory_sliceing.exe` 同一目录)
- 打包脚本会自动复制 `config.toml` 到 `dist/` 目录
- 如果配置文件不存在,程序会使用默认值或提示错误
- 可以复制 `config.example.toml` 并重命名为 `config.toml`,然后根据实际情况修改
@@ -0,0 +1,3 @@
# auto_sliceing_operate package
# 注意:不在这里导入子模块,避免循环导入
# 使用 from auto_sliceing_operate.main_xxx import main 的方式导入
@@ -0,0 +1,116 @@
from auto_sliceing_operate.utils.import_all_file import modify_file_dialog_path_and_import_all
from auto_sliceing_operate.utils.click_soft_button import clickFileIMportShow, clickBegingSlice,checkIsSliceingText
import time
from auto_sliceing_operate.utils.oss_redis import redisClient
from auto_sliceing_operate.utils.oss_redis_intranet import redisClientIntranet
from auto_sliceing_operate.utils.exe_operate import start_exe, click_confirm, close
from auto_sliceing_operate.utils.logs import log
import os
from auto_sliceing_operate.utils.miniIo import upload_file
from auto_sliceing_operate.utils.request import requestApiToUpdateSliceStatusComplate
from utils.config import cfg
def upLoadSliceFileToOSS(data):
exe_path = cfg('exe.small_exe', None)
if not exe_path:
print(f"错误:exe_path 未配置")
exit(1)
dataFolder = exe_path.replace('NormalTek.ThreeDPrinter.UI.exe','data')
if not os.path.exists(dataFolder):
print(f"错误:dataFolder 文件夹不存在 - {dataFolder}")
exit(1)
dataFilePath = ""
print(f"开始上传文件: {dataFolder},{os.listdir(dataFolder)}")
for file1 in os.listdir(dataFolder):
if os.path.isdir(os.path.join(dataFolder,file1)):
dataFilePath = os.path.join(dataFolder,file1, 'data.snslc')
if os.path.exists(dataFilePath):
print(f"开始上传文件: {dataFilePath},文件存在")
return upload_file(f"slice/complate/{data}/data.snslc", dataFilePath)
print(f"开始上传文件: {dataFilePath},文件不存在")
return False
#"C:/test/10192_small_No4/data"
def BeginSliceing(batchId,machineId, folderPath,data):
# 打开3d切片软件
start_exe(data)
time.sleep(5)
#点击确认按钮
# click_confirm()
#先打开导入文件的弹框
clickFileIMportShow()
time.sleep(1)
#在输入路径导入obj文件
isSuccess = modify_file_dialog_path_and_import_all(folderPath)
if not isSuccess:
print("导入文件失败")
exit()
#点击开始切片
time.sleep(60)
log("开始切片")
clickBegingSlice()
#开启是循环,判断切片是否完成
while True:
time.sleep(60)
sliceTime = checkIsSliceingText()
if sliceTime is not True:
print(f"还未切完,切片时长: {sliceTime} 分钟")
time.sleep(60)
elif sliceTime is True:
break
# 开启死循环,判断文件是否全部导入成功
log("切片结束")
time.sleep(5)
res = upLoadSliceFileToOSS(data)
if not res:
print("上传切片文件失败")
exit(1)
#切片完成之后,将切片文件打包成zip文件,上传到OSS,并且请求api 更新批次状态为切片完成
# requestApiToUpdateSliceStatusComplate(batchId, 0)
# sadd 插入对应的队列
rIntranet = redisClientIntranet()
rIntranet.sadd('pb:begin_print_machine_'+machineId, data)
#关闭切片软件
close()
# def main(work_dir=None):
# r = redisClient()
# while True:
# #判断队列连接是否正常,进行重连
# if not r.ping():
# log("队列连接异常,进行重连")
# r = redisClient()
# time.sleep(10)
# continue
# data = r.spop('pb:to_sliceing')
# if data is None:
# log("队列为空")
# time.sleep(10)
# continue
# data = data.decode('utf-8')
# # data = "10192_small_No4"
# # 判断是否是字符串
# if not isinstance(data, str):
# log("取出的数据不是字符串")
# time.sleep(10)
# continue
# folderPath = os.path.join(work_dir,"batchPrint", data,"data")
# # 判断目录是否存在
# if not os.path.exists(folderPath):
# log(f"目录不存在: {folderPath}")
# time.sleep(10)
# continue
# # data 格式为 batchId_machineType_No machineId
# batchId = data.split('_')[0] #批次ID
# # machineType = data.split('_')[1] #机型类型
# machineId = data.split('_')[2].replace('No', '') #机器ID
# BeginSliceing(batchId,machineId,folderPath,data)
# time.sleep(10)
@@ -0,0 +1,79 @@
from auto_sliceing_operate.utils import miniIo as mio
import zipfile,time
import os
from auto_sliceing_operate.utils.oss_redis import redisClient
from auto_sliceing_operate.utils.oss_redis_intranet import redisClientIntranet
from auto_sliceing_operate.utils.logs import log
from utils.config import cfg
# 打印机旁边的电脑下载切片文件到本地
# 解压压缩包,将数据移动到指定的目录
#
#
#
# 从 minio 服务上下载 zip 文件到指定目录和解压
def downloadZip(ossZipPath, localZipPath):
# 下载 zip 文件
mio.download_file(ossZipPath, localZipPath)
def unzip(localZipPath, localUnzipPath):
# 解压 zip 文件
with zipfile.ZipFile(localZipPath, 'r') as zip_ref:
zip_ref.extractall(localUnzipPath)
# 解压完成后删除 zip 文件
os.remove(localZipPath)
def getCurrentMachineId():
# 获取当前电脑的id
machineId = cfg('machine.id', None)
if machineId is None:
log("获取当前电脑id失败")
exit(0)
return machineId
def main(work_dir=None):
downloadPath = cfg('machine.download_path', None)
if downloadPath is None:
log("工作目录不能为空")
exit(0)
if not os.path.exists(downloadPath):
os.makedirs(downloadPath)
rIntranet = redisClientIntranet()
machineId = getCurrentMachineId()
if machineId is None:
log("获取当前电脑id失败")
exit(0)
while True:
data = rIntranet.spop('pb:begin_print_machine_'+machineId)
if data is None:
log("队列为空")
time.sleep(10)
continue
data = data.decode('utf-8')
# 判断是否是数字
if data.isdigit():
log("取出的数据是数字")
time.sleep(10)
continue
# batchId = str(data)
ossZipPath = f'slice/complate/{data}/data.snslc'
localPath = os.path.join(downloadPath,data, f'data.snslc')
# 判断目录是否存在,不存在就创建
if not os.path.exists(os.path.join(downloadPath)):
os.makedirs(os.path.join(downloadPath))
else:
# 删除目录下指定文件,如果存在
if os.path.exists(os.path.join(downloadPath,data, f'data.snslc')):
os.remove(os.path.join(downloadPath,data, f'data.snslc'))
# 下载 zip 文件
downloadZip(ossZipPath, localPath)
# # 解压 zip 文件
# localUnzipPath = os.path.join(work_dir,batchId)
# unzip(localZipPath, localUnzipPath)
# # 触发打印处理
@@ -0,0 +1,10 @@
from auto_sliceing_operate.utils.miniIo import upload_file
# 测试上传文件
def test_upload_file():
upload_file("test.txt", "test.txt")
if __name__ == "__main__":
test_upload_file()
@@ -0,0 +1 @@
AAAAAAAAAAA
@@ -0,0 +1,2 @@
# utils package
@@ -0,0 +1,371 @@
import uiautomation as auto
from utils.config import cfg
import time
import math
softName = cfg('exe.soft_name', None)
# 遍历所有控件 找到
def findAndClick(control, depth=0,index=1):
for child in control.GetChildren():
# print(child)
# print(' ' * depth + f"{child.ControlType}: {child.Name} | {child.AutomationId}")
if child.Name == "切片及打印":
# 点击和 child 同组的第二个元素(即 control 的子控件中的第二个)
siblings = control.GetChildren() # 获取所有同级元素
# print(f"\n找到目标控件: {child.Name}")
# print(f"同级元素数量: {len(siblings)}")
# 打印所有同级元素信息,方便调试
for i, sibling in enumerate(siblings):
marker = " <-- 这是找到的目标" if sibling == child else ""
print(f" 同级元素[{i}]: {sibling.ControlType} | {sibling.Name} | {sibling.AutomationId}{marker}")
# 确保有至少2个同级元素
if len(siblings) >= 2:
target = siblings[index] # 第二个元素(索引从0开始)
print(f"\n准备点击同级{index+1}个元素:")
print(f" 控件类型: {target.ControlType}")
print(f" 控件名称: {target.Name}")
print(f" 自动化ID: {target.AutomationId}")
# 尝试多种点击方式
try:
# 方法1: 直接点击
target.Click()
print("✓ 使用 Click() 方法点击成功")
return True
except Exception as e:
print(f"✗ Click() 失败: {e}")
try:
# 方法2: 使用 Invoke(如果是可调用的控件)
target.Invoke()
print("✓ 使用 Invoke() 方法点击成功")
return True
except Exception as e2:
print(f"✗ Invoke() 失败: {e2}")
try:
# 方法3: 先设置焦点再点击
target.SetFocus()
import time
time.sleep(0.1)
target.Click()
print("✓ 使用 SetFocus() + Click() 方法点击成功")
return True
except Exception as e3:
print(f"✗ SetFocus() + Click() 失败: {e3}")
# 方法4: 使用鼠标点击坐标
try:
rect = target.BoundingRectangle
# BoundingRectangle 是一个矩形对象,计算中心点
x = rect.left + ((rect.right - rect.left) / 2)
y = rect.top + ((rect.bottom - rect.top) / 2)
auto.Click(x, y)
print(f"✓ 使用坐标点击成功: ({x}, {y})")
return True
except Exception as e4:
print(f"✗ 坐标点击失败: {e4}")
else:
print(f"✗ 同级元素数量不足,只有 {len(siblings)} 个")
return False
else:
findAndClick(child, depth + 1,index=index)
# 点击导入文件按钮
def clickFileIMportShow():
if softName is None:
print(f"错误:软件名称未配置")
exit(1)
control = auto.WindowControl(searchDepth=1, Name=softName)
clickRes = findAndClick(control,0,index=1)
return clickRes
# 点击开始切片按钮
def clickBegingSlice():
if softName is None:
print(f"错误:软件名称未配置")
exit(1)
control = auto.WindowControl(searchDepth=1, Name=softName)
clickRes = findAndClick(control,0,index=2)
print(f"点击开始切片按钮结果: {clickRes}")
return clickRes
# 全局变量:存储已找到的.obj文件名
arrObjName = []
scroll_direction = "up"
scroll_amount = 360
def findObjFiles(control, depth=0):
"""递归查找所有包含.obj的控件,并添加到全局数组中(去重)"""
for child in control.GetChildren():
if ".obj" in child.Name and child.Name not in arrObjName:
arrObjName.append(child.Name)
print(f"找到文件: {child.Name}")
findObjFiles(child, depth + 1)
def findListControl(item_control, max_depth=5):
"""向上查找列表控件(ListView等)"""
current = item_control
for _ in range(max_depth):
try:
parent = current.GetParentControl()
if not parent:
break
# 检查是否是列表控件
is_list_control = (
parent.ControlType in [auto.ControlType.ListControl, auto.ControlType.DataGridControl] or
"list" in str(parent.ControlType).lower() or
"50028" in str(parent.AutomationId)
)
if is_list_control:
return parent
current = parent
except:
break
return None
def scrollListView(list_control):
"""滚动列表控件
Args:
list_control: 列表控件对象
scroll_direction: "up" 向上滚动, "down" 向下滚动
Returns:
bool: 是否滚动成功
"""
try:
import win32api
import win32con
# 获取列表控件中心位置
rect = list_control.BoundingRectangle
center_x = int(rect.left + (rect.right - rect.left) / 2)
center_y = int(rect.top + (rect.bottom - rect.top) / 2)
# 移动鼠标到列表中心
win32api.SetCursorPos((center_x, center_y))
time.sleep(0.1)
# 根据方向决定滚动量(负数向上,正数向下)
# scroll_amount = -360 if scroll_direction == "up" else 360
# if scroll_direction == "up":
# scroll_amount = -scroll_amount
scroll_count = 3 # 滚动次数
scroll_interval = 0.1 # 每次滚动间隔
# 向上取整
scroll_count = abs(math.ceil(scroll_amount / 360))
# 执行滚动
for _ in range(scroll_count):
win32api.mouse_event(win32con.MOUSEEVENTF_WHEEL, 0, 0, scroll_amount, 0)
time.sleep(scroll_interval)
time.sleep(0.2)
direction_text = "向上" if scroll_amount < 0 else "向下"
print(f"✓ 使用鼠标滚轮滚动成功,方向: {direction_text}")
return True
except Exception as e:
print(f"✗ 滚动列表失败: {e}")
return False
def clickAndScrollFirstObj(control, depth=0):
"""找到第一个.obj文件,点击并滚动列表
Args:
control: 控件对象
depth: 递归深度
Returns:
bool: 是否成功找到并点击
"""
for child in control.GetChildren():
if ".obj" in child.Name:
# 获取第一个子控件作为点击目标
siblings = control.GetChildren()
if not siblings:
continue
target = siblings[0]
# 尝试点击
try:
target.Click()
print("✓ 使用 Click() 方法点击成功")
# 等待点击生效
time.sleep(0.2)
# 查找列表控件并滚动
list_control = findListControl(child)
if list_control:
print(f"找到列表控件: {list_control.ControlType} - {list_control.AutomationId}")
print("开始滚动列表...")
scrollListView(list_control)
else:
print("未找到列表控件,跳过滚动")
return True
except Exception as e:
print(f"✗ Click() 失败: {e}")
return True
# 递归查找
if clickAndScrollFirstObj(child, depth + 1):
return True
return False
# 获取所有窗口信息
def getAllWindowsInfo():
windows_info = []
try:
import win32gui
def enum_windows_callback(hwnd, windows):
try:
if win32gui.IsWindowVisible(hwnd):
window_title = win32gui.GetWindowText(hwnd)
class_name = win32gui.GetClassName(hwnd)
# 获取最顶层窗口
top_hwnd = win32gui.GetForegroundWindow()
is_top = (hwnd == top_hwnd)
windows.append({
'hwnd': hwnd,
'title': window_title,
'class_name': class_name,
'is_visible': True,
'is_top': is_top
})
except:
pass
return True
win32gui.EnumWindows(enum_windows_callback, windows_info)
# 按是否最顶层排序(最顶层在前)
windows_info.sort(key=lambda x: (not x['is_top'], x['title']))
return windows_info
except ImportError:
print("错误: win32gui 未安装,无法获取窗口信息")
return []
except Exception as e:
print(f"获取窗口信息时发生异常: {e}")
import traceback
traceback.print_exc()
return []
# 检测是否存在标题为"生成代码信息"的弹框,并获取其文本信息
def checkIsSliceingText():
try:
import win32gui
# 获取所有窗口信息
all_windows = getAllWindowsInfo()
# 打印所有窗口信息
print(f"当前所有可见窗口(共 {len(all_windows)} 个):")
for win_info in all_windows:
if "SlicingInfoDialog" in win_info['title']:
#获取控件对象,获取到时间信息,并返回
control = auto.WindowControl(searchDepth=1, hwnd=win_info['hwnd'])
a = getWindowText(control)
return a[0]['text']
return True
except ImportError:
print("错误: win32gui 未安装,无法查找窗口")
return False
except Exception as e:
print(f"检查弹框时发生异常: {e}")
import traceback
traceback.print_exc()
return False
# 获取窗口内所有文本信息
def getWindowText(control, depth=0, texts=None):
"""递归获取控件及其子控件的所有文本信息
Args:
control: 控件对象
depth: 递归深度
texts: 文本列表
Returns:
list: 所有文本信息的列表
"""
if texts is None:
texts = []
try:
# 获取控件名称
if hasattr(control, 'Name') and control.Name:
if getattr(control, 'AutomationId', '') == "labelDuration":
texts.append({
'depth': depth,
'type': str(control.ControlType),
'name': control.Name,
'automation_id': getattr(control, 'AutomationId', ''),
'text': control.Name
})
# 递归获取子控件文本
try:
for child in control.GetChildren():
getWindowText(child, depth + 1, texts)
except:
pass
except Exception as e:
pass
return texts
def checkImportFileIsSuccess(modelCounts = 0,find_interval=2, scroll_interval=1):
if modelCounts == 0:
print(f"错误:模型数量未配置")
return False
if softName is None:
print("错误:软件名称未配置")
return
control = auto.WindowControl(searchDepth=1, Name=softName)
if not control.Exists(0, 0):
print(f"错误:未找到窗口 {softName}")
return
global arrObjName
global scroll_direction
global scroll_amount
arrObjName = []
scroll_direction = "up"
scroll_amount = 120*50*3
# 每次滚动 移动到最顶部
clickAndScrollFirstObj(control, 0)
time.sleep(10)
rection = "up"
scroll_amount = -120*10
temptoTalCounts = 0
compareCounts = 0
#死循环
while True:
if len(arrObjName) >= modelCounts:
break
else:
findObjFiles(control, 0)
time.sleep(find_interval)
clickAndScrollFirstObj(control, 0)
time.sleep(scroll_interval)
#判断 arrObjName 长度,连续3次没有变化,则退出循环
if temptoTalCounts < len(arrObjName):
temptoTalCounts = len(arrObjName)
else:
compareCounts += 1
if compareCounts >= 3:
break
print(f"当前已找到文件数量: {len(arrObjName)}")
if len(arrObjName) == modelCounts:
print(f"文件导入完成")
return True
print(f"文件导入未完成")
return False
@@ -0,0 +1,261 @@
#启动exe 文件
import os
import subprocess
import shutil
import time
import uiautomation as auto
import win32api
import win32con
from utils.config import cfg
# exe_path = cfg('exe.small_exe', None)
# if exe_path is None:
# print(f"错误:exe_path 未配置")
# exit(1)
def start_exe(data):
exe_path = None
if "small" in data:
exe_path = cfg('exe.small_exe', None)
elif "big" in data:
exe_path = cfg('exe.big_exe', None)
else:
print(f"错误:机型类型未配置")
exit(1)
try:
# 检查文件是否存在
if not os.path.exists(exe_path):
print(f"错误:切片软件路径文件不存在 - {exe_path}")
return False
# 检查是否是文件(不是目录)
if not os.path.isfile(exe_path):
print(f"错误:路径不是文件 - {exe_path}")
return False
# 获取 exe 文件所在的目录(工作目录)
exe_dir = os.path.dirname(exe_path)
print(f"工作目录设置为: {exe_dir}")
# 检查工作目录是否存在
if not os.path.exists(exe_dir):
print(f"错误:工作目录不存在 - {exe_dir}")
return False
#清空data文件夹下的所有文件
data_dir = os.path.join(exe_dir, 'data')
if os.path.exists(data_dir):
# 删除该目录
shutil.rmtree(data_dir)
if not os.path.exists(data_dir):
os.makedirs(data_dir)
# 使用 win32api.ShellExecute 完全模拟双击启动
# ShellExecute 是 Windows 系统双击文件时调用的 API,行为最接近真实双击
try:
# 先切换到工作目录(ShellExecute 会使用当前工作目录)
original_cwd = os.getcwd()
os.chdir(exe_dir)
# ShellExecute(hwnd, operation, file, parameters, directory, showCmd)
# operation: "open" 表示打开文件(模拟双击)
# showCmd: SW_SHOWNORMAL (1) 表示正常显示窗口
result = win32api.ShellExecute(
0, # hwnd: 父窗口句柄,0 表示桌面
"open", # operation: 打开文件
exe_path, # file: 要执行的文件路径
"", # parameters: 命令行参数(空字符串表示无参数)
exe_dir, # directory: 工作目录
win32con.SW_SHOWNORMAL # showCmd: 正常显示窗口
)
# 恢复原目录
os.chdir(original_cwd)
# ShellExecute 返回值 > 32 表示成功
if result > 32:
print(f"✓ 成功启动程序(模拟双击): {exe_path}")
print(f" 工作目录: {exe_dir}")
print(f" 返回值: {result}")
return True
else:
print(f"✗ ShellExecute 启动失败,返回值: {result}")
# 如果 ShellExecute 失败,尝试备用方案
raise Exception(f"ShellExecute 返回错误代码: {result}")
except Exception as e:
print(f"ShellExecute 启动失败: {e}")
# 备用方案1:使用 os.startfile(也使用 ShellExecute,但无法设置工作目录)
try:
original_cwd = os.getcwd()
os.chdir(exe_dir)
os.startfile(exe_path)
os.chdir(original_cwd)
print(f"✓ 使用 os.startfile 成功启动程序: {exe_path}")
return True
except Exception as e2:
print(f"os.startfile 启动也失败: {e2}")
# 备用方案2:使用 subprocess(最后手段)
try:
process = subprocess.Popen(
[exe_path],
cwd=exe_dir,
creationflags=subprocess.CREATE_NEW_CONSOLE
)
print(f"✓ 使用 subprocess 成功启动程序: {exe_path}")
print(f" 进程ID: {process.pid}")
return True
except Exception as e3:
print(f"subprocess 启动也失败: {e3}")
return False
except Exception as e:
print(f"启动程序时发生异常: {e}")
import traceback
traceback.print_exc()
return False
def click_confirm():
"""
触发点击 ESC 键,用于关闭弹框
"""
try:
# 发送 ESC 键到当前活动窗口
auto.SendKeys('{ESC}')
print("✓ 已发送 ESC 键")
time.sleep(0.3)
return True
except Exception as e:
print(f"发送 ESC 键失败: {e}")
return False
def close():
"""
关闭打开的应用程序
通过查找窗口名称并关闭窗口,使用多种方法确保成功
"""
try:
import win32gui
import win32con
import win32api
import win32process
# 应用程序的窗口名称
window_name = cfg('exe.soft_name', None)
if window_name is None:
print(f"错误:切片软件名称未配置")
exit(1)
print(f"正在查找窗口: {window_name}")
# 方法1: 使用 win32gui 查找窗口(更可靠)
hwnd = win32gui.FindWindow(None, window_name)
if hwnd:
print(f"✓ 找到窗口句柄: {hwnd}")
# 激活窗口
win32gui.SetForegroundWindow(hwnd)
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) # 确保窗口不是最小化
time.sleep(0.3)
# 方法1: 使用 SendMessage 发送 WM_CLOSE(比 PostMessage 更强制)
try:
win32gui.SendMessage(hwnd, win32con.WM_CLOSE, 0, 0)
print("✓ 使用 SendMessage(WM_CLOSE) 关闭窗口")
time.sleep(1)
# 检查窗口是否还存在
if not win32gui.IsWindow(hwnd):
print("✓ 窗口已成功关闭")
return True
except Exception as e:
print(f"SendMessage(WM_CLOSE) 失败: {e}")
# 方法2: 使用 win32api 发送 Alt+F4 按键事件(更可靠)
try:
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.2)
# 按下 Alt
win32api.keybd_event(win32con.VK_MENU, 0, 0, 0)
time.sleep(0.05)
# 按下 F4
win32api.keybd_event(win32con.VK_F4, 0, 0, 0)
time.sleep(0.05)
# 释放 F4
win32api.keybd_event(win32con.VK_F4, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(0.05)
# 释放 Alt
win32api.keybd_event(win32con.VK_MENU, 0, win32con.KEYEVENTF_KEYUP, 0)
print("✓ 使用 win32api 发送 Alt+F4")
time.sleep(1)
# 检查窗口是否还存在
if not win32gui.IsWindow(hwnd):
print("✓ 窗口已成功关闭")
return True
except Exception as e:
print(f"win32api Alt+F4 失败: {e}")
# 方法3: 尝试使用 uiautomation 的 Close()
try:
window = auto.WindowControl(searchDepth=1, Name=window_name)
if window.Exists(0, 0):
window.SetFocus()
time.sleep(0.2)
window.Close()
print("✓ 使用 uiautomation Close() 关闭窗口")
time.sleep(1)
if not win32gui.IsWindow(hwnd):
print("✓ 窗口已成功关闭")
return True
except Exception as e:
print(f"uiautomation Close() 失败: {e}")
# 方法4: 如果以上都失败,尝试终止进程(最后手段)
print("尝试通过进程ID终止程序...")
try:
import psutil
# 通过窗口句柄获取进程ID
_, pid = win32process.GetWindowThreadProcessId(hwnd)
process = psutil.Process(pid)
process.terminate()
print(f"✓ 已终止进程 (PID: {pid})")
time.sleep(0.5)
return True
except ImportError:
print("psutil 未安装,无法使用进程终止方式")
except Exception as e:
print(f"终止进程失败: {e}")
print("✗ 所有关闭方法都失败了")
return False
else:
# 如果 win32gui 找不到,尝试使用 uiautomation
print("win32gui 未找到窗口,尝试使用 uiautomation...")
window = auto.WindowControl(searchDepth=1, Name=window_name)
if window.Exists(0, 0):
print(f"✓ 使用 uiautomation 找到窗口")
window.SetFocus()
time.sleep(0.2)
hwnd = window.Handle
win32gui.SendMessage(hwnd, win32con.WM_CLOSE, 0, 0)
print("✓ 使用 SendMessage(WM_CLOSE) 关闭窗口")
time.sleep(1)
return True
else:
print(f"✗ 未找到窗口: {window_name}")
return False
except Exception as e:
print(f"关闭应用程序时发生异常: {e}")
import traceback
traceback.print_exc()
return False
#if __name__ == "__main__":
# start_exe()
# #启动成功之后,有一个弹框 的窗口,点击确定
# time.sleep(5)
# click_confirm()
# time.sleep(10)
# close()
@@ -0,0 +1,195 @@
import win32gui, win32con, time
import win32api
import ctypes
from auto_sliceing_operate.utils.click_soft_button import checkImportFileIsSuccess
import os
def find_window(title=None, class_name=None):
# 任一参数为 None 即忽略该条件
hwnd = win32gui.FindWindow(class_name, title)
return hwnd if hwnd else None
def send_text_to_window(hwnd, text):
"""向窗口发送文本"""
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.2)
for char in text:
# 获取虚拟键码
vk_code = win32api.VkKeyScan(char)
if vk_code != -1:
# 提取低字节(虚拟键码)和高字节(修饰键)
vk = vk_code & 0xFF
shift_state = (vk_code >> 8) & 0xFF
# 如果需要 Shift
if shift_state & 1: # Shift 键
win32api.keybd_event(win32con.VK_SHIFT, 0, 0, 0)
time.sleep(0.01)
# 按下键
win32api.keybd_event(vk, 0, 0, 0)
time.sleep(0.01)
# 释放键
win32api.keybd_event(vk, 0, win32con.KEYEVENTF_KEYUP, 0)
# 释放 Shift(如果按下了)
if shift_state & 1:
time.sleep(0.01)
win32api.keybd_event(win32con.VK_SHIFT, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(0.02) # 字符之间的延迟
#发送 Alt+D 快捷键(聚焦地址栏)
def send_alt_d(hwnd):
"""发送 Alt+D 快捷键(聚焦地址栏)"""
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.2)
win32api.keybd_event(win32con.VK_MENU, 0, 0, 0) # Alt
time.sleep(0.05)
win32api.keybd_event(ord('D'), 0, 0, 0)
time.sleep(0.05)
win32api.keybd_event(ord('D'), 0, win32con.KEYEVENTF_KEYUP, 0)
win32api.keybd_event(win32con.VK_MENU, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(0.3)
#发送 Ctrl+A 快捷键(全选)
def send_ctrl_a(hwnd):
"""发送 Ctrl+A 快捷键(全选)"""
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.2)
win32api.keybd_event(win32con.VK_CONTROL, 0, 0, 0)
time.sleep(0.05)
win32api.keybd_event(ord('A'), 0, 0, 0)
time.sleep(0.05)
win32api.keybd_event(ord('A'), 0, win32con.KEYEVENTF_KEYUP, 0)
win32api.keybd_event(win32con.VK_CONTROL, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(0.3)
#发送 Enter 键
def send_enter(hwnd):
"""发送 Enter 键"""
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.1)
win32api.keybd_event(win32con.VK_RETURN, 0, 0, 0)
time.sleep(0.05)
win32api.keybd_event(win32con.VK_RETURN, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(0.2)
#
def send_tab(hwnd, count=1):
"""发送 Tab 键(用于导航焦点)"""
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.1)
for _ in range(count):
win32api.keybd_event(win32con.VK_TAB, 0, 0, 0)
time.sleep(0.05)
win32api.keybd_event(win32con.VK_TAB, 0, win32con.KEYEVENTF_KEYUP, 0)
time.sleep(0.1)
time.sleep(0.2)
#聚焦文件列表窗口
def focus_file_list(hwnd):
"""将焦点移到文件列表区域"""
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.2)
# 使用鼠标点击文件列表区域(最可靠的方法)
try:
# 获取窗口位置和大小
left, top, right, bottom = win32gui.GetWindowRect(hwnd)
width = right - left
height = bottom - top
# 文件列表通常在窗口中间偏下的位置
# 计算文件列表的大概位置(窗口中心偏下,避开地址栏和按钮区域)
click_x = left + width // 2
click_y = top + height // 2 + 30 # 稍微偏下一点,避开地址栏
# 点击文件列表区域
win32api.SetCursorPos((click_x, click_y))
time.sleep(0.1)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
time.sleep(0.05)
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
time.sleep(0.2)
except Exception as e:
print(f"点击文件列表失败,使用Tab键方式: {e}")
# 如果点击失败,使用Tab键导航(备用方案)
send_tab(hwnd, 1)
time.sleep(0.2)
def modify_file_dialog_path_and_import_all(target_path=""):
if not target_path:
return False
"""
修改文件对话框的路径并导入所有文件
参数:
target_path: 目标文件夹路径,默认为 C://work/batchPrint
"""
# 1. 找到"打开"窗口
print("正在查找'打开'窗口...")
hwnd = find_window(title='打开')
if not hwnd:
print("未找到'打开'窗口,请确保文件对话框已打开")
return False
print(f"✓ 找到'打开'窗口,句柄: {hwnd}")
time.sleep(0.5)
# 2. 激活窗口并聚焦地址栏(使用 Alt+D 快捷键)
print("正在聚焦地址栏...")
send_alt_d(hwnd)
time.sleep(0.3)
# 3. 清空地址栏并输入新路径
print(f"正在设置路径为: {target_path}")
send_ctrl_a(hwnd) # 全选现有路径
time.sleep(0.2)
# 输入新路径
send_text_to_window(hwnd, target_path)
time.sleep(0.3)
# 按 Enter 确认路径
send_enter(hwnd)
print("✓ 路径已设置")
time.sleep(1.5) # 等待路径加载完成
# 4. 将焦点移到文件列表
print("正在将焦点移到文件列表...")
focus_file_list(hwnd)
time.sleep(0.3)
# 5. 选择所有文件
print("正在选择所有文件...")
send_ctrl_a(hwnd) # Ctrl+A 选择所有文件
time.sleep(0.5)
print("✓ 已选择所有文件")
# 6. 点击"打开"按钮(使用 Enter 键)
print("正在确认打开...")
send_enter(hwnd)
print("✓ 已确认打开")
# 获取 target_path 路径下 有多少个obj
objsCounts = 0
for file in os.listdir(target_path):
if ".obj" in file:
objsCounts +=1
# 开启死循环,判断文件是否全部导入成功
while True:
res = checkImportFileIsSuccess(objsCounts)
if res:
break
else:
time.sleep(30)
return True
@@ -0,0 +1,4 @@
import time
def log(message):
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} {message}')
@@ -0,0 +1,230 @@
import os
from minio import Minio
from minio.error import S3Error
from auto_sliceing_operate.utils.logs import log
from utils.config import cfg
# 全局变量:存储桶名称(从配置文件读取)
_bucket_name = None
def get_bucket_name():
"""获取存储桶名称(从配置文件读取)
Returns:
str: 存储桶名称
"""
global _bucket_name
if _bucket_name is None:
# 优先从配置文件读取,如果没有则从环境变量读取,最后使用默认值
_bucket_name = cfg('minio.bucket', None)
log(f"MiniIO 存储桶名称: {_bucket_name}")
return _bucket_name
# MiniIO 客户端单例模式
class MiniIOClientSingleton:
_instance = None
_client = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(MiniIOClientSingleton, cls).__new__(cls)
return cls._instance
def get_client(self):
if self._client is None:
# MiniIO 连接配置
# 可以从环境变量或配置文件中读取
endpoint = cfg('minio.endpoint', 'localhost:9000')
access_key = cfg('minio.access_key', 'minioadmin')
secret_key = cfg('minio.secret_key', 'minioadmin')
# secure 参数:True 使用 HTTPS,False 使用 HTTP
# 从配置文件读取,如果没有配置则默认为 False(HTTP)
secure_config = cfg('minio.secure', False)
# 确保是布尔值(处理字符串 'true'/'false' 的情况)
if isinstance(secure_config, str):
secure = secure_config.lower() in ('true', '1', 'yes')
else:
secure = bool(secure_config)
try:
self._client = Minio(
endpoint,
access_key=access_key,
secret_key=secret_key,
secure=secure
)
log(f"MiniIO 客户端初始化成功: {endpoint} (secure={secure})")
except Exception as e:
error_msg = str(e)
# 检查是否是端口错误
if "API port" in error_msg or "InvalidArgument" in error_msg:
log(f"MiniIO 连接失败: 请确保使用的是 API 端口(通常是 9000),而不是 Console 端口(通常是 9001)")
log(f"当前配置的 endpoint: {endpoint}")
log(f"错误详情: {error_msg}")
else:
log(f"MiniIO 客户端初始化失败: {error_msg}")
raise
return self._client
def miniIOClient():
"""获取 MiniIO 客户端单例"""
return MiniIOClientSingleton().get_client()
def upload_file(object_name, file_path, bucket_name=None):
"""
上传文件到 MiniIO
Args:
object_name: 对象名称(在 MiniIO 中的路径)
file_path: 本地文件路径
bucket_name: 存储桶名称(可选,如果不提供则使用全局配置)
Returns:
bool: 上传成功返回 True,失败返回 False
"""
try:
if bucket_name is None:
bucket_name = get_bucket_name()
client = miniIOClient()
# 检查存储桶是否存在,不存在则创建
if not client.bucket_exists(bucket_name):
client.make_bucket(bucket_name)
log(f"创建存储桶: {bucket_name}")
# 上传文件
log(f"开始上传文件: {file_path} -> {bucket_name}/{object_name}")
client.fput_object(bucket_name, object_name, file_path)
log(f"文件上传成功: {bucket_name}/{object_name}")
return True
except S3Error as e:
log(f"上传文件失败 (S3Error): {str(e)}")
return False
except Exception as e:
log(f"上传文件失败: {str(e)}")
return False
def download_file(object_name, file_path, bucket_name=None):
"""
从 MiniIO 下载文件到本地
Args:
object_name: 对象名称(在 MiniIO 中的路径)
file_path: 本地保存路径
bucket_name: 存储桶名称(可选,如果不提供则使用全局配置)
Returns:
bool: 下载成功返回 True,失败返回 False
"""
try:
if bucket_name is None:
bucket_name = get_bucket_name()
client = miniIOClient()
# 检查文件是否存在
if not check_file_exists(object_name, bucket_name):
log(f"文件不存在: {bucket_name}/{object_name}")
return False
# 确保本地目录存在
local_dir = os.path.dirname(file_path)
if local_dir and not os.path.exists(local_dir):
os.makedirs(local_dir, exist_ok=True)
# 下载文件
log(f"开始下载文件: {bucket_name}/{object_name} -> {file_path}")
client.fget_object(bucket_name, object_name, file_path)
# 验证文件是否下载成功
if os.path.exists(file_path):
file_size = os.path.getsize(file_path)
log(f"文件下载成功: {file_path} (大小: {file_size} 字节)")
return True
else:
log("文件下载失败: 本地文件不存在")
return False
except S3Error as e:
log(f"下载文件失败 (S3Error): {str(e)}")
return False
except Exception as e:
log(f"下载文件失败: {str(e)}")
return False
def delete_file(object_name, bucket_name=None):
"""
从 MiniIO 删除文件
Args:
object_name: 对象名称(在 MiniIO 中的路径)
bucket_name: 存储桶名称(可选,如果不提供则使用全局配置)
Returns:
bool: 删除成功返回 True,失败返回 False
"""
try:
if bucket_name is None:
bucket_name = get_bucket_name()
client = miniIOClient()
# 检查文件是否存在
if not check_file_exists(object_name, bucket_name):
log(f"文件不存在,无需删除: {bucket_name}/{object_name}")
return True
# 删除文件
log(f"开始删除文件: {bucket_name}/{object_name}")
client.remove_object(bucket_name, object_name)
log(f"文件删除成功: {bucket_name}/{object_name}")
return True
except S3Error as e:
log(f"删除文件失败 (S3Error): {str(e)}")
return False
except Exception as e:
log(f"删除文件失败: {str(e)}")
return False
def check_file_exists(object_name, bucket_name=None):
"""
检查文件是否存在于 MiniIO
Args:
object_name: 对象名称(在 MiniIO 中的路径)
bucket_name: 存储桶名称(可选,如果不提供则使用全局配置)
Returns:
bool: 文件存在返回 True,不存在返回 False
"""
try:
if bucket_name is None:
bucket_name = get_bucket_name()
client = miniIOClient()
# 检查存储桶是否存在
if not client.bucket_exists(bucket_name):
log(f"存储桶不存在: {bucket_name}")
return False
# 检查对象是否存在
try:
client.stat_object(bucket_name, object_name)
return True
except S3Error as e:
if e.code == 'NoSuchKey':
return False
else:
log(f"检查文件存在性时出错: {str(e)}")
return False
except S3Error as e:
log(f"检查文件存在性失败 (S3Error): {str(e)}")
return False
except Exception as e:
log(f"检查文件存在性失败: {str(e)}")
return False
@@ -0,0 +1,45 @@
import os
from auto_sliceing_operate.utils.oss_redis import ossClient
from auto_sliceing_operate.utils.logs import log
# 从 OSS 下载文件到本地,并做完整性校验(状态码 + 文件大小)
def download_file_with_check(ossFilePath, localFilePath):
try:
log(f"开始从 OSS 下载文件: {ossFilePath} -> {localFilePath}")
if not checkFileExists(ossFilePath):
log(f"文件不存在: {ossFilePath}")
return False
# 同步阻塞下载,下载完成后才会返回
result = ossClient().get_object_to_file(ossFilePath, localFilePath)
# 1. 检查 HTTP 状态码
status = getattr(result, "status", None)
if status != 200:
log(f"下载失败,HTTP 状态码异常: status={status}")
return False
# 2. 远端 / 本地文件大小对比,作为二次校验
remote_meta = ossClient().head_object(ossFilePath)
remote_size = getattr(remote_meta, "content_length", None)
if remote_size is None:
log("无法获取远端文件大小,放弃本次下载结果")
return False
if not os.path.exists(localFilePath):
log("本地文件不存在,下载可能失败")
return False
local_size = os.path.getsize(localFilePath)
if remote_size != local_size:
log(f"文件大小不一致,下载可能不完整: remote={remote_size}, local={local_size}")
return False
log("文件下载成功且完整性校验通过")
return True
except Exception as e:
log(f"下载文件出现异常: {str(e)}")
return False
def checkFileExists(ossFilePath):
return ossClient().object_exists(ossFilePath)
@@ -0,0 +1,125 @@
import oss2,redis
import time
import logging
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 连接oss - 单例模式
class OSSClientSingleton:
_instance = None
_client = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(OSSClientSingleton, cls).__new__(cls)
return cls._instance
def get_client(self):
if self._client is None:
AccessKeyId = 'LTAI5tSReWm8hz7dSYxxth8f'
AccessKeySecret = '8ywTDF9upPAtvgXtLKALY2iMYHIxdS'
Endpoint = 'oss-cn-shanghai.aliyuncs.com'
Bucket = 'suwa3d-securedata'
self._client = oss2.Bucket(oss2.Auth(AccessKeyId, AccessKeySecret), Endpoint, Bucket)
return self._client
def ossClient():
"""获取OSS客户端单例"""
return OSSClientSingleton().get_client()
#连接redis,单例模式
class RedisClientSingleton:
_instance = None
_client = None
# Redis连接配置
REDIS_CONFIG = {
'host': 'mp.api.suwa3d.com',
'password': 'kcV2000',
'port': 6379,
'db': 6,
'socket_timeout': 30, # 操作超时30秒
'socket_connect_timeout': 10, # 连接超时10秒
'socket_keepalive': True, # 启用 TCP keepalive
'socket_keepalive_options': {}, # keepalive 选项
'health_check_interval': 30 # 健康检查间隔30秒
}
# 重试配置
RETRY_INTERVAL = 5 # 重试间隔(秒)
MAX_RETRY_INTERVAL = 60 # 最大重试间隔(秒),用于指数退避
def __new__(cls):
if cls._instance is None:
cls._instance = super(RedisClientSingleton, cls).__new__(cls)
return cls._instance
def _create_redis_client(self):
"""创建Redis客户端"""
return redis.Redis(**self.REDIS_CONFIG)
def _connect_with_retry(self, max_retries=None, retry_interval=None):
"""
带重试机制的Redis连接方法
:param max_retries: 最大重试次数,None表示无限重试
:param retry_interval: 重试间隔(秒),None表示使用默认值,支持指数退避
:return: Redis客户端实例
"""
if retry_interval is None:
retry_interval = self.RETRY_INTERVAL
retry_count = 0
current_interval = retry_interval
while True:
try:
logger.info(f"尝试连接Redis (第 {retry_count + 1} 次)...")
client = self._create_redis_client()
# 测试连接
client.ping()
logger.info("Redis连接成功!")
return client
except (redis.ConnectionError, redis.TimeoutError, AttributeError, Exception) as e:
retry_count += 1
error_msg = str(e)
logger.warning(f"Redis连接失败 (第 {retry_count} 次): {error_msg}")
# 如果设置了最大重试次数且已达到,则抛出异常
if max_retries is not None and retry_count >= max_retries:
logger.error(f"达到最大重试次数 {max_retries},停止重试")
raise
# 指数退避:每次重试间隔逐渐增加,但不超过最大值
logger.info(f"等待 {current_interval} 秒后重试...")
time.sleep(current_interval)
current_interval = min(current_interval * 1.5, self.MAX_RETRY_INTERVAL)
def get_client(self):
"""
获取Redis客户端,如果连接断开则自动重连(带重试机制)
:return: Redis客户端实例
"""
if self._client is None:
# 首次连接,使用无限重试直到成功
logger.info("初始化Redis连接...")
self._client = self._connect_with_retry(max_retries=None)
else:
# 检查连接是否有效,如果断开则重新连接(带重试)
try:
self._client.ping()
except (redis.ConnectionError, redis.TimeoutError, AttributeError) as e:
# 连接断开,重新创建连接(使用无限重试直到成功)
logger.warning(f"Redis连接已断开: {str(e)},开始重新连接...")
self._client = None
self._client = self._connect_with_retry(max_retries=None)
return self._client
def redisClient():
"""获取Redis客户端单例"""
return RedisClientSingleton().get_client()
@@ -0,0 +1,130 @@
import oss2,redis
import time
import logging
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 连接oss - 单例模式
class OSSClientSingleton:
_instance = None
_client = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(OSSClientSingleton, cls).__new__(cls)
return cls._instance
def get_client(self):
if self._client is None:
AccessKeyId = 'LTAI5tSReWm8hz7dSYxxth8f'
AccessKeySecret = '8ywTDF9upPAtvgXtLKALY2iMYHIxdS'
Endpoint = 'oss-cn-shanghai.aliyuncs.com'
Bucket = 'suwa3d-securedata'
self._client = oss2.Bucket(oss2.Auth(AccessKeyId, AccessKeySecret), Endpoint, Bucket)
return self._client
def ossClient():
"""获取OSS客户端单例"""
return OSSClientSingleton().get_client()
#连接redis,单例模式
class RedisClientSingleton:
_instance = None
_client = None
# Redis连接配置
REDIS_CONFIG = {
'host': '192.168.100.254',
'password': '',
'port': 6379,
'db': 6,
'socket_timeout': 30, # 操作超时30秒
'socket_connect_timeout': 10, # 连接超时10秒
'socket_keepalive': True, # 启用 TCP keepalive
'socket_keepalive_options': {}, # keepalive 选项
'health_check_interval': 30 # 健康检查间隔30秒
}
# 重试配置
RETRY_INTERVAL = 5 # 重试间隔(秒)
MAX_RETRY_INTERVAL = 60 # 最大重试间隔(秒),用于指数退避
def __new__(cls):
if cls._instance is None:
cls._instance = super(RedisClientSingleton, cls).__new__(cls)
return cls._instance
def _create_redis_client(self):
"""创建Redis客户端"""
return redis.Redis(**self.REDIS_CONFIG)
def _connect_with_retry(self, max_retries=None, retry_interval=None):
"""
带重试机制的Redis连接方法
:param max_retries: 最大重试次数,None表示无限重试
:param retry_interval: 重试间隔(秒),None表示使用默认值,支持指数退避
:return: Redis客户端实例
"""
if retry_interval is None:
retry_interval = self.RETRY_INTERVAL
retry_count = 0
current_interval = retry_interval
while True:
try:
logger.info(f"尝试连接Redis (第 {retry_count + 1} 次)...")
client = self._create_redis_client()
# 测试连接
client.ping()
logger.info("Redis连接成功!")
return client
except (redis.ConnectionError, redis.TimeoutError, AttributeError, Exception) as e:
retry_count += 1
error_msg = str(e)
logger.warning(f"Redis连接失败 (第 {retry_count} 次): {error_msg}")
# 如果设置了最大重试次数且已达到,则抛出异常
if max_retries is not None and retry_count >= max_retries:
logger.error(f"达到最大重试次数 {max_retries},停止重试")
raise
# 指数退避:每次重试间隔逐渐增加,但不超过最大值
logger.info(f"等待 {current_interval} 秒后重试...")
time.sleep(current_interval)
current_interval = min(current_interval * 1.5, self.MAX_RETRY_INTERVAL)
def get_client(self):
"""
获取Redis客户端,如果连接断开则自动重连(带重试机制)
:return: Redis客户端实例
"""
if self._client is None:
# 首次连接,使用无限重试直到成功
logger.info("初始化Redis连接...")
self._client = self._connect_with_retry(max_retries=None)
else:
# 检查连接是否有效,如果断开则重新连接(带重试)
try:
self._client.ping()
except (redis.ConnectionError, redis.TimeoutError, AttributeError) as e:
# 连接断开,重新创建连接(使用无限重试直到成功)
logger.warning(f"Redis连接已断开: {str(e)},开始重新连接...")
self._client = None
self._client = self._connect_with_retry(max_retries=None)
return self._client
# def redisClient():
# """获取Redis客户端单例"""
# return RedisClientSingleton().get_client()
def redisClientIntranet():
"""获取内网Redis客户端单例"""
return RedisClientSingleton().get_client()
@@ -0,0 +1,27 @@
import requests
from auto_sliceing_operate.utils.logs import log
url = 'https://mp.api.suwa3d.com'
def requestApiToUpdateSliceStatusComplate(versionId, downloadCounts):
api_url = f"{url}/api/printTypeSettingOrder/updateBatchSliceing?batch_id={versionId}&is_slice_complete=1&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}')
#判断返回的code是否是1000
if res.json()["code"] != 1000:
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
@@ -0,0 +1,69 @@
import os,shutil
import zipfile
from logs import log
def zip_file(folderPath, outputZipPath=None):
"""
压缩指定目录为zip文件
Args:
folderPath: 要压缩的目录路径
outputZipPath: 输出的zip文件路径,如果为None则使用目录名+'.zip'
Returns:
str: 生成的zip文件路径,如果失败返回None
"""
try:
if not os.path.exists(folderPath):
log(f"目录不存在: {folderPath}")
return None
if not os.path.isdir(folderPath):
log(f"路径不是目录: {folderPath}")
return None
# 如果没有指定输出路径,使用目录名+'.zip'
if outputZipPath is None:
folderName = os.path.basename(folderPath.rstrip(os.sep))
parentDir = os.path.dirname(folderPath)
outputZipPath = os.path.join(parentDir, f"{folderName}.zip")
log(f"开始压缩目录: {folderPath} -> {outputZipPath}")
# 创建zip文件
with zipfile.ZipFile(outputZipPath, 'w', zipfile.ZIP_DEFLATED) as zipf:
# 遍历目录中的所有文件
for root, dirs, files in os.walk(folderPath):
# 计算相对路径
# 如果root就是folderPath,相对路径为空
# 否则计算相对于folderPath的路径
if root == folderPath:
arcname_prefix = ''
else:
# 获取相对于folderPath的路径
arcname_prefix = os.path.relpath(root, folderPath) + os.sep
for filename in files:
file_path = os.path.join(root, filename)
# 在zip中的路径(相对路径)
arcname = arcname_prefix + filename
zipf.write(file_path, arcname)
log(f"已添加文件到压缩包: {arcname}")
log(f"目录压缩完成: {outputZipPath}")
return outputZipPath
except Exception as e:
log(f"压缩目录时出现错误: {str(e)}")
return None
# def uploadAndZip(folderPath):
# # 上传文件到oss
# # upload_file(folderPath) # 暂时注释,等待实现
# # 压缩文件
# zip_file(folderPath)
# zip_file("/Users/dcx/code/make2/script/batchPrint/1","1024.zip")
+39
View File
@@ -0,0 +1,39 @@
@echo off
chcp 65001 >nul
echo ========================================
echo 开始打包 factory_sliceing 项目为 exe
echo ========================================
echo.
REM 检查 Python 是否安装
python --version >nul 2>&1
if errorlevel 1 (
echo 错误: 未找到 Python,请先安装 Python
pause
exit /b 1
)
REM 检查 PyInstaller 是否安装
python -c "import PyInstaller" >nul 2>&1
if errorlevel 1 (
echo PyInstaller 未安装,正在安装...
pip install pyinstaller
if errorlevel 1 (
echo 安装 PyInstaller 失败
pause
exit /b 1
)
)
REM 执行打包脚本
python build_exe.py --clean
echo.
echo ========================================
echo 打包完成!
echo ========================================
echo.
echo 生成的 exe 文件位于: dist\factory_sliceing.exe
echo.
pause
+307
View File
@@ -0,0 +1,307 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
打包脚本:将 factory_sliceing 项目打包成 exe 文件
使用方法: python build_exe.py
"""
import os
import sys
import subprocess
import shutil
def build_exe():
"""使用 PyInstaller 打包项目"""
# 获取当前脚本所在目录(factory_sliceing 目录)
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir) # make2 目录
# 切换到 factory_sliceing 目录
os.chdir(current_dir)
# PyInstaller 命令参数
main_script = os.path.join(current_dir, 'main.py')
# 构建 PyInstaller 命令
cmd = [
'pyinstaller',
'--name=factory_sliceing', # 生成的 exe 名称
'--onefile', # 打包成单个 exe 文件
'--console', # 显示控制台窗口(因为需要命令行参数)
'--clean', # 清理临时文件
'--noconfirm', # 不询问确认
# 添加隐藏导入(如果 PyInstaller 无法自动检测)
'--hidden-import=auto_sliceing_operate',
'--hidden-import=auto_sliceing_operate.main_begin_sliceing',
'--hidden-import=auto_sliceing_operate.main_download_zip',
'--hidden-import=download_batch_data',
'--hidden-import=download_batch_data.main_download_batch_data_and_trans',
# 收集所有子模块,确保 PyInstaller 能找到所有模块
# 使用 collect-submodules 明确收集所有子模块
'--collect-submodules=auto_sliceing_operate', # 收集所有子模块
'--collect-submodules=download_batch_data', # 收集所有子模块
'--collect-all=auto_sliceing_operate', # 使用 collect-all 确保包含所有子模块和依赖
'--collect-all=download_batch_data', # 使用 collect-all 确保包含所有子模块和依赖
'--hidden-import=auto_sliceing_operate.utils',
'--hidden-import=auto_sliceing_operate.utils.exe_operate',
'--hidden-import=auto_sliceing_operate.utils.click_soft_button',
'--hidden-import=auto_sliceing_operate.utils.import_all_file',
'--hidden-import=auto_sliceing_operate.utils.miniIo',
'--hidden-import=auto_sliceing_operate.utils.oss_redis',
'--hidden-import=auto_sliceing_operate.utils.request',
'--hidden-import=auto_sliceing_operate.utils.logs',
'--hidden-import=download_batch_data.utils',
'--hidden-import=download_batch_data.utils.funcs',
'--hidden-import=download_batch_data.utils.small_machine_transform',
'--hidden-import=download_batch_data.utils.oss_redis',
'--hidden-import=download_batch_data.utils.oss_func',
'--hidden-import=download_batch_data.utils.changeFiles',
'--hidden-import=download_batch_data.utils.logs',
'--hidden-import=utils',
'--hidden-import=utils.config',
# UI 自动化相关模块
'--hidden-import=uiautomation',
# Windows API 相关模块(pywin32)
# 注意:pywin32 需要特殊处理,使用 collect-all 确保包含所有 DLL 和模块
'--hidden-import=win32gui',
'--hidden-import=win32con',
'--hidden-import=win32api',
'--hidden-import=win32process',
'--hidden-import=win32timezone',
'--hidden-import=win32clipboard',
'--hidden-import=pywintypes',
'--collect-all=pywin32', # 收集 pywin32 的所有模块和 DLL 文件
# 其他可能需要的模块
'--hidden-import=psutil', # 进程管理(可选,但有 try-except)
# NumPy 相关模块(用于 small_machine_transform.py)
'--hidden-import=numpy',
'--hidden-import=numpy.core',
'--hidden-import=numpy.core._methods',
'--hidden-import=numpy.lib',
'--hidden-import=numpy.lib.format',
'--collect-all=numpy', # 收集 numpy 的所有子模块和依赖
# OSS2 客户端相关模块
'--hidden-import=oss2',
'--hidden-import=oss2.models',
'--hidden-import=oss2.exceptions',
'--collect-all=oss2', # 收集 oss2 的所有子模块和依赖
# Redis 客户端相关模块
'--hidden-import=redis',
'--hidden-import=redis.connection',
'--hidden-import=redis.client',
'--collect-all=redis', # 收集 redis 的所有子模块和依赖
# Requests 相关模块
'--hidden-import=requests',
'--hidden-import=requests.adapters',
'--hidden-import=urllib3',
'--collect-all=requests', # 收集 requests 的所有子模块和依赖
# TOML 配置文件解析模块
'--hidden-import=toml', # 如果 Python < 3.11 需要 toml 包
'--hidden-import=tomllib', # Python 3.11+ 内置模块
# MinIO 客户端相关模块
'--hidden-import=minio',
'--hidden-import=minio.error',
'--hidden-import=minio.api',
'--hidden-import=minio.commonconfig',
'--hidden-import=minio.credentials',
'--hidden-import=minio.deleteobjects',
'--hidden-import=minio.helpers',
'--hidden-import=minio.select',
'--hidden-import=minio.signer',
'--hidden-import=minio.time',
'--collect-all=minio', # 收集 minio 的所有子模块和依赖
# 收集 uiautomation 的所有子模块(确保完整打包)
'--collect-all=uiautomation',
# 添加数据文件
# small_machine_transform.py 需要作为数据文件包含,因为它会被 Blender 作为外部脚本调用
# 注意:PyInstaller 会将数据文件放在 _MEIPASS 目录下
]
# 添加 small_machine_transform.py 数据文件
small_transform_file = os.path.join(current_dir, "download_batch_data", "utils", "small_machine_transform.py")
if os.path.exists(small_transform_file):
# Windows 使用分号,Linux/Mac 使用冒号
path_sep = ';' if sys.platform == 'win32' else ':'
# 添加到 utils/ 目录(get_resource_path('utils/small_machine_transform.py') 会查找这里)
cmd.append(f'--add-data={small_transform_file}{path_sep}utils')
# 也添加到 download_batch_data/utils/ 目录(备用路径)
cmd.append(f'--add-data={small_transform_file}{path_sep}download_batch_data/utils')
# 添加 Python 路径,确保 PyInstaller 能找到所有模块
# 将当前目录添加到 Python 路径中
cmd.append(f'--paths={current_dir}')
# 添加 hook 文件路径(如果存在)
hook_path = current_dir
# 确保 hook 文件存在并添加到 PyInstaller 的 hook 搜索路径
if os.path.exists(os.path.join(current_dir, 'hook-auto_sliceing_operate.py')):
cmd.append(f'--additional-hooks-dir={hook_path}')
if os.path.exists(os.path.join(current_dir, 'hook-download_batch_data.py')):
# 如果还没有添加 hook 路径,再次添加(不会重复)
if f'--additional-hooks-dir={hook_path}' not in cmd:
cmd.append(f'--additional-hooks-dir={hook_path}')
# 添加工作目录设置和主脚本
cmd.extend([
f'--workpath={os.path.join(current_dir, "build")}',
f'--distpath={os.path.join(current_dir, "dist")}',
f'--specpath={os.path.join(current_dir, "build")}',
main_script
])
print("=" * 60)
print("开始打包 factory_sliceing 项目...")
print("=" * 60)
print(f"工作目录: {current_dir}")
print(f"主脚本: {main_script}")
print()
try:
# 检查 PyInstaller 是否安装
result = subprocess.run(['pyinstaller', '--version'],
capture_output=True, text=True)
if result.returncode != 0:
print("错误: PyInstaller 未安装")
print("请运行: pip install pyinstaller")
return False
print(f"PyInstaller 版本: {result.stdout.strip()}")
print()
# 执行打包命令
print("执行打包命令...")
print(" ".join(cmd))
print()
result = subprocess.run(cmd, check=True)
if result.returncode == 0:
print()
print("=" * 60)
print("打包成功!")
print("=" * 60)
exe_path = os.path.join(current_dir, 'dist', 'factory_sliceing.exe')
if os.path.exists(exe_path):
file_size = os.path.getsize(exe_path) / (1024 * 1024) # MB
print(f"生成的 exe 文件: {exe_path}")
print(f"文件大小: {file_size:.2f} MB")
print()
# 复制配置文件到 dist 目录(如果存在)
config_file = os.path.join(current_dir, 'config.toml')
dist_config = os.path.join(current_dir, 'dist', 'config.toml')
if os.path.exists(config_file):
try:
shutil.copy2(config_file, dist_config)
print(f"配置文件已复制到: {dist_config}")
except Exception as e:
print(f"警告: 复制配置文件失败: {e}")
print(f"请手动将 config.toml 复制到 exe 同级目录: {os.path.dirname(exe_path)}")
else:
# 如果 config.toml 不存在,尝试复制示例文件
example_config = os.path.join(current_dir, 'config.example.toml')
if os.path.exists(example_config):
try:
shutil.copy2(example_config, dist_config)
print(f"配置文件示例已复制到: {dist_config}")
print("请根据实际情况修改配置文件")
except Exception as e:
print(f"警告: 复制配置文件示例失败: {e}")
else:
print("提示: 请将 config.toml 放在 exe 同级目录")
print()
print("使用方法:")
print(" factory_sliceing.exe <command> [work_dir]")
print(" 可用命令:")
print(" - batch_download: 批量下载数据并转换")
print(" - begin_sliceing: 开始切片处理")
print(" - download_zip: 下载切片文件")
print()
print(" 示例:")
print(" factory_sliceing.exe batch_download D:/work")
print(" factory_sliceing.exe begin_sliceing")
print(" factory_sliceing.exe download_zip")
print()
print("注意:")
print(" - 配置文件 config.toml 需要放在 exe 同级目录")
print(" - work_dir 参数可选,如果未提供将从配置文件读取默认值")
return True
else:
print("打包失败!")
return False
except subprocess.CalledProcessError as e:
print(f"打包过程中出现错误: {e}")
return False
except FileNotFoundError:
print("错误: 找不到 PyInstaller")
print("请先安装: pip install pyinstaller")
return False
except Exception as e:
print(f"发生未知错误: {e}")
return False
def clean_build_files():
"""清理构建产生的临时文件"""
current_dir = os.path.dirname(os.path.abspath(__file__))
dirs_to_remove = [
os.path.join(current_dir, 'build'),
os.path.join(current_dir, '__pycache__'),
]
files_to_remove = [
os.path.join(current_dir, 'factory_sliceing.spec'),
]
print("清理临时文件...")
for dir_path in dirs_to_remove:
if os.path.exists(dir_path):
try:
shutil.rmtree(dir_path)
print(f"已删除: {dir_path}")
except Exception as e:
print(f"删除 {dir_path} 失败: {e}")
for file_path in files_to_remove:
if os.path.exists(file_path):
try:
os.remove(file_path)
print(f"已删除: {file_path}")
except Exception as e:
print(f"删除 {file_path} 失败: {e}")
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(description='打包 factory_sliceing 项目为 exe')
parser.add_argument('--clean', action='store_true',
help='打包后清理临时文件')
parser.add_argument('--clean-only', action='store_true',
help='仅清理临时文件,不打包')
args = parser.parse_args()
if args.clean_only:
clean_build_files()
else:
success = build_exe()
if success and args.clean:
print()
clean_build_files()
+40
View File
@@ -0,0 +1,40 @@
# Factory Sliceing 配置文件示例
# 复制此文件为 config.toml 并根据实际情况修改配置
# Redis 配置
[redis]
host = "your-redis-host"
password = "your-redis-password"
port = 6379
db = 6
socket_timeout = 30
socket_connect_timeout = 10
socket_keepalive = true
health_check_interval = 30
# Redis 重试配置
[redis.retry]
retry_interval = 5
max_retry_interval = 60
# OSS 配置
[oss]
access_key_id = "your-access-key-id"
access_key_secret = "your-access-key-secret"
endpoint = "oss-cn-shanghai.aliyuncs.com"
bucket = "your-bucket-name"
# API 配置
[api]
base_url = "https://mp.api.suwa3d.com"
env = "prod" # prod 或 dev
# dev_url = "http://mp.api.dev.com" # 开发环境 URL(可选)
# 工作目录配置
[work]
default_dir = "" # 默认工作目录,为空则使用脚本所在目录
# 日志配置
[log]
level = "INFO" # DEBUG, INFO, WARNING, ERROR
+67
View File
@@ -0,0 +1,67 @@
# Factory Sliceing 配置文件
# Redis 配置
[redis]
host = "mp.api.suwa3d.com"
password = "kcV2000"
port = 6379
db = 6
socket_timeout = 30
socket_connect_timeout = 10
socket_keepalive = true
health_check_interval = 30
[redis_intranet]
host = "192.168.100.254"
password = ""
port = 6379
db = 6
socket_timeout = 30
socket_connect_timeout = 10
socket_keepalive = true
health_check_interval = 30
# Redis 重试配置
[redis.retry]
retry_interval = 5
max_retry_interval = 60
# OSS 配置
[oss]
access_key_id = "LTAI5tSReWm8hz7dSYxxth8f"
access_key_secret = "8ywTDF9upPAtvgXtLKALY2iMYHIxdS"
endpoint = "oss-cn-shanghai.aliyuncs.com"
bucket = "suwa3d-securedata"
#minio
[minio]
bucket="slicedata"
# endpoint 必须使用 API 端口(通常是 9000),不是 Console 端口(通常是 9001)
# Console 端口用于 Web 管理界面,API 端口用于 S3 API 操作
endpoint="192.168.10.102:9000"
access_key="minio"
secret_key="MinIO@suwa3d2025."
# API 配置
[api]
base_url = "https://mp.api.suwa3d.com"
env = "prod" # prod 或 dev
# 工作目录配置
[work]
default_dir = "C://work" # 默认工作目录,为空则使用脚本所在目录
# 日志配置
[log]
level = "INFO" # DEBUG, INFO, WARNING, ERROR
#切片软件的执行路径
[exe]
soft_name = "赛纳3D打印控制系统 V1.4.3.6"
big_exe = ""
small_exe = "C:/Users/Administrator/print_factory_type_setting/software/小机型 切片软件/切片软件V1.4.3.6_2/切片软件V1.4.3.6_2/NormalTek.ThreeDPrinter.UI.exe"
#机器编号
[machine]
id="4"
download_path = "C://work/batchPrintSlice"
+67
View File
@@ -0,0 +1,67 @@
# Factory Sliceing 配置文件
# Redis 配置
[redis]
host = "mp.api.suwa3d.com"
password = "kcV2000"
port = 6379
db = 6
socket_timeout = 30
socket_connect_timeout = 10
socket_keepalive = true
health_check_interval = 30
[redis_intranet]
host = "192.168.100.254"
password = ""
port = 6379
db = 6
socket_timeout = 30
socket_connect_timeout = 10
socket_keepalive = true
health_check_interval = 30
# Redis 重试配置
[redis.retry]
retry_interval = 5
max_retry_interval = 60
# OSS 配置
[oss]
access_key_id = "LTAI5tSReWm8hz7dSYxxth8f"
access_key_secret = "8ywTDF9upPAtvgXtLKALY2iMYHIxdS"
endpoint = "oss-cn-shanghai.aliyuncs.com"
bucket = "suwa3d-securedata"
#minio
[minio]
bucket="slicedata"
# endpoint 必须使用 API 端口(通常是 9000),不是 Console 端口(通常是 9001)
# Console 端口用于 Web 管理界面,API 端口用于 S3 API 操作
endpoint="192.168.10.102:9000"
access_key="minio"
secret_key="MinIO@suwa3d2025."
# API 配置
[api]
base_url = "https://mp.api.suwa3d.com"
env = "prod" # prod 或 dev
# 工作目录配置
[work]
default_dir = "C://work" # 默认工作目录,为空则使用脚本所在目录
# 日志配置
[log]
level = "INFO" # DEBUG, INFO, WARNING, ERROR
#切片软件的执行路径
[exe]
soft_name = "赛纳3D打印控制系统 V1.4.3.6"
big_exe = ""
small_exe = "C:/Users/Administrator/print_factory_type_setting/software/小机型 切片软件/切片软件V1.4.3.6_2/切片软件V1.4.3.6_2/NormalTek.ThreeDPrinter.UI.exe"
#机器编号
[machine]
id="4"
download_path = "C://work/batchPrintSlice"
Binary file not shown.
@@ -0,0 +1,2 @@
# download_batch_data package
@@ -0,0 +1,163 @@
import os,time,sys
from download_batch_data.utils.funcs import requestApiToUpdateSliceStatus
from auto_sliceing_operate.main_begin_sliceing import BeginSliceing
from download_batch_data.utils.oss_redis import redisClient
from download_batch_data.utils.funcs import downloadJsonAndJpgFileAndMoveToCorrectDir, downloadDataByOssAndTransformSave
# 默认使用脚本所在目录
currentDir = os.path.dirname(os.path.abspath(__file__))
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'
#判断是否上传了 JSON 文件
def step1(versionId):
# 下载json 文件 和 图片
dirName,machineInfo,isAlreadyExist = downloadJsonAndJpgFileAndMoveToCorrectDir(versionId,currentDir)
if not dirName:
return False
#判断是否是小机台
isSmallMachine = False
if str(machineInfo["machine_type"]) == '1':
isSmallMachine = True
if isAlreadyExist:
objFilePath = os.path.join(dirName, 'data')
keyValue = f'{versionId}_small_No{machineInfo["id"]}'
BeginSliceing(versionId,machineInfo["id"],objFilePath,keyValue)
return True
#下载数据,转换数据
res = downloadDataByOssAndTransformSave(dirName,isSmallMachine)
if not res:
return False
#判断下载的obj文件数量和json里的是否一致,排除arrange文件夹
objFilePath = os.path.join(dirName, 'data')
objCounts = 0
for file in os.listdir(objFilePath):
if file == 'arrange':
continue
if file.endswith('.obj'):
objCounts += 1
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 下载处理完成的obj文件数量: {objCounts}')
isSuccess = requestApiToUpdateSliceStatus(versionId,objCounts)
if not isSuccess:
return False
#塞入切片的队列 ,SADD 插入 , {batchId}_{machineType}_No{machineId}
machineType = ""
if str(machineInfo["machine_type"]) == '1':
machineType = 'small'
elif str(machineInfo["machine_type"]) == '2':
machineType = 'big'
keyValue = f'{versionId}_{machineType}_No{machineInfo["id"]}'
# 小机台才塞入我的切片队列,大机台有自己的切片队列
if machineType == 'small':
BeginSliceing(versionId,machineInfo["id"],objFilePath,keyValue)
#调用切片方法进行切片处理
# # 内网redis
# r = redisClientIntranet()
# r = redisClient()
# r.sadd('pb:to_sliceing', keyValue)
return True
# 读取 队列中一个数据出来
def main(work_dir=None):
global currentDir
# 如果指定了工作目录,使用指定的目录
if work_dir:
work_dir = os.path.abspath(work_dir)
if not os.path.exists(work_dir):
print(f'指定的工作目录不存在: {work_dir},将创建该目录')
os.makedirs(work_dir, exist_ok=True)
currentDir = work_dir
print(f'使用指定的工作目录: {currentDir}')
else:
print(f'没有指定工作目录,退出')
exit(0)
# 循环处理,直到队列为空
try:
while True:
try:
r = redisClient()
#检测队列是否有值
if r.scard('pb:sliceing') == 0:
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 队列为空,等待10秒')
time.sleep(10)
continue
#获取队列中的值
data = r.spop('pb:sliceing')
if data is None:
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 取出的数据为空,等待10秒')
time.sleep(10)
continue
data = data.decode('utf-8')
#判断是否是数字
if not data.isdigit():
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 取出的数据不是数字,等待10秒')
time.sleep(10)
continue
versionId = str(data)
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 正在处理版次ID={versionId}')
res = step1(versionId)
if res == False:
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} JSON文件下载数据失败,等待10秒')
time.sleep(10)
continue
# 在长时间操作后,确保 Redis 连接仍然有效
# 通过重新获取客户端来触发连接检查
try:
r = redisClient()
r.ping() # 测试连接
except Exception as e:
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} Redis连接检查失败: {str(e)},将在下次循环时自动重连')
#time.sleep(10)
except KeyboardInterrupt:
# 在循环内部捕获 KeyboardInterrupt,允许在 sleep 或操作中被中断
raise # 重新抛出,让外层捕获
except KeyboardInterrupt:
print(f'\n{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 收到中断信号,正在优雅退出...')
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 程序已停止')
sys.exit(0)
# def testMain():
# # global currentDir
# # currentDir = "/Users/dcx/code/make2/script/factory_sliceing_v2/tempData"
# # versionId = '10153' #'10153 10158'
# # print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 正在处理版次ID={versionId}')
# # res = step1(versionId)
# # print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 处理完成,res={res}')
# sendWechatMessage("18950403426", "测试企业微信通知")
# if __name__ == '__main__':
# #testMain()
# parser = argparse.ArgumentParser(description='排版打印订单处理程序')
# parser.add_argument(
# '--work-dir',
# type=str,
# default=None,
# help='指定工作目录(磁盘路径),例如: D:/work 或 /Users/username/work。如果不指定,则使用脚本所在目录'
# )
# args = parser.parse_args()
# main(work_dir=args.work_dir)
@@ -0,0 +1,2 @@
# utils package
@@ -0,0 +1,100 @@
import os,time
import re
from download_batch_data.utils.logs import log
def changeMtlFile(localMtlPath,newName):
beginTime = time.time()
try:
# 使用更高效的文件读取方式 [6,8](@ref)
with open(localMtlPath, 'r', encoding='utf-8') as f:
content = f.read()
# 使用字符串方法直接查找和替换,避免不必要的循环 [9](@ref)
lines = content.split('\n')
for i, line in enumerate(lines):
stripped_line = line.strip()
if stripped_line.startswith('map_Kd '):
lines[i] = f"map_Kd {newName}"
break
# 批量写入,减少I/O操作 [6](@ref)
with open(localMtlPath, 'w', encoding='utf-8') as f:
f.write('\n'.join(lines))
except IOError as e:
log(f"处理文件 {localMtlPath} 时出错: {e}")
return False
except UnicodeDecodeError as e:
log(f"文件编码错误 {localMtlPath}: {e}")
return False
endTime = time.time()
log(f"修改文件文件内容 {localMtlPath} : 耗时{endTime - beginTime}秒")
return True
def changeObjFile(localObjPath,newName):
beginTime = time.time()
try:
# 优化策略:mtllib通常在文件前几行,先只读取前100行
# 对于大文件(如155万行),这样可以大幅减少内存使用和处理时间
with open(localObjPath, 'r', encoding='utf-8') as f:
first_100_lines = []
mtllib_line_idx = -1
# 读取前100行查找mtllib
for i in range(100):
line = f.readline()
if not line:
break
first_100_lines.append(line)
if mtllib_line_idx == -1 and line.strip().startswith('mtllib '):
mtllib_line_idx = i
# 如果在前100行找到了mtllib,直接修改
if mtllib_line_idx != -1:
parts = first_100_lines[mtllib_line_idx].split(' ', 1)
if len(parts) > 1:
#old_mtl_name = parts[1].strip()
new_mtl_name = f"{newName}"
first_100_lines[mtllib_line_idx] = f"mtllib {new_mtl_name}\n"
# 读取剩余内容并合并
remaining_content = f.read()
# 写回文件
with open(localObjPath, 'w', encoding='utf-8') as f_out:
f_out.writelines(first_100_lines)
if remaining_content:
f_out.write(remaining_content)
endTime = time.time()
log(f"修改文件文件内容 {localObjPath} : 耗时{endTime - beginTime}秒")
return True
log(f"浪费时间读取完整的文件,进行替换mtllib路径, localObjPath={localObjPath}, newName={newName}")
# 如果前100行没找到,读取整个文件(很少见的情况)
f.seek(0) # 重置文件指针
content = f.read()
# 使用正则表达式一次性替换(更高效)
pattern = r'^mtllib\s+(.+)$'
def replace_mtllib(match):
old_mtl_name = match.group(1).strip()
new_mtl_name = f"{newName}_{old_mtl_name}"
return f"mtllib {new_mtl_name}"
new_content = re.sub(pattern, replace_mtllib, content, flags=re.MULTILINE)
if new_content != content:
with open(localObjPath, 'w', encoding='utf-8') as f_out:
f_out.write(new_content)
endTime = time.time()
log(f"完整读取修改文件文件内容 {localObjPath} : 耗时{endTime - beginTime}秒")
return True
except IOError as e:
print(f"处理文件 {localObjPath} 时出错: {e}")
return False
except UnicodeDecodeError as e:
print(f"文件编码错误 {localObjPath}: {e}")
return False
return False
@@ -0,0 +1,712 @@
import requests
import time
import os
import platform
import shutil
import json
import shlex
import subprocess
import sys
import signal
from concurrent.futures import ThreadPoolExecutor, as_completed
from download_batch_data.utils.oss_redis import ossClient
from download_batch_data.utils.logs import log
from download_batch_data.utils.oss_func import download_file_with_check, checkFileExists
from download_batch_data.utils.changeFiles import changeObjFile, changeMtlFile
from download_batch_data.utils.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'
def get_resource_path(relative_path):
"""
获取资源文件的绝对路径,兼容 PyInstaller 打包后的环境
参数:
relative_path: 相对于脚本目录的相对路径
返回:
资源文件的绝对路径
"""
try:
# PyInstaller 创建临时文件夹,并将路径存储在 _MEIPASS 中
base_path = sys._MEIPASS
except AttributeError:
# 如果不是打包环境,使用脚本所在目录
base_path = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base_path, relative_path)
def get_transform_script_path():
"""
获取 small_machine_transform.py 脚本的路径
在打包成exe后,如果脚本不在可访问位置,则将其提取到临时目录
返回:
transform_script_path: 脚本的绝对路径
"""
# 首先尝试从资源路径获取
if getattr(sys, 'frozen', False):
# 打包后的环境
# 尝试从资源路径获取
resource_path = get_resource_path('utils/small_machine_transform.py')
# 如果资源路径存在,直接使用
if os.path.exists(resource_path):
return resource_path
# 如果不存在,尝试提取到临时目录
import tempfile
temp_dir = tempfile.gettempdir()
extract_path = os.path.join(temp_dir, 'factory_sliceing_utils', 'small_machine_transform.py')
# 确保目录存在
os.makedirs(os.path.dirname(extract_path), exist_ok=True)
# 如果提取路径不存在,尝试从资源中复制
if not os.path.exists(extract_path):
# 尝试从多个可能的资源路径查找
possible_paths = [
get_resource_path('small_machine_transform.py'),
get_resource_path('utils/small_machine_transform.py'),
]
for src_path in possible_paths:
if os.path.exists(src_path):
shutil.copy2(src_path, extract_path)
log(f"已将 small_machine_transform.py 提取到: {extract_path}")
return extract_path
# 如果提取路径存在,直接使用
if os.path.exists(extract_path):
return extract_path
# 如果都找不到,返回资源路径(即使不存在,让调用者处理错误)
return resource_path
else:
# 开发环境,直接使用相对路径
script_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.join(script_dir, 'small_machine_transform.py')
# 根据打印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}')
sendWechatMessage("18950403426", f"切片状态变更请求失败, res={res.text}")
return False
log(f'状态变更请求成功, res={res.text}')
#判断返回的code是否是1000
if res.json()["code"] != 1000:
log(f'状态变更请求失败, res={res.text}')
sendWechatMessage("18950403426", 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):
return dirNewName, machineInfo,True
shutil.rmtree(dirNewName)
# 创建目录
os.makedirs(dirNewName)
# 创建json子目录
jsonSubDir = os.path.join(dirNewName, 'json')
if not os.path.exists(jsonSubDir):
os.makedirs(jsonSubDir)
dataSubDir = os.path.join(dirNewName, 'data')
if not os.path.exists(dataSubDir):
os.makedirs(dataSubDir)
# 将数据移动过来
shutil.move(localFilePath, os.path.join(jsonSubDir, '3DPrintLayout.json'))
shutil.move(localJpgFilePath, os.path.join(jsonSubDir, f'{versionId}.jpg'))
#如果是大机台,将 json 文件复制到 data 目录下
if str(machineInfo["machine_type"]) != '1':
shutil.copy(os.path.join(jsonSubDir, '3DPrintLayout.json'), os.path.join(dataSubDir, '3DPrintLayout.json'))
if not os.path.exists(os.path.join(dataSubDir, '3DPrintLayout.json')):
log(f"JSON文件不存在, dataSubDir={dataSubDir}")
return False, False
# 检测文件是否移动成功
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 的绝对路径(兼容打包环境)
transform_script_path = get_transform_script_path()
# 将 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.Popen 以便更好地控制进程和信号处理
# 在 Windows 上,Blender 输出可能是 UTF-8,需要指定编码并处理错误
# 准备进程启动参数
startupinfo = None
preexec_fn = None
if platform.system() == 'Windows':
# Windows 上,不设置 CREATE_NEW_PROCESS_GROUP,以便 Ctrl+C 可以传播
import subprocess as sp
startupinfo = sp.STARTUPINFO()
startupinfo.dwFlags |= sp.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = sp.SW_HIDE
else:
# Unix 系统,设置 preexec_fn 以确保信号处理正确
preexec_fn = os.setsid
process = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding='utf-8', # 指定 UTF-8 编码
errors='replace', # 遇到无法解码的字符时替换为占位符,而不是抛出异常
startupinfo=startupinfo,
preexec_fn=preexec_fn
)
try:
# 等待进程完成,捕获输出
stdout, stderr = process.communicate(timeout=300) # 5分钟超时
returncode = process.returncode
except subprocess.TimeoutExpired:
# 超时,终止进程
log(f"Blender 命令执行超时,正在终止进程...")
if platform.system() == 'Windows':
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
else:
# Unix 系统,发送 SIGTERM 到进程组
try:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
process.wait(timeout=5)
except (subprocess.TimeoutExpired, ProcessLookupError, OSError):
try:
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
except (ProcessLookupError, OSError):
pass
raise
except KeyboardInterrupt:
# 收到中断信号,终止进程
log(f"收到中断信号,正在终止 Blender 进程...")
if platform.system() == 'Windows':
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
else:
# Unix 系统,发送 SIGTERM 到进程组
try:
os.killpg(os.getpgid(process.pid), signal.SIGTERM)
process.wait(timeout=5)
except (subprocess.TimeoutExpired, ProcessLookupError, OSError):
try:
os.killpg(os.getpgid(process.pid), signal.SIGKILL)
except (ProcessLookupError, OSError):
pass
raise # 重新抛出,让调用者处理
# 创建结果对象(模拟 subprocess.run 的返回值)
class Result:
def __init__(self, returncode, stdout, stderr):
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
result = Result(returncode, stdout, stderr)
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
#发起企业微信通知
def sendWechatMessage(tel,content):
# 发起企业微信通知
api_url = f"https://mp.api.suwa3d.com/api/qyNotify"
data = {
"message": content,
"userId": tel
}
res = requests.post(api_url, data=json.dumps(data))
if res.status_code != 200:
log(f"发起企业微信通知失败, res={res.text}")
return False
return True
@@ -0,0 +1,4 @@
import time
def log(message):
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} {message}')
@@ -0,0 +1,45 @@
import os
from download_batch_data.utils.oss_redis import ossClient
from download_batch_data.utils.logs import log
# 从 OSS 下载文件到本地,并做完整性校验(状态码 + 文件大小)
def download_file_with_check(ossFilePath, localFilePath):
try:
log(f"开始从 OSS 下载文件: {ossFilePath} -> {localFilePath}")
if not checkFileExists(ossFilePath):
log(f"文件不存在: {ossFilePath}")
return False
# 同步阻塞下载,下载完成后才会返回
result = ossClient().get_object_to_file(ossFilePath, localFilePath)
# 1. 检查 HTTP 状态码
status = getattr(result, "status", None)
if status != 200:
log(f"下载失败,HTTP 状态码异常: status={status}")
return False
# 2. 远端 / 本地文件大小对比,作为二次校验
remote_meta = ossClient().head_object(ossFilePath)
remote_size = getattr(remote_meta, "content_length", None)
if remote_size is None:
log("无法获取远端文件大小,放弃本次下载结果")
return False
if not os.path.exists(localFilePath):
log("本地文件不存在,下载可能失败")
return False
local_size = os.path.getsize(localFilePath)
if remote_size != local_size:
log(f"文件大小不一致,下载可能不完整: remote={remote_size}, local={local_size}")
return False
log("文件下载成功且完整性校验通过")
return True
except Exception as e:
log(f"下载文件出现异常: {str(e)}")
return False
def checkFileExists(ossFilePath):
return ossClient().object_exists(ossFilePath)
@@ -0,0 +1,125 @@
import oss2,redis
import time
import logging
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 连接oss - 单例模式
class OSSClientSingleton:
_instance = None
_client = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(OSSClientSingleton, cls).__new__(cls)
return cls._instance
def get_client(self):
if self._client is None:
AccessKeyId = 'LTAI5tSReWm8hz7dSYxxth8f'
AccessKeySecret = '8ywTDF9upPAtvgXtLKALY2iMYHIxdS'
Endpoint = 'oss-cn-shanghai.aliyuncs.com'
Bucket = 'suwa3d-securedata'
self._client = oss2.Bucket(oss2.Auth(AccessKeyId, AccessKeySecret), Endpoint, Bucket)
return self._client
def ossClient():
"""获取OSS客户端单例"""
return OSSClientSingleton().get_client()
#连接redis,单例模式
class RedisClientSingleton:
_instance = None
_client = None
# Redis连接配置
REDIS_CONFIG = {
'host': 'mp.api.suwa3d.com',
'password': 'kcV2000',
'port': 6379,
'db': 6,
'socket_timeout': 30, # 操作超时30秒
'socket_connect_timeout': 10, # 连接超时10秒
'socket_keepalive': True, # 启用 TCP keepalive
'socket_keepalive_options': {}, # keepalive 选项
'health_check_interval': 30 # 健康检查间隔30秒
}
# 重试配置
RETRY_INTERVAL = 5 # 重试间隔(秒)
MAX_RETRY_INTERVAL = 60 # 最大重试间隔(秒),用于指数退避
def __new__(cls):
if cls._instance is None:
cls._instance = super(RedisClientSingleton, cls).__new__(cls)
return cls._instance
def _create_redis_client(self):
"""创建Redis客户端"""
return redis.Redis(**self.REDIS_CONFIG)
def _connect_with_retry(self, max_retries=None, retry_interval=None):
"""
带重试机制的Redis连接方法
:param max_retries: 最大重试次数,None表示无限重试
:param retry_interval: 重试间隔(秒),None表示使用默认值,支持指数退避
:return: Redis客户端实例
"""
if retry_interval is None:
retry_interval = self.RETRY_INTERVAL
retry_count = 0
current_interval = retry_interval
while True:
try:
logger.info(f"尝试连接Redis (第 {retry_count + 1} 次)...")
client = self._create_redis_client()
# 测试连接
client.ping()
logger.info("Redis连接成功!")
return client
except (redis.ConnectionError, redis.TimeoutError, AttributeError, Exception) as e:
retry_count += 1
error_msg = str(e)
logger.warning(f"Redis连接失败 (第 {retry_count} 次): {error_msg}")
# 如果设置了最大重试次数且已达到,则抛出异常
if max_retries is not None and retry_count >= max_retries:
logger.error(f"达到最大重试次数 {max_retries},停止重试")
raise
# 指数退避:每次重试间隔逐渐增加,但不超过最大值
logger.info(f"等待 {current_interval} 秒后重试...")
time.sleep(current_interval)
current_interval = min(current_interval * 1.5, self.MAX_RETRY_INTERVAL)
def get_client(self):
"""
获取Redis客户端,如果连接断开则自动重连(带重试机制)
:return: Redis客户端实例
"""
if self._client is None:
# 首次连接,使用无限重试直到成功
logger.info("初始化Redis连接...")
self._client = self._connect_with_retry(max_retries=None)
else:
# 检查连接是否有效,如果断开则重新连接(带重试)
try:
self._client.ping()
except (redis.ConnectionError, redis.TimeoutError, AttributeError) as e:
# 连接断开,重新创建连接(使用无限重试直到成功)
logger.warning(f"Redis连接已断开: {str(e)},开始重新连接...")
self._client = None
self._client = self._connect_with_retry(max_retries=None)
return self._client
def redisClient():
"""获取Redis客户端单例"""
return RedisClientSingleton().get_client()
@@ -0,0 +1,130 @@
import oss2,redis
import time
import logging
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# 连接oss - 单例模式
class OSSClientSingleton:
_instance = None
_client = None
def __new__(cls):
if cls._instance is None:
cls._instance = super(OSSClientSingleton, cls).__new__(cls)
return cls._instance
def get_client(self):
if self._client is None:
AccessKeyId = 'LTAI5tSReWm8hz7dSYxxth8f'
AccessKeySecret = '8ywTDF9upPAtvgXtLKALY2iMYHIxdS'
Endpoint = 'oss-cn-shanghai.aliyuncs.com'
Bucket = 'suwa3d-securedata'
self._client = oss2.Bucket(oss2.Auth(AccessKeyId, AccessKeySecret), Endpoint, Bucket)
return self._client
def ossClient():
"""获取OSS客户端单例"""
return OSSClientSingleton().get_client()
#连接redis,单例模式
class RedisClientSingleton:
_instance = None
_client = None
# Redis连接配置
REDIS_CONFIG = {
'host': '192.168.100.254',
'password': '',
'port': 6379,
'db': 6,
'socket_timeout': 30, # 操作超时30秒
'socket_connect_timeout': 10, # 连接超时10秒
'socket_keepalive': True, # 启用 TCP keepalive
'socket_keepalive_options': {}, # keepalive 选项
'health_check_interval': 30 # 健康检查间隔30秒
}
# 重试配置
RETRY_INTERVAL = 5 # 重试间隔(秒)
MAX_RETRY_INTERVAL = 60 # 最大重试间隔(秒),用于指数退避
def __new__(cls):
if cls._instance is None:
cls._instance = super(RedisClientSingleton, cls).__new__(cls)
return cls._instance
def _create_redis_client(self):
"""创建Redis客户端"""
return redis.Redis(**self.REDIS_CONFIG)
def _connect_with_retry(self, max_retries=None, retry_interval=None):
"""
带重试机制的Redis连接方法
:param max_retries: 最大重试次数,None表示无限重试
:param retry_interval: 重试间隔(秒),None表示使用默认值,支持指数退避
:return: Redis客户端实例
"""
if retry_interval is None:
retry_interval = self.RETRY_INTERVAL
retry_count = 0
current_interval = retry_interval
while True:
try:
logger.info(f"尝试连接Redis (第 {retry_count + 1} 次)...")
client = self._create_redis_client()
# 测试连接
client.ping()
logger.info("Redis连接成功!")
return client
except (redis.ConnectionError, redis.TimeoutError, AttributeError, Exception) as e:
retry_count += 1
error_msg = str(e)
logger.warning(f"Redis连接失败 (第 {retry_count} 次): {error_msg}")
# 如果设置了最大重试次数且已达到,则抛出异常
if max_retries is not None and retry_count >= max_retries:
logger.error(f"达到最大重试次数 {max_retries},停止重试")
raise
# 指数退避:每次重试间隔逐渐增加,但不超过最大值
logger.info(f"等待 {current_interval} 秒后重试...")
time.sleep(current_interval)
current_interval = min(current_interval * 1.5, self.MAX_RETRY_INTERVAL)
def get_client(self):
"""
获取Redis客户端,如果连接断开则自动重连(带重试机制)
:return: Redis客户端实例
"""
if self._client is None:
# 首次连接,使用无限重试直到成功
logger.info("初始化Redis连接...")
self._client = self._connect_with_retry(max_retries=None)
else:
# 检查连接是否有效,如果断开则重新连接(带重试)
try:
self._client.ping()
except (redis.ConnectionError, redis.TimeoutError, AttributeError) as e:
# 连接断开,重新创建连接(使用无限重试直到成功)
logger.warning(f"Redis连接已断开: {str(e)},开始重新连接...")
self._client = None
self._client = self._connect_with_retry(max_retries=None)
return self._client
# def redisClient():
# """获取Redis客户端单例"""
# return RedisClientSingleton().get_client()
def redisClientIntranet():
"""获取内网Redis客户端单例"""
return RedisClientSingleton().get_client()
@@ -0,0 +1,193 @@
import os,time,sys,argparse
import numpy as np
import json
import warnings
def findBpyModule():
# /Applications/Blender.app/Contents/Resources/4.4/python/bin
blender_bin_path = '/Applications/Blender.app/Contents/Resources/4.4/python/bin/blender'
return blender_bin_path
def custom_mesh_transform(vertices, transform_matrix):
"""
手动实现网格变换:对每个顶点应用齐次变换矩阵
参数:
vertices: 网格顶点数组 (N, 3)
transform_matrix: 4x4 齐次变换矩阵
返回:
变换后的顶点数组 (N, 3)
"""
# 验证变换矩阵
transform_matrix = np.array(transform_matrix, dtype=np.float64)
# 检查矩阵形状
if transform_matrix.shape != (4, 4):
raise ValueError(f"变换矩阵必须是 4x4 矩阵,当前形状: {transform_matrix.shape}")
# 检查矩阵中是否包含无效值(NaN 或 Inf)
if np.any(np.isnan(transform_matrix)) or np.any(np.isinf(transform_matrix)):
invalid_count = np.sum(np.isnan(transform_matrix)) + np.sum(np.isinf(transform_matrix))
raise ValueError(f"变换矩阵包含无效值 (NaN/Inf): {invalid_count} 个无效元素\n矩阵:\n{transform_matrix}")
# 检查矩阵行列式(对于齐次变换矩阵,最后一行应该是 [0, 0, 0, 1])
# 如果矩阵接近奇异,行列式会接近0
det = np.linalg.det(transform_matrix[:3, :3]) # 只检查3x3旋转/缩放部分
if abs(det) < 1e-10:
print(f"警告: 变换矩阵的3x3部分行列式接近0 (det={det:.2e}),可能导致数值不稳定")
# 检查最后一行是否为 [0, 0, 0, 1](齐次变换矩阵的标准形式)
last_row = transform_matrix[3, :]
if not np.allclose(last_row, [0, 0, 0, 1], atol=1e-6):
print(f"警告: 变换矩阵最后一行不是标准形式 [0,0,0,1],而是: {last_row}")
# 1. 顶点转齐次坐标 (N, 3) → (N, 4)
homogeneous_vertices = np.hstack((vertices, np.ones((vertices.shape[0], 1))))
# 2. 应用变换矩阵:矩阵乘法 (4x4) * (4xN) → (4xN)
# 捕获警告并检查结果
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always") # 捕获所有警告
transformed_homogeneous = transform_matrix @ homogeneous_vertices.T
# 如果有警告,记录详细信息
if w:
warning_msgs = [str(warning.message) for warning in w]
print(f"警告: 矩阵乘法过程中出现 {len(w)} 个警告:")
for msg in warning_msgs:
print(f" - {msg}")
print(f"变换矩阵:\n{transform_matrix}")
print(f"矩阵包含 NaN: {np.any(np.isnan(transform_matrix))}")
print(f"矩阵包含 Inf: {np.any(np.isinf(transform_matrix))}")
if not np.any(np.isnan(transform_matrix)) and not np.any(np.isinf(transform_matrix)):
print(f"矩阵最大值: {np.nanmax(transform_matrix):.6e}")
print(f"矩阵最小值: {np.nanmin(transform_matrix):.6e}")
# 检查结果是否包含无效值
if np.any(np.isnan(transformed_homogeneous)) or np.any(np.isinf(transformed_homogeneous)):
invalid_count = np.sum(np.isnan(transformed_homogeneous)) + np.sum(np.isinf(transformed_homogeneous))
raise ValueError(f"变换后的顶点包含无效值 (NaN/Inf): {invalid_count} 个无效元素")
# 3. 转回非齐次坐标 (3xN) → (N, 3)
transformed_vertices = transformed_homogeneous[:3, :].T
return transformed_vertices
# {
# "file_name": "888687_324912_P117183_13.8cm_x1.obj",
# "transform": {
# "homo_matrix": [
# [-0.09437,0.995195,0.026116,77.975499],
# [-0.58016,-0.033658,-0.813807,322.60588],
# [-0.809017,-0.09195,0.580549,24.503686],
# [0.0,0.0,0.0,1.0]
# ]
# }
# },
def transform_save(obj_path,homo_matrix):
import bpy
# 延迟导入 bpy,只在 Blender 环境中可用
# try:
# blender_bin_path = findBpyModule()
# subprocess.run([blender_bin_path, "--background", "--python-expr", "import bpy"])
# except ImportError:
# log("错误: bpy 模块不可用。此函数只能在 Blender 环境中运行。")
# log("请使用 Blender 的 Python 解释器运行此脚本,或通过 Blender 命令行运行。")
# raise ImportError("bpy 模块不可用。此函数只能在 Blender 环境中运行。")
print("开始转换数据")
obj_name = obj_path.split("/")[-1]
print(f"obj_name={obj_name}")
# 清除场景
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete(use_global=False)
## 尺寸调整,环境设置
bpy.ops.object.delete(use_global=False, confirm=False)
bpy.context.scene.unit_settings.length_unit = 'CENTIMETERS'
bpy.context.scene.unit_settings.scale_length = 0.001
bpy.context.scene.unit_settings.mass_unit = 'GRAMS'
reconstructed_matrix = np.array(homo_matrix, dtype=np.float64)
bpy.ops.wm.obj_import(filepath=obj_path)
imported_object = bpy.context.object
if imported_object is None or imported_object.type != 'MESH':
print(f"警告: 未能成功导入网格对象 {obj_name}。跳过。")
return
mesh_data = imported_object.data
mesh_data.calc_loop_triangles()
original_vertices = np.empty(len(mesh_data.vertices) * 3, dtype=np.float64)
mesh_data.vertices.foreach_get('co', original_vertices)
original_vertices = original_vertices.reshape(-1, 3)
try:
transformed_vertices = custom_mesh_transform(original_vertices, reconstructed_matrix)
# 验证转换后的顶点是否有效
if np.any(np.isnan(transformed_vertices)) or np.any(np.isinf(transformed_vertices)):
invalid_count = np.sum(np.isnan(transformed_vertices)) + np.sum(np.isinf(transformed_vertices))
error_msg = f"错误: 转换后的顶点包含无效值 (NaN/Inf): {invalid_count} 个无效元素, obj_name={obj_name}"
print(error_msg)
print(f"变换矩阵:\n{reconstructed_matrix}")
raise ValueError(error_msg)
print(f"转换成功: {len(transformed_vertices)} 个顶点")
print(f"顶点坐标范围: X[{transformed_vertices[:, 0].min():.2f}, {transformed_vertices[:, 0].max():.2f}], "
f"Y[{transformed_vertices[:, 1].min():.2f}, {transformed_vertices[:, 1].max():.2f}], "
f"Z[{transformed_vertices[:, 2].min():.2f}, {transformed_vertices[:, 2].max():.2f}]")
except Exception as e:
error_msg = f"转换顶点时发生错误, obj_name={obj_name}, error={str(e)}"
print(error_msg)
print(f"变换矩阵:\n{reconstructed_matrix}")
raise
offset = np.array([-380, -345, 0])
transformed_vertices += offset
print(f"已对模型 {obj_name} 应用偏移: {offset}")
flattened_verts = transformed_vertices.reshape(-1)
mesh_data.vertices.foreach_set('co', flattened_verts)
mesh_data.update()
# meshes.append(imported_object)
# 导出前确保选中当前对象
bpy.ops.object.select_all(action='DESELECT')
imported_object.select_set(True)
bpy.context.view_layer.objects.active = imported_object
bpy.ops.wm.obj_export(
filepath=obj_path,
export_selected_objects=True,
export_materials=True,
path_mode='AUTO'
)
#os.remove(old_mtl_path)
if __name__ == "__main__":
script_args = []
try:
separator_index = sys.argv.index("--")
script_args = sys.argv[separator_index + 1:]
except ValueError:
pass
parser = argparse.ArgumentParser()
parser.add_argument("--objPathName", type=str, required=True, help="obj文件路径")
parser.add_argument("--trans", type=str, required=True, help="旋转参数(JSON格式)")
args = parser.parse_args(script_args)
print(f"objPathName={args.objPathName}, trans={args.trans}")
# 解析 JSON 字符串为列表
try:
homo_matrix = json.loads(args.trans)
except json.JSONDecodeError as e:
print(f"错误: 无法解析 trans 参数为 JSON: {e}")
sys.exit(1)
print("进入转换方法")
transform_save(args.objPathName, homo_matrix)
@@ -0,0 +1,25 @@
# PyInstaller hook for auto_sliceing_operate package
from PyInstaller.utils.hooks import collect_submodules, collect_data_files
# 收集所有子模块(包括递归子模块)
hiddenimports = collect_submodules('auto_sliceing_operate')
# 确保包含所有必要的模块(即使 collect_submodules 已经包含,显式声明更安全)
explicit_imports = [
'auto_sliceing_operate',
'auto_sliceing_operate.main_begin_sliceing',
'auto_sliceing_operate.main_download_zip',
'auto_sliceing_operate.utils',
'auto_sliceing_operate.utils.exe_operate',
'auto_sliceing_operate.utils.click_soft_button',
'auto_sliceing_operate.utils.import_all_file',
'auto_sliceing_operate.utils.miniIo',
'auto_sliceing_operate.utils.oss_redis',
'auto_sliceing_operate.utils.request',
'auto_sliceing_operate.utils.logs',
'auto_sliceing_operate.utils.zip',
]
# 合并列表并去重
hiddenimports = list(set(hiddenimports + explicit_imports))
@@ -0,0 +1,22 @@
# PyInstaller hook for download_batch_data package
from PyInstaller.utils.hooks import collect_submodules, collect_data_files
# 收集所有子模块(包括递归子模块)
hiddenimports = collect_submodules('download_batch_data')
# 确保包含所有必要的模块(即使 collect_submodules 已经包含,显式声明更安全)
explicit_imports = [
'download_batch_data',
'download_batch_data.main_download_batch_data_and_trans',
'download_batch_data.utils',
'download_batch_data.utils.funcs',
'download_batch_data.utils.small_machine_transform',
'download_batch_data.utils.oss_redis',
'download_batch_data.utils.oss_func',
'download_batch_data.utils.changeFiles',
'download_batch_data.utils.logs',
]
# 合并列表并去重
hiddenimports = list(set(hiddenimports + explicit_imports))
+84
View File
@@ -0,0 +1,84 @@
import sys
import os
import importlib
from utils.config import cfg, get_work_dir
# 在模块级别显式导入,确保 PyInstaller 能检测到并打包这些模块
# 这些导入语句会被 PyInstaller 分析,确保模块被包含
# 注意:不要使用 try-except,因为 PyInstaller 需要看到这些导入才能正确打包
import auto_sliceing_operate.main_begin_sliceing
import auto_sliceing_operate.main_download_zip
import download_batch_data.main_download_batch_data_and_trans
# 同时导入这些模块的依赖,确保所有子模块都被包含
import auto_sliceing_operate.utils
import auto_sliceing_operate.utils.exe_operate
import auto_sliceing_operate.utils.click_soft_button
import auto_sliceing_operate.utils.import_all_file
import auto_sliceing_operate.utils.miniIo
import auto_sliceing_operate.utils.oss_redis
import auto_sliceing_operate.utils.request
import auto_sliceing_operate.utils.logs
import download_batch_data.utils
def get_command_handler(command):
"""动态导入并返回命令处理函数"""
try:
if command == 'batch_download':
module = importlib.import_module('download_batch_data.main_download_batch_data_and_trans')
return module.main
elif command == 'begin_sliceing':
module = importlib.import_module('auto_sliceing_operate.main_begin_sliceing')
return module.main
elif command == 'download_slice':
module = importlib.import_module('auto_sliceing_operate.main_download_zip')
return module.main
else:
return None
except ModuleNotFoundError as e:
print(f'错误: 无法找到模块 - {e}')
print('这可能是 PyInstaller 打包问题,请检查打包配置。')
print(f'尝试导入的模块: {command}')
raise
except Exception as e:
print(f'错误: 导入模块时发生异常 - {e}')
raise
if __name__ == '__main__':
# 可用命令列表
available_commands = ['batch_download', 'begin_sliceing', 'download_slice']
# 检查参数数量
if len(sys.argv) < 2:
print('Usage: python main.py <command> [work_dir]')
print('可用命令:', ' | '.join(available_commands))
print('\n注意: work_dir 参数可选,如果未提供将从配置文件读取默认值')
sys.exit(1)
command = sys.argv[1]
# 检查命令是否存在
if command not in available_commands:
print(f'错误: 未知命令 "{command}"')
print('可用命令:', ' | '.join(available_commands))
sys.exit(1)
# 获取工作目录:优先使用命令行参数,其次使用配置文件,最后使用 None
work_dir = None
if len(sys.argv) > 2:
work_dir = sys.argv[2]
# 打印配置信息(可选,用于调试)
if cfg('log.level', 'INFO') == 'DEBUG':
print(f'配置信息:')
print(f' - API URL: {cfg("api.base_url", "未配置")}')
print(f' - Redis Host: {cfg("redis.host", "未配置")}')
print(f' - 工作目录: {work_dir if work_dir else "未指定(将使用默认值)"}')
# 动态导入并执行对应的处理函数
handler = get_command_handler(command)
if handler is None:
print(f'错误: 无法加载命令 "{command}" 的处理模块')
sys.exit(1)
handler(work_dir=work_dir)
+29
View File
@@ -0,0 +1,29 @@
# Factory Sliceing 项目依赖
# 配置文件解析
toml>=0.10.2
# Redis 客户端
redis>=4.0.0
# OSS 客户端
oss2>=2.17.0
# HTTP 请求
requests>=2.28.0
# 数值计算(用于 small_machine_transform.py)
numpy>=1.20.0
# UI 自动化(Windows)
uiautomation>=2.0.0
# Windows API(Windows 系统需要)
pywin32>=300; sys_platform == 'win32'
# 打包工具(可选,仅打包时需要)
pyinstaller>=5.0.0
# MinIO 客户端(如果使用)
minio>=7.0.0
+20
View File
@@ -0,0 +1,20 @@
from auto_sliceing_operate.utils.miniIo import upload_file,check_file_exists,delete_file,download_file
# 测试上传文件
def test_upload_file():
upload_file("test.txt", "test.txt")
# 测试下载文件
def test_download_file():
download_file("test.txt", "testAAA.txt")
# 测试删除文件
def test_delete_file():
delete_file("test.txt")
# 测试检查文件是否存在
def test_check_file_exists():
print(check_file_exists("test.txt"))
test_download_file()
+1
View File
@@ -0,0 +1 @@
AAAAAAAAAAA
+1
View File
@@ -0,0 +1 @@
AAAAAAAAAAA
+2
View File
@@ -0,0 +1,2 @@
# utils 模块
+177
View File
@@ -0,0 +1,177 @@
"""
配置文件读取工具
支持从 config.toml 文件中读取配置信息
"""
import os
import sys
# 优先使用 Python 3.11+ 内置的 tomllib,否则回退到 toml 包
_USE_STDLIB_TOMLLIB = False
try:
import tomllib # Python 3.11+
_USE_STDLIB_TOMLLIB = True
except ImportError:
import toml as tomllib # 回退到 toml 包
_USE_STDLIB_TOMLLIB = False
def get_config_path():
"""
获取配置文件路径
查找顺序:
1. 如果是打包后的 exe,优先查找 exe 同级目录的 config.toml
2. 开发环境:查找 factory_sliceing 目录下的 config.toml
3. 当前目录下的 config.toml
"""
# 判断是否在 PyInstaller 打包后的环境中
if getattr(sys, 'frozen', False):
# 打包后的 exe 环境
# sys.executable 是 exe 文件的完整路径
exe_dir = os.path.dirname(os.path.abspath(sys.executable))
config_path = os.path.join(exe_dir, 'config.toml')
# 如果 exe 同级目录存在配置文件,直接返回
if os.path.exists(config_path):
return config_path
# 如果不存在,也返回这个路径(让调用者知道应该在哪里创建配置文件)
return config_path
# 开发环境:查找 factory_sliceing 目录下的 config.toml
current_dir = os.path.dirname(os.path.abspath(__file__))
# 向上查找 factory_sliceing 目录
factory_sliceing_dir = os.path.dirname(current_dir)
config_path = os.path.join(factory_sliceing_dir, 'config.toml')
# 如果文件存在,返回
if os.path.exists(config_path):
return config_path
# 如果不存在,尝试当前目录
config_path = os.path.join(current_dir, 'config.toml')
if os.path.exists(config_path):
return config_path
# 都不存在,返回 factory_sliceing 目录下的路径(作为默认路径)
return os.path.join(factory_sliceing_dir, 'config.toml')
def cfg(key_name, default=None):
"""
读取配置文件中的值
:param key_name: 配置项的键名,支持点号分隔的嵌套键,如 "redis.host" 或 "redis.retry.interval"
:param default: 如果配置项不存在,返回的默认值
:return: 配置项的值,如果不存在且未提供默认值则返回 None
"""
try:
config_path = get_config_path()
if not os.path.exists(config_path):
print(f"警告: 配置文件不存在,路径: {config_path}")
return default
# tomllib 需要二进制模式,toml 包需要文本模式
if _USE_STDLIB_TOMLLIB:
# Python 3.11+ 的 tomllib,需要二进制模式
with open(config_path, 'rb') as f:
config = tomllib.load(f)
else:
# toml 包,需要文本模式
with open(config_path, 'r', encoding='utf-8') as f:
config = tomllib.load(f)
# 处理嵌套键名,如 "redis.host"
if "." in key_name:
keys = key_name.split(".")
value = config
for key in keys:
if isinstance(value, dict) and key in value:
value = value[key]
else:
if default is not None:
return default
print(f"配置项 {key_name} 不存在")
return None
return value
else:
# 单层键名
if key_name in config:
return config[key_name]
else:
if default is not None:
return default
print(f"配置项 {key_name} 不存在")
return None
except FileNotFoundError:
print(f"配置文件不存在,路径: {config_path}")
return default
except Exception as e:
# 提供更详细的错误信息
lib_type = "tomllib (标准库)" if _USE_STDLIB_TOMLLIB else "toml (第三方包)"
error_msg = str(e)
print(f"读取配置文件时发生错误 (使用 {lib_type}): {error_msg}")
print(f"配置文件路径: {config_path}")
# 如果是 TOML 解析错误,提供额外提示
if "Expecting" in error_msg or "parse" in error_msg.lower() or "syntax" in error_msg.lower():
print("提示: 请检查配置文件格式是否正确,确保使用 UTF-8 编码")
return default
def get_redis_config():
"""
获取 Redis 配置字典
:return: Redis 配置字典
"""
return {
'host': cfg('redis.host', 'localhost'),
'password': cfg('redis.password', ''),
'port': cfg('redis.port', 6379),
'db': cfg('redis.db', 0),
'socket_timeout': cfg('redis.socket_timeout', 30),
'socket_connect_timeout': cfg('redis.socket_connect_timeout', 10),
'socket_keepalive': cfg('redis.socket_keepalive', True),
'health_check_interval': cfg('redis.health_check_interval', 30)
}
def get_oss_config():
"""
获取 OSS 配置字典
:return: OSS 配置字典
"""
return {
'access_key_id': cfg('oss.access_key_id'),
'access_key_secret': cfg('oss.access_key_secret'),
'endpoint': cfg('oss.endpoint'),
'bucket': cfg('oss.bucket')
}
def get_api_url():
"""
获取 API 基础 URL
:return: API URL 字符串
"""
env = cfg('api.env', 'prod')
base_url = cfg('api.base_url', 'https://mp.api.suwa3d.com')
# 如果环境是 dev,可以覆盖 URL
if env == 'dev':
dev_url = cfg('api.dev_url', 'http://mp.api.dev.com')
return dev_url
return base_url
def get_work_dir():
"""
获取默认工作目录
:return: 工作目录路径,如果未配置则返回 None
"""
return cfg('work.default_dir', None)
def get_exe_path(typeExe):
return cfg('exe.'+typeExe+'_exe', None)
Binary file not shown.
+46 -9
View File
@@ -55,18 +55,32 @@ class DataTransfer:
# 列出所有对象
objects = []
prefix = self.oss_path.lstrip('/') # 移除开头的 '/' 以匹配 OSS 格式
ishaveCodeJpg = False
for obj in oss2.ObjectIterator(self.oss_client, prefix=prefix):
if obj.key != prefix: # 跳过目录本身
objects.append(obj.key)
print(f"obj.key={obj.key}")
if ".jpg" in obj.key:
if obj.key.endswith(f"printId_{json_model_info.print_id}Tex1.jpg"):
objects.append(obj.key)
ishaveCodeJpg = True
else:
objects.append(obj.key)
if not ishaveCodeJpg:
for obj in oss2.ObjectIterator(self.oss_client, prefix=prefix):
if obj.key == f"{json_model_info.pid}Tex1.jpg":
objects.append(obj.key)
break
print(f"objects={objects}")
download_count = 0
# 下载所有文件,添加进度条
for obj_key in tqdm(objects, desc="下载进度"):
if obj_key.endswith('/'):
continue
if "printId" in obj_key:
continue
# if "printId" in obj_key:
# continue
# 计算相对路径
rel_path = obj_key[len(prefix):].lstrip('/')
@@ -76,6 +90,9 @@ class DataTransfer:
# 根据文件后缀名进行重命名
if file_ext.lower() in ['.mtl', '.jpg', '.jpeg', '.png']:
if file_name.endswith(f"printId_{json_model_info.print_id}Tex1.jpg"):
file_name = file_name.replace(f"printId_{json_model_info.print_id}Tex1.jpg", f"{json_model_info.pid}Tex1.jpg")
# 对于.mtl和图片文件,在原名前加order_id
new_file_name = f"{json_model_info.order_id}_{file_name}"
# new_file_name = file_name
@@ -101,7 +118,7 @@ class DataTransfer:
# 下载文件
self.oss_client.get_object_to_file(obj_key, local_path)
download_count += 1
if file_ext == '.obj': # 10MB以上
try:
# 使用临时文件避免内存问题 [8](@ref)
@@ -163,6 +180,13 @@ class DataTransfer:
print(f"下载文件: {obj_key} -> {local_path}")
if download_count != 3:
print(f"下载文件数量不正确: {download_count}")
#直接杀死进程
os._exit(1)
return True
import requests
import json
import shutil
@@ -186,6 +210,7 @@ class JSONModelInfo:
order_id: str
pid: str
model_height: str
print_id: str
def read_pids_from_json(pid_file):
"""从文件读取所有PID"""
@@ -198,7 +223,12 @@ def read_pids_from_json(pid_file):
# 检查JSON文件是否存在
if not os.path.exists(json_path):
print(f"错误: JSON文件不存在 - {json_path}")
return [], {}
return [], {}, ''
# 检查是否是文件而不是目录
if not os.path.isfile(json_path):
print(f"错误: 路径是目录而不是文件 - {json_path}")
return [], {}, ''
# 读取JSON文件
try:
@@ -206,12 +236,17 @@ def read_pids_from_json(pid_file):
data = json.load(f)
except Exception as e:
print(f"读取JSON文件失败: {e}")
return [], {}
return [], {}, ''
list_model_info = []
summary = data.get('summary')
print("summary=", summary)
selected_machine = summary['selected_machine']
# 安全获取 selected_machine
if isinstance(summary, dict):
selected_machine = summary.get('selected_machine', '')
else:
# 如果 summary 不是字典,尝试从根级别获取
selected_machine = data.get('selected_machine', '')
print("selected_machine=", selected_machine)
# 处理每个模型
for model in data.get('models', []):
@@ -220,6 +255,7 @@ def read_pids_from_json(pid_file):
parts = obj_name.split('_')
order_id = parts[0]
print_id = parts[2].replace("P", "")
pid = parts[1]
model_height = parts[3]
@@ -227,7 +263,8 @@ def read_pids_from_json(pid_file):
obj_name=obj_name,
order_id=order_id,
pid=pid,
model_height=model_height
model_height=model_height,
print_id=print_id
)
list_model_info.append(model_info)
+117 -33
View File
@@ -2,7 +2,7 @@ import os,shutil
import redis
import oss2,time,sys
import requests
import argparse
import argparse,json
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from download_print_out import download_transform_save_by_json
@@ -53,7 +53,40 @@ class RedisClientSingleton:
def get_client(self):
if self._client is None:
self._client = redis.Redis(host='mp.api.suwa3d.com', password='kcV2000', port=6379, db=6)
# 添加超时参数,防止连接超时
# socket_timeout: 每次操作的超时时间(秒)
# socket_connect_timeout: 连接超时时间(秒)
# socket_keepalive: 启用 TCP keepalive
# socket_keepalive_options: keepalive 选项
self._client = redis.Redis(
host='mp.api.suwa3d.com',
password='kcV2000',
port=6379,
db=6,
socket_timeout=30, # 操作超时30秒
socket_connect_timeout=10, # 连接超时10秒
socket_keepalive=True, # 启用 TCP keepalive
socket_keepalive_options={}, # keepalive 选项
health_check_interval=30 # 健康检查间隔30秒
)
else:
# 检查连接是否有效,如果断开则重新连接
try:
self._client.ping()
except (redis.ConnectionError, redis.TimeoutError, AttributeError):
# 连接断开,重新创建连接
self._client = None
self._client = redis.Redis(
host='mp.api.suwa3d.com',
password='kcV2000',
port=6379,
db=6,
socket_timeout=30,
socket_connect_timeout=10,
socket_keepalive=True,
socket_keepalive_options={},
health_check_interval=30
)
return self._client
@@ -65,14 +98,22 @@ def redisClient():
# 根据批次id,进行切片中的状态变更
def requestApiToUpdateSliceStatus(versionId,downloadCounts):
api_url = f"{url}/api/typeSettingPrintOrder/updateBatchSliceing?batch_id={versionId}&download_counts="+str(downloadCounts)
api_url = f"{url}/api/printTypeSettingOrder/updateBatchSliceing?batch_id={versionId}&download_counts="+str(downloadCounts)
print(f'发起状态变更请求url={api_url}, versionId={versionId}')
res = requests.post(api_url)
if res.status_code != 200:
print(f'状态变更请求失败, res={res.text}')
try:
# 添加超时参数,防止请求时间过长
res = requests.post(api_url, timeout=60) # 60秒超时
if res.status_code != 200:
print(f'状态变更请求失败, res={res.text}')
return False
print(f'状态变更请求成功, res={res.text}')
return True
except requests.exceptions.Timeout:
print(f'状态变更请求超时, url={api_url}')
return False
except requests.exceptions.RequestException as e:
print(f'状态变更请求异常, error={str(e)}')
return False
print(f'状态变更请求成功, res={res.text}')
return True
#判断是否上传了 JSON 文件
def step1(versionId):
@@ -82,32 +123,60 @@ def step1(versionId):
if not ossClient().object_exists(jsonFilePath):
return False
#下载文件到项目所在目录
dirNow = os.path.join(currentDir, 'batchPrint', versionId, 'json')
if not os.path.exists(dirNow):
os.makedirs(dirNow)
localFilePath = os.path.join(dirNow, f'{versionId}.json')
# 先下载JSON文件到临时位置,读取 selected_machine 值
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')
ossClient().get_object_to_file(jsonFilePath, localFilePath)
#判断 json 文件里的 selected_machine 值是否是大机型
with open(localFilePath, 'r', encoding='utf-8') as f:
jsonData = json.load(f)
selectedMachine = jsonData.get('selected_machine')
dirNewName = ""
if selectedMachine == '大机型':
#修改 dirNow 目录里的 versionId 命名为 versionId_big
dirNewName = os.path.join(dirNow, versionId + '_big')
os.rename(os.path.join(dirNow, versionId), dirNewName)
# 修复:正确获取嵌套的 selected_machine 值
summary = jsonData.get('summary', {})
if isinstance(summary, str):
# 如果 summary 是字符串,可能是旧格式,尝试直接获取
selectedMachine = jsonData.get('selected_machine', '')
else:
#修改 dirNow 目录里的 versionId 命名为 versionId_small
dirNewName = versionId + '_small'
os.rename(os.path.join(dirNow, versionId), dirNewName)
selectedMachine = summary.get('selected_machine', '') if isinstance(summary, dict) else ''
# 根据机型类型确定新的目录名,直接创建 batchPrint/10010_big 或 batchPrint/10010_small
batchPrintDir = os.path.join(currentDir, 'batchPrint')
if selectedMachine == '大机型':
dirNewName = os.path.join(batchPrintDir, versionId + '_big')
else:
dirNewName = os.path.join(batchPrintDir, versionId + '_small')
# 如果新目录不存在,创建它
if not os.path.exists(dirNewName):
os.makedirs(dirNewName)
# 创建json子目录
jsonSubDir = os.path.join(dirNewName, 'json')
if not os.path.exists(jsonSubDir):
os.makedirs(jsonSubDir)
# 移动JSON文件到新目录
newJsonPath = os.path.join(jsonSubDir, '3DPrintLayout.json')
if os.path.exists(localFilePath):
if os.path.exists(newJsonPath):
os.remove(newJsonPath)
shutil.move(localFilePath, newJsonPath)
# 下载JPG文件(如果存在)
if ossClient().object_exists(jpgFilePath):
localJpgFilePath = os.path.join(dirNewName,"json", f'{versionId}.jpg')
localJpgFilePath = os.path.join(jsonSubDir, f'{versionId}.jpg')
ossClient().get_object_to_file(jpgFilePath, localJpgFilePath)
# 清理临时目录
try:
if os.path.exists(tempDir):
shutil.rmtree(tempDir)
except:
pass
return dirNewName
#根据下载下来的JSON文件传递,调用其它参数,进行处理
@@ -116,6 +185,7 @@ def step2(jsonFilePath,folder, versionId):
# 直接调用函数,而不是通过os.system执行python命令
# 这样可以避免在打包成exe后找不到python命令的问题
workdir = os.path.join(folder, 'data')
print(f"模型数据存储目录={workdir}, jsonFilePath={jsonFilePath}")
download_transform_save_by_json(jsonFilePath, workdir, '')
@@ -158,23 +228,28 @@ def main(work_dir=None):
versionId = str(data)
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 正在处理版次ID={versionId}')
#执行前,先删除指定目录下的所有文件
objFilePath = os.path.join(currentDir, 'batchPrint', versionId, 'data')
#判断目录是否存在,存在就直接删除
if os.path.exists(objFilePath):
shutil.rmtree(objFilePath)
#执行前,先删除可能存在的旧数据目录(包括 _big 和 _small),删除整个目录
for suffix in ['_big', '_small']:
objFilePath = os.path.join(currentDir, 'batchPrint', versionId + suffix)
if os.path.exists(objFilePath):
shutil.rmtree(objFilePath, ignore_errors=True)
res = step1(versionId)
if res == False:
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} JSON文件下载数据失败,等待10秒')
time.sleep(10)
continue
jsonFilePath = os.path.join(res, 'json', f'{versionId}.json')
step2(res,res, versionId)
jsonFilePath = os.path.join(res, 'json', '3DPrintLayout.json')
# 确保 jsonFilePath 是文件而不是目录
if not os.path.isfile(jsonFilePath):
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} JSON文件不存在: {jsonFilePath},等待10秒')
time.sleep(10)
continue
step2(jsonFilePath, res, versionId)
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} 数据处理完成,等待10秒')
#判断下载的obj文件数量和json里的是否一致,排除arrange文件夹
objFilePath = os.path.join(currentDir, 'batchPrint', versionId, 'data')
objFilePath = os.path.join(res, 'data')
objCounts = 0
for file in os.listdir(objFilePath):
if file == 'arrange':
@@ -182,7 +257,16 @@ def main(work_dir=None):
if file.endswith('.obj'):
objCounts += 1
requestApiToUpdateSliceStatus(versionId,objCounts)
time.sleep(10)
# 在长时间操作后,确保 Redis 连接仍然有效
# 通过重新获取客户端来触发连接检查
try:
r = redisClient()
r.ping() # 测试连接
except Exception as e:
print(f'{time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())} Redis连接检查失败: {str(e)},将在下次循环时自动重连')
#time.sleep(10)
if __name__ == '__main__':
+10 -51
View File
@@ -1,54 +1,13 @@
from pathlib import Path
import os
import shutil
# 指定要遍历的文件夹路径
folder_path = Path('G://obj_fix/print')
import requests,json,os
# 定义函数来递归遍历文件夹下的所有文件
def list_files(directory):
for root, dirs, files in os.walk(directory):
for file in files:
yield os.path.join(root, file)
objFilePath = "/Users/dcx/code/make2/script/factory_sliceing/batchPrint/45/data"
objCounts = 0
for file in os.listdir(objFilePath):
print(file)
# if file == 'arrange':
# continue
if file.endswith('.obj'):
objCounts += 1
# 指定要遍历的文件夹路径
# folder_path = folder_path
# 使用list_files函数来列出文件夹下的所有文件
nums = 0
for file_path in list_files(folder_path):
if "around" in file_path:
continue
if "_new.obj" not in file_path:
continue
#提取pid
arrTemp = file_path.split('\\')
pid = arrTemp[len(arrTemp)-2]
#判段是否存在 {pid}.obj 文件,存在的话就进行重命名
initObjPath = f'G://obj_fix/print/{pid}/{pid}.obj'
if not os.path.exists(initObjPath):
continue
#存在的话就重命名文件
old_file_path = f'G://obj_fix/print/{pid}/{pid}_old.obj'
os.rename(initObjPath, old_file_path)
newObjfilePath = f'G://obj_fix/print/{pid}/{pid}_new.obj'
os.rename(newObjfilePath, initObjPath)
# # 将 new.obj 文件复制到指定的文件夹下 G:\obj_fix\print\{pid}\
# source_obj = file_path
# target_obj = f'G://obj_fix/print/{pid}/{pid}_new.obj'
# print(f"当前正在处理{pid}")
# shutil.copy(source_obj, target_obj)
nums += 1
print(pid,file_path,nums)
print(objCounts)
+21 -30
View File
@@ -1,33 +1,24 @@
import os
import shutil
import requests
folder_path = 'G:\\obj_fix\print'
#向 https://lenovodata.blob.core.windows.net/secretdata/photos/1/photo2/92_2.jpg?se=2025-10-30T10%3A01%3A13Z&sig=bv2KAPuNkYT5%2B4tNVMo53kWKIoniQaDkog27AYAXm4s%3D&sp=rcw&spr=https&sr=b&st=2025-10-30T09%3A01%3A13Z&sv=2025-05-05
# 上传图片
def upload_image(image_path):
url = 'https://lenovodata.blob.core.windows.net/secretdata/photos/1/photo2/92_2.jpg?se=2025-10-30T10%3A01%3A13Z&sig=bv2KAPuNkYT5%2B4tNVMo53kWKIoniQaDkog27AYAXm4s%3D&sp=rcw&spr=https&sr=b&st=2025-10-30T09%3A01%3A13Z&sv=2025-05-05'
with open(image_path, 'rb') as f:
image_data = f.read()
headers = {
'x-ms-blob-type': 'BlockBlob',
'Content-Type': 'image/jpeg',
}
response = requests.put(url, data=image_data, headers=headers)
try:
response.raise_for_status()
except requests.HTTPError as e:
raise RuntimeError(f'上传失败 {response.status_code}: {response.text}') from e
return {
'status_code': response.status_code,
'etag': response.headers.get('ETag'),
}
# 遍历文件夹下的所有文件
# 定义函数来递归遍历文件夹下的所有文件
def list_files(directory):
for root, dirs, files in os.walk(directory):
for file in files:
yield os.path.join(root, file)
nums = 0
for file_path in list_files(folder_path):
if "_preview.png" not in file_path:
continue
print(file_path)
nums += 1
# #提取pid
# arrTemp = file_path.split('\\')
# pid = arrTemp[len(arrTemp)-2]
# #判断 G:\obj_fix\print\wrong\ 文件夹里 是否存在 {pid}_preview.png 文件
# filePath = f'G://obj_fix\print\wrong\{pid}_preview.png'
# #不存在跳过
# if not os.path.exists(filePath):
# continue
# #存在的话就重命名文件
# new_file_path = f'G://obj_fix\print\wrong\{pid}_preview_already_right.png'
# os.rename(filePath, new_file_path)
print(nums)
upload_image('273628Tex1.jpg')