Browse Source

Merge remote-tracking branch 'origin/master'

# Conflicts:
#	electron/config/config.default.js
#	electron/config/config.local.js
#	package.json
panqiuyao 8 months ago
parent
commit
2f75190b4e
71 changed files with 13826 additions and 109 deletions
  1. 4 0
      .gitignore
  2. 105 40
      electron/config/bin.js
  3. 3 1
      electron/config/builder.json
  4. 48 31
      electron/config/config.default.js
  5. 32 1
      electron/config/config.local.js
  6. 99 0
      electron/controller/cross.js
  7. 11 7
      electron/preload/index.js
  8. 116 0
      electron/service/cross.js
  9. 0 29
      electron/service/example.js
  10. 2 0
      package.json
  11. 20 0
      python/.env
  12. 8 0
      python/.gitignore
  13. 230 0
      python/action.json
  14. 319 0
      python/api.py
  15. 42 0
      python/config.ini
  16. 239 0
      python/databases.py
  17. 212 0
      python/docs/socket命令.md
  18. 52 0
      python/index.py
  19. 26 0
      python/logger.py
  20. 97 0
      python/mcu/BaseClass.py
  21. 451 0
      python/mcu/BlueToothMode.py
  22. 1273 0
      python/mcu/DeviceControl.py
  23. 136 0
      python/mcu/DeviceHandler.py
  24. 1066 0
      python/mcu/Mcu.py
  25. 79 0
      python/mcu/McuDebug.py
  26. 292 0
      python/mcu/McuDeviationSet.py
  27. 395 0
      python/mcu/ProgramItem.py
  28. 500 0
      python/mcu/RemoteControlV2.py
  29. 321 0
      python/mcu/SerialIns.py
  30. 0 0
      python/mcu/__init__.py
  31. 0 0
      python/mcu/capture/__init__.py
  32. 199 0
      python/mcu/capture/capture_basic_mode.py
  33. 50 0
      python/mcu/capture/module_digicam.py
  34. 199 0
      python/mcu/capture/module_watch_dog.py
  35. 80 0
      python/middleware.py
  36. 3 0
      python/model/__init__.py
  37. 31 0
      python/model/device_config.py
  38. 15 0
      python/model/photo_record.py
  39. 10 0
      python/model/sys_configs.py
  40. 57 0
      python/models.py
  41. BIN
      python/requestments.txt
  42. 475 0
      python/service/base.py
  43. 1152 0
      python/service/base_deal.py
  44. 28 0
      python/service/data.py
  45. 133 0
      python/service/deal_cutout.py
  46. 437 0
      python/service/deal_image.py
  47. 423 0
      python/service/deal_one_image.py
  48. 138 0
      python/service/excel_base_func.py
  49. 446 0
      python/service/grenerate_main_image_test.py
  50. 46 0
      python/service/handle_detail.py
  51. 264 0
      python/service/image_deal_base_func.py
  52. 639 0
      python/service/image_pic_deal.py
  53. 95 0
      python/service/init_load_source.py
  54. 279 0
      python/service/module_generate_goods_art_no_table.py
  55. 102 0
      python/service/pic_deal.py
  56. 268 0
      python/service/remove_bg_ali.py
  57. 1004 0
      python/service/run_main.py
  58. 460 0
      python/service/upload_pic.py
  59. 118 0
      python/settings.py
  60. 25 0
      python/setup.py
  61. 2 0
      python/sockets/__init__.py
  62. 39 0
      python/sockets/connect_manager.py
  63. 108 0
      python/sockets/message_handler.py
  64. 44 0
      python/sockets/socket_client.py
  65. 75 0
      python/sockets/socket_server.py
  66. 14 0
      python/sys_configs.json
  67. 13 0
      python/temp.py
  68. 12 0
      python/utils/SingletonType.py
  69. 4 0
      python/utils/common.py
  70. 22 0
      python/utils/hlm_http_request.py
  71. 139 0
      python/utils/utils_func.py

+ 4 - 0
.gitignore

@@ -8,3 +8,7 @@ data/
 .vscode/launch.json
 public/electron/
 pnpm-lock.yaml
+__pycache__
+*.pyc
+build/*
+**/dist/

+ 105 - 40
electron/config/bin.js

@@ -9,19 +9,20 @@ module.exports = {
    */
   dev: {
     frontend: {
-      directory: './frontend',
-      cmd: 'npm',
-      args: ['run', 'dev'],
-      protocol: 'http://',
-      hostname: 'localhost',
+      directory: "./frontend",
+      cmd: "npm",
+      args: ["run", "dev"],
+      protocol: "http://",
+      hostname: "localhost",
       port: 8080,
-      indexPath: 'index.html'
+      indexPath: "index.html",
     },
     electron: {
-      directory: './',
-      cmd: 'electron',
-      args: ['.', '--env=local', '--color=always'],
-    }
+      directory: "./",
+      cmd: "electron",
+      args: [".", "--env=local"],
+      loadingPage: "/public/html/loading.html",
+    },
   },
 
   /**
@@ -30,52 +31,92 @@ module.exports = {
    */
   build: {
     frontend: {
-      directory: './frontend',
-      cmd: 'npm',
-      args: ['run', 'build'],
-    }
+      directory: "./frontend",
+      cmd: "npm",
+      args: ["run", "build"],
+    },
+    go_w: {
+      directory: "./go",
+      cmd: "go",
+      args: ["build", "-o=../build/extraResources/goapp.exe"],
+    },
+    go_m: {
+      directory: "./go",
+      cmd: "go",
+      args: ["build", "-o=../build/extraResources/goapp"],
+    },
+    go_l: {
+      directory: "./go",
+      cmd: "go",
+      args: ["build", "-o=../build/extraResources/goapp"],
+    },
+    python: {
+      directory: "./python",
+      cmd: "python",
+      args: ["./setup.py", "build"],
+    },
   },
 
   /**
    * 移动资源
-   * ee-bin move 
+   * ee-bin move
    */
   move: {
     frontend_dist: {
-      dist: './frontend/dist',
-      target: './public/dist'
-    }
-  },  
+      dist: "./frontend/dist",
+      target: "./public/dist",
+    },
+    go_static: {
+      dist: "./frontend/dist",
+      target: "./go/public/dist",
+    },
+    go_config: {
+      dist: "./go/config",
+      target: "./go/public/config",
+    },
+    go_package: {
+      dist: "./package.json",
+      target: "./go/public/package.json",
+    },
+    go_images: {
+      dist: "./public/images",
+      target: "./go/public/images",
+    },
+    python_dist: {
+      dist: "./python/dist",
+      target: "./build/extraResources/py",
+    },
+  },
 
   /**
    * 预发布模式(prod)
    * ee-bin start
    */
   start: {
-    directory: './',
-    cmd: 'electron',
-    args: ['.', '--env=prod']
+    directory: "./",
+    cmd: "electron",
+    args: [".", "--env=prod"],
   },
 
   /**
    * 加密
-   */  
+   */
   encrypt: {
-    type: 'confusion',
+    type: "confusion",
     files: [
-      'electron/**/*.(js|json)',
-      '!electron/config/encrypt.js',
-      '!electron/config/nodemon.json',
-      '!electron/config/builder.json',
-      '!electron/config/bin.json',
+      "electron/**/*.(js|json)",
+      "!electron/config/encrypt.js",
+      "!electron/config/nodemon.json",
+      "!electron/config/builder.json",
+      "!electron/config/bin.js",
     ],
-    fileExt: ['.js'],
+    fileExt: [".js"],
     confusionOptions: {
-      compact: true,      
+      compact: true,
       stringArray: true,
-      stringArrayEncoding: ['none'],
+      stringArrayEncoding: ["none"],
       deadCodeInjection: false,
-    }
+    },
   },
 
   /**
@@ -84,14 +125,38 @@ module.exports = {
    */
   exec: {
     node_v: {
-      directory: './',
-      cmd: 'node',
-      args: ['-v'],
+      directory: "./",
+      cmd: "node",
+      args: ["-v"],
     },
     npm_v: {
-      directory: './',
-      cmd: 'npm',
-      args: ['-v'],
+      directory: "./",
+      cmd: "npm",
+      args: ["-v"],
     },
-  },   
+    // 单独调试,air 实现 go 热重载
+    go: {
+      directory: "./go",
+      cmd: "air",
+      args: ["-c=config/.air.toml"],
+    },
+    // windows 单独调试,air 实现 go 热重载
+    go_w: {
+      directory: "./go",
+      cmd: "air",
+      args: ["-c=config/.air.windows.toml"],
+    },
+    // 单独调试,以基础方式启动 go
+    go2: {
+      directory: "./go",
+      cmd: "go",
+      args: ["run", "./main.go", "--env=dev", "--basedir=../", "--port=7073"],
+    },
+    python: {
+      directory: "./python",
+      cmd: "python",
+      args: ["./main.py", "--port=7074"],
+      stdio: "inherit", // ignore
+    },
+  },
 };

+ 3 - 1
electron/config/builder.json

@@ -11,6 +11,8 @@
     "!frontend/",
     "!run/",
     "!logs/",
+    "!go/",
+    "!python/",
     "!data/"
   ],
   "extraResources": {
@@ -50,7 +52,7 @@
     ]
   },
   "linux": {
-    "icon": "build/icons/icon.icns",
+    "icon": "build/icons",
     "artifactName": "${productName}-${os}-${version}-${arch}.${ext}",
     "target": [
       "deb"

+ 48 - 31
electron/config/config.default.js

@@ -49,16 +49,16 @@ module.exports = (appInfo) => {
    * ee框架日志
    */
   config.logger = {
-    encoding: 'utf8',
-    level: 'INFO',
+    encoding: "utf8",
+    level: "INFO",
     outputJSON: false,
     buffer: true,
     enablePerformanceTimer: false,
-    rotator: 'day',
-    appLogName: 'ee.log',
-    coreLogName: 'ee-core.log',
-    errorLogName: 'ee-error.log'
-  }
+    rotator: "day",
+    appLogName: "ee.log",
+    coreLogName: "ee-core.log",
+    errorLogName: "ee-error.log",
+  };
 
   /**
    * 远程模式-web地址
@@ -83,9 +83,29 @@ module.exports = (appInfo) => {
     cors: {
       origin: true,
     },
-    channel: 'c1'
+    channel: "c1",
+  };
+  /**
+   * Cross-language service
+   * 跨语言服务
+   * 例如:执行go的二进制程序,默认目录为 ./extraResources/
+   */
+  config.cross = {
+    go: {
+      enable: false,
+      name: "goapp",
+      args: ["--port=7073"],
+      appExit: true,
+    },
+    python: {
+      enable: false,
+      name: "pyapp",
+      cmd: "./py/pyapp",
+      directory: "./py",
+      args: ["--port=7074"],
+      appExit: true,
+    },
   };
-
   /**
    * 内置http服务
    */
@@ -93,26 +113,24 @@ module.exports = (appInfo) => {
     enable: true,
     https: {
       enable: false,
-      key: '/public/ssl/localhost+1.key',
-      cert: '/public/ssl/localhost+1.pem'
+      key: "/public/ssl/localhost+1.key",
+      cert: "/public/ssl/localhost+1.pem",
     },
-    host: '127.0.0.1',
+    host: "127.0.0.1",
     port: 7071,
     cors: {
-      origin: "*"
+      origin: "*",
     },
     body: {
       multipart: true,
       formidable: {
-        keepExtensions: true
-      }
+        keepExtensions: true,
+      },
     },
     filterRequest: {
-      uris:  [
-        'favicon.ico'
-      ],
-      returnData: ''
-    }
+      uris: ["favicon.ico"],
+      returnData: "",
+    },
   };
 
   /**
@@ -127,7 +145,7 @@ module.exports = (appInfo) => {
    * 硬件加速
    */
   config.hardGpu = {
-    enable: true
+    enable: true,
   };
 
   /**
@@ -143,7 +161,7 @@ module.exports = (appInfo) => {
    * jobs
    */
   config.jobs = {
-    messageLog: true
+    messageLog: true,
   };
 
   /**
@@ -155,16 +173,16 @@ module.exports = (appInfo) => {
     },
     tray: {
       enable: true,
-      title: 'EE程序',
-      icon: '/public/images/tray.png'
+      title: "EE程序",
+      icon: "/public/images/tray.png",
     },
     security: {
       enable: true,
     },
     awaken: {
       enable: true,
-      protocol: 'ee',
-      args: []
+      protocol: "ee",
+      args: [],
     },
     autoUpdater: {
       enable: true,
@@ -172,14 +190,13 @@ module.exports = (appInfo) => {
       macOS: false,
       linux: false,
       options: {
-        provider: 'generic',
-        url: 'http://kodo.qiniu.com/'
+        provider: "generic",
+        url: "http://kodo.qiniu.com/",
       },
       force: false,
-    }
+    },
   };
-
   return {
-    ...config
+    ...config,
   };
 }

+ 32 - 1
electron/config/config.local.js

@@ -10,13 +10,44 @@ module.exports = (appInfo) => {
   /**
    * 开发者工具
    */
-  config.openDevTools = true;
+  config.openDevTools = {
+    mode: "undocked",
+  };
 
   /**
    * 应用程序顶部菜单
    */
   config.openAppMenu = false;
 
+  config.openAppMenu = true;
+  /**
+   * Cross-language service
+   * 跨语言服务
+   * 如果有cmd参数,则执行该命令且需要指定 directory
+   */
+  config.cross = {
+    go: {
+      // 应用运行时启动
+      enable: false,
+      // 程序名
+      name: "goapp",
+      // 可执行程序
+      cmd: "go",
+      // 程序目录
+      directory: "./go",
+      args: ["run", "./main.go", "--env=dev", "--basedir=../", "--port=7073"],
+      appExit: true,
+    },
+    python: {
+      enable: false,
+      name: "pyapp",
+      cmd: "python",
+      directory: "./python",
+      args: ["./main.py", "--port=7074"],
+      stdio: "ignore",
+      appExit: true,
+    },
+  };
   /**
    * jobs
    */

+ 99 - 0
electron/controller/cross.js

@@ -0,0 +1,99 @@
+"use strict";
+
+const { Controller } = require("ee-core");
+const Cross = require("ee-core/cross");
+const Log = require("ee-core/log");
+const HttpClient = require("ee-core/httpclient");
+const Services = require("ee-core/services");
+
+/**
+ * Cross
+ * @class
+ */
+class CrossController extends Controller {
+  constructor(ctx) {
+    super(ctx);
+  }
+
+  /**
+   * View process service information
+   */
+  info() {
+    const pids = Cross.getPids();
+    Log.info("cross pids:", pids);
+
+    let num = 1;
+    pids.forEach((pid) => {
+      let entity = Cross.getProc(pid);
+      Log.info(`server-${num} name:${entity.name}`);
+      Log.info(`server-${num} config:`, entity.config);
+      num++;
+    });
+
+    return "hello electron-egg";
+  }
+
+  /**
+   * Get service url
+   */
+  async getUrl(args) {
+    const { name } = args;
+    const serverUrl = Cross.getUrl(name);
+    return serverUrl;
+  }
+
+  /**
+   * kill service
+   * By default (modifiable), killing the process will exit the electron application.
+   */
+  async killServer(args) {
+    const { type, name } = args;
+    if (type == "all") {
+      Cross.killAll();
+    } else {
+      Cross.killByName(name);
+    }
+
+    return;
+  }
+
+  /**
+   * create service
+   */
+  async createServer(args) {
+    const { program } = args;
+    if (program == "go") {
+      Services.get("cross").createGoServer();
+    } else if (program == "java") {
+      Services.get("cross").createJavaServer();
+    } else if (program == "python") {
+      Services.get("cross").createPythonServer();
+    }
+
+    return;
+  }
+
+  /**
+   * Access the api for the cross service
+   */
+  async requestApi(args) {
+    const { name, urlPath, params } = args;
+    const hc = new HttpClient();
+    const serverUrl = Cross.getUrl(name);
+    console.log("Server Url:", serverUrl);
+
+    const apiHello = serverUrl + urlPath;
+    const options = {
+      method: "GET",
+      data: params || {},
+      dataType: "json",
+      timeout: 1000,
+    };
+    const result = await hc.request(apiHello, options);
+
+    return result.data;
+  }
+}
+
+CrossController.toString = () => "[class CrossController]";
+module.exports = CrossController;

+ 11 - 7
electron/preload/index.js

@@ -1,14 +1,18 @@
 /*************************************************
  ** preload为预加载模块,该文件将会在程序启动时加载 **
  *************************************************/
-const Addon = require('ee-core/addon');
+const Addon = require("ee-core/addon");
+const Services = require("ee-core/services");
 
 /**
-* 预加载模块入口
-*/
+ * 预加载模块入口
+ */
 module.exports = async () => {
+  // 已实现的功能模块,可选择性使用和修改
+  Addon.get("tray").create();
+  Addon.get("security").create();
+  Addon.get("awaken").create();
+  Addon.get("autoUpdater").create();
 
-  // 示例功能模块,可选择性使用和修改
-  Addon.get('tray').create();
-  Addon.get('security').create();
-}
+  Services.get("cross").createPythonServer();
+};

+ 116 - 0
electron/service/cross.js

@@ -0,0 +1,116 @@
+"use strict";
+
+const { Service } = require("ee-core");
+const Cross = require("ee-core/cross");
+const Log = require("ee-core/log");
+const Ps = require("ee-core/ps");
+const path = require("path");
+const Is = require("ee-core/utils/is");
+
+/**
+ * cross(service层为单例)
+ * @class
+ */
+class CrossService extends Service {
+  constructor(ctx) {
+    super(ctx);
+  }
+
+  /**
+   * create go service
+   * In the default configuration, services can be started with applications.
+   * Developers can turn off the configuration and create it manually.
+   */
+  async createGoServer() {
+    // method 1: Use the default Settings
+    //const entity = await Cross.run(serviceName);
+
+    // method 2: Use custom configuration
+    const serviceName = "go";
+    const opt = {
+      name: "goapp",
+      cmd: path.join(Ps.getExtraResourcesDir(), "goapp"),
+      directory: Ps.getExtraResourcesDir(),
+      args: ["--port=7073"],
+      appExit: true,
+    };
+    const entity = await Cross.run(serviceName, opt);
+    Log.info("server name:", entity.name);
+    Log.info("server config:", entity.config);
+    Log.info("server url:", entity.getUrl());
+
+    return;
+  }
+
+  /**
+   * create java server
+   */
+  async createJavaServer() {
+    const serviceName = "java";
+    const jarPath = path.join(Ps.getExtraResourcesDir(), "java-app.jar");
+    const opt = {
+      name: "javaapp",
+      cmd: path.join(Ps.getExtraResourcesDir(), "jre1.8.0_201/bin/javaw.exe"),
+      directory: Ps.getExtraResourcesDir(),
+      args: [
+        "-jar",
+        "-server",
+        "-Xms512M",
+        "-Xmx512M",
+        "-Xss512k",
+        "-Dspring.profiles.active=prod",
+        `-Dserver.port=18080`,
+        `-Dlogging.file.path=${Ps.getLogDir()}`,
+        `${jarPath}`,
+      ],
+      appExit: false,
+    };
+    if (Is.macOS()) {
+      // Setup Java program
+      opt.cmd = path.join(
+        Ps.getExtraResourcesDir(),
+        "jre1.8.0_201.jre/Contents/Home/bin/java"
+      );
+    }
+    if (Is.linux()) {
+      // Setup Java program
+    }
+
+    const entity = await Cross.run(serviceName, opt);
+    Log.info("server name:", entity.name);
+    Log.info("server config:", entity.config);
+    Log.info("server url:", Cross.getUrl(entity.name));
+
+    return;
+  }
+
+  /**
+   * create python service
+   * In the default configuration, services can be started with applications.
+   * Developers can turn off the configuration and create it manually.
+   */
+  async createPythonServer() {
+    // method 1: Use the default Settings
+    //const entity = await Cross.run(serviceName);
+
+    // method 2: Use custom configuration
+    const serviceName = "python";
+    const opt = {
+      name: "pyapp",
+      cmd: path.join(Ps.getExtraResourcesDir(), "py", "pyapp"),
+      directory: path.join(Ps.getExtraResourcesDir(), "py"),
+      args: ["--port=7074"],
+      windowsExtname: true,
+      appExit: true,
+    };
+    const entity = await Cross.run(serviceName, opt);
+    Log.info("server name:", entity.name);
+    Log.info("server config:", entity.config);
+    Log.info("server url:", entity.getUrl());
+
+    return;
+  }
+}
+
+CrossService.toString = () => "[class CrossService]";
+module.exports = CrossService;

+ 0 - 29
electron/service/example.js

@@ -1,29 +0,0 @@
-'use strict';
-
-const { Service } = require('ee-core');
-
-/**
- * 示例服务(service层为单例)
- * @class
- */
-class ExampleService extends Service {
-
-  constructor(ctx) {
-    super(ctx);
-  }
-
-  /**
-   * test
-   */
-  async test(args) {
-    let obj = {
-      status:'ok',
-      params: args
-    }
-
-    return obj;
-  }
-}
-
-ExampleService.toString = () => '[class ExampleService]';
-module.exports = ExampleService;

+ 2 - 0
package.json

@@ -5,6 +5,7 @@
   "main": "main.js",
   "scripts": {
     "dev": "ee-bin dev",
+    "dev-python": "ee-bin exec --cmds=python",
     "dev-frontend": "ee-bin dev --serve=frontend",
     "dev-electron": "ee-bin dev --serve=electron",
     "build-frontend": "ee-bin build --cmds=frontend && ee-bin move --flag=frontend_dist",
@@ -12,6 +13,7 @@
     "rd": "ee-bin move --flag=frontend_dist",
     "encrypt": "ee-bin encrypt",
     "clean": "ee-bin clean",
+    "build-python": "ee-bin build --cmds=python && ee-bin move --flag=python_dist",
     "icon": "ee-bin icon",
     "reload": "nodemon --config ./electron/config/nodemon.json",
     "rebuild": "electron-rebuild",

+ 20 - 0
python/.env

@@ -0,0 +1,20 @@
+# 应用名称
+app_name="智慧拍-后端应用"
+# 应用host地址
+app_host="0.0.0.0"
+# 应用服务启动名称
+app_run="api:app"
+# 应用版本号
+version="1.0.0"
+# 是否调试
+debug=True
+# 端口号
+port=7074
+# 线程数
+works=1
+# 日志相关
+log_file_name="app.log" #日志名称
+#最大字节数
+max_bytes=102400 
+#备份数量
+backup_counts=3 

+ 8 - 0
python/.gitignore

@@ -0,0 +1,8 @@
+.venv/
+venv/
+*.log
+*.log.*
+resources/
+custom_plugins/
+*.db
+*.pdf

+ 230 - 0
python/action.json

@@ -0,0 +1,230 @@
+[
+    {
+        "mode_type": "执行左脚程序",
+        "execution_type": "程序1",
+        "action_name": "左脚俯拍鞋子",
+        "action_index": 20,
+        "picture_index": 99,
+        "camera_height": 200,
+        "camera_angle": 14.0,
+        "number_focus": 2,
+        "take_picture": true,
+        "turntable_position": 300.0,
+        "turntable_angle": -32.0,
+        "shoe_upturn": false,
+        "pre_delay": 0.0,
+        "after_delay": 0.0,
+        "led_switch": false,
+        "is_wait": false,
+        "is_need_confirm": false
+    },
+    {
+        "mode_type": "执行左脚程序",
+        "execution_type": "程序1",
+        "action_name": "侧视图",
+        "action_index": 10,
+        "picture_index": 99,
+        "camera_height": 0,
+        "camera_angle": 3.0,
+        "number_focus": 0,
+        "take_picture": true,
+        "turntable_position": 300.0,
+        "turntable_angle": 0.0,
+        "shoe_upturn": false,
+        "pre_delay": 0.0,
+        "after_delay": 0.0,
+        "led_switch": false,
+        "is_wait": false,
+        "is_need_confirm": false
+    },
+    {
+        "mode_type": "执行左脚程序",
+        "execution_type": "程序1",
+        "action_name": "拍后跟",
+        "action_index": 30,
+        "picture_index": 99,
+        "camera_height": 0,
+        "camera_angle": 3.0,
+        "number_focus": 0,
+        "take_picture": true,
+        "turntable_position": 450.0,
+        "turntable_angle": 70.0,
+        "shoe_upturn": false,
+        "pre_delay": 0.0,
+        "after_delay": 0.0,
+        "led_switch": false,
+        "is_wait": false,
+        "is_need_confirm": false
+    },
+    {
+        "mode_type": "执行左脚程序",
+        "execution_type": "程序2",
+        "action_name": "拍鞋底",
+        "action_index": 40,
+        "picture_index": 99,
+        "camera_height": 0,
+        "camera_angle": 3.0,
+        "number_focus": 0,
+        "take_picture": true,
+        "turntable_position": 100.0,
+        "turntable_angle": 0.0,
+        "shoe_upturn": true,
+        "pre_delay": 0.0,
+        "after_delay": 0.0,
+        "led_switch": false,
+        "is_wait": false,
+        "is_need_confirm": false
+    },
+    {
+        "mode_type": "执行左脚程序",
+        "execution_type": "程序2",
+        "action_name": "拍内里",
+        "action_index": 50,
+        "picture_index": 99,
+        "camera_height": 0,
+        "camera_angle": 3.0,
+        "number_focus": 0,
+        "take_picture": true,
+        "turntable_position": 500.0,
+        "turntable_angle": 180.0,
+        "shoe_upturn": false,
+        "pre_delay": 0.0,
+        "after_delay": 0.0,
+        "led_switch": false,
+        "is_wait": false,
+        "is_need_confirm": false
+    },
+    {
+        "mode_type": "执行左脚程序",
+        "execution_type": "程序2",
+        "action_name": "初始化位置",
+        "action_index": 60,
+        "picture_index": 99,
+        "camera_height": 200,
+        "camera_angle": 12.0,
+        "number_focus": 1,
+        "take_picture": false,
+        "turntable_position": 300.0,
+        "turntable_angle": 0.0,
+        "shoe_upturn": false,
+        "pre_delay": 0.0,
+        "after_delay": 0.0,
+        "led_switch": true,
+        "is_wait": false,
+        "is_need_confirm": false
+    },
+    {
+        "mode_type": "执行右脚程序",
+        "execution_type": "程序1",
+        "action_name": "右脚俯拍鞋子",
+        "action_index": 20,
+        "picture_index": 99,
+        "camera_height": 200,
+        "camera_angle": 14.0,
+        "number_focus": 2,
+        "take_picture": true,
+        "turntable_position": 300.0,
+        "turntable_angle": -32.0,
+        "shoe_upturn": false,
+        "pre_delay": 0.0,
+        "after_delay": 0.0,
+        "led_switch": false,
+        "is_wait": false,
+        "is_need_confirm": false
+    },
+    {
+        "mode_type": "执行右脚程序",
+        "execution_type": "程序1",
+        "action_name": "侧视图",
+        "action_index": 10,
+        "picture_index": 99,
+        "camera_height": 0,
+        "camera_angle": 3.0,
+        "number_focus": 0,
+        "take_picture": true,
+        "turntable_position": 300.0,
+        "turntable_angle": 0.0,
+        "shoe_upturn": false,
+        "pre_delay": 0.0,
+        "after_delay": 0.0,
+        "led_switch": false,
+        "is_wait": false,
+        "is_need_confirm": false
+    },
+    {
+        "mode_type": "执行右脚程序",
+        "execution_type": "程序1",
+        "action_name": "拍后跟",
+        "action_index": 30,
+        "picture_index": 99,
+        "camera_height": 0,
+        "camera_angle": 3.0,
+        "number_focus": 0,
+        "take_picture": true,
+        "turntable_position": 450.0,
+        "turntable_angle": 70.0,
+        "shoe_upturn": false,
+        "pre_delay": 0.0,
+        "after_delay": 0.0,
+        "led_switch": false,
+        "is_wait": false,
+        "is_need_confirm": false
+    },
+    {
+        "mode_type": "执行右脚程序",
+        "execution_type": "程序2",
+        "action_name": "拍鞋底",
+        "action_index": 40,
+        "picture_index": 99,
+        "camera_height": 0,
+        "camera_angle": 3.0,
+        "number_focus": 0,
+        "take_picture": true,
+        "turntable_position": 100.0,
+        "turntable_angle": 0.0,
+        "shoe_upturn": true,
+        "pre_delay": 0.0,
+        "after_delay": 0.0,
+        "led_switch": false,
+        "is_wait": false,
+        "is_need_confirm": false
+    },
+    {
+        "mode_type": "执行右脚程序",
+        "execution_type": "程序2",
+        "action_name": "拍内里",
+        "action_index": 50,
+        "picture_index": 99,
+        "camera_height": 0,
+        "camera_angle": 3.0,
+        "number_focus": 0,
+        "take_picture": true,
+        "turntable_position": 500.0,
+        "turntable_angle": 180.0,
+        "shoe_upturn": false,
+        "pre_delay": 0.0,
+        "after_delay": 0.0,
+        "led_switch": false,
+        "is_wait": false,
+        "is_need_confirm": false
+    },
+    {
+        "mode_type": "执行右脚程序",
+        "execution_type": "程序2",
+        "action_name": "初始化位置",
+        "action_index": 60,
+        "picture_index": 99,
+        "camera_height": 200,
+        "camera_angle": 12.0,
+        "number_focus": 1,
+        "take_picture": false,
+        "turntable_position": 300.0,
+        "turntable_angle": 0.0,
+        "shoe_upturn": false,
+        "pre_delay": 0.0,
+        "after_delay": 0.0,
+        "led_switch": true,
+        "is_wait": false,
+        "is_need_confirm": false
+    }
+]

+ 319 - 0
python/api.py

@@ -0,0 +1,319 @@
+from natsort.natsort import order_by_index
+from sqlalchemy import func
+from models import *
+import requests
+import json
+from logger import logger
+from serial.tools import list_ports
+from model import PhotoRecord
+from utils.hlm_http_request import forward_request
+from sockets.socket_client import socket_manager
+from mcu.DeviceControl import DeviceControl
+import time
+from sqlalchemy import and_, asc, desc
+
+from service.deal_image import DealImage
+from databases import DeviceConfig, SqlQuery, CRUD, select
+
+
+@app.get("/")
+async def index():
+    # await socket_manager.send_message(msg="测试")
+    return {"message": "Hello World"}
+
+
+@app.get("/send_test")
+async def index():
+    data = {"data1": 1, "data2": 2, "data3": 3, "data4": 4}
+    await socket_manager.send_message(msg="测试", data=data)
+    return {"message": "Hello World"}
+
+
+@app.get("/scan_serials", description="扫描可用的设备端口")
+async def scanSerials():
+    """扫描串口"""
+    ports = list_ports.comports()
+    print("Scanning", ports)
+    return {"message": "Hello World"}
+
+
+@app.get("/test_conndevice")
+def test_conndevice():
+    device_control = DeviceControl()
+    p_list = []
+    temp_ports_dict = {}
+    # while 1:
+    time.sleep(1)
+    ports_dict = device_control.scan_serial_port()
+    temp_ports_dict = ports_dict
+
+    if not ports_dict:
+        # 全部清空 移除所有串口
+        if p_list:
+            _p = p_list.pop()
+            device_control.remove_port(_p)
+        # continue
+
+    if ports_dict:
+        # print(plist)
+        for index, _i in enumerate(p_list):
+            if _i not in ports_dict:
+                _p = p_list.pop(index)
+                device_control.remove_port(_p)
+
+        for _port_name, _port_value in ports_dict.items():
+            if _port_name not in p_list:
+                try:
+                    p_list.append(_port_name)
+                    device_control.add_port_by_linkage(_port_name)
+                except BaseException as e:
+                    print(
+                        e.__traceback__.tb_frame.f_globals["__file__"]
+                    )  # 发生异常所在的文件
+                    print(e.__traceback__.tb_lineno)  # 发生异常所在的行数
+                    print("串口不存在{} {}".format(_port_name, e))
+
+            # threading.Thread(target=self.add_port, args=(_port_name, _port_value)).start()
+            # self.add_port(_p)
+
+
+@app.api_route(
+    "/forward_request", methods=["GET", "POST"], description="代理转发hlm项目得请求"
+)
+async def forwardRequest(request: HlmForwardRequest):
+    """
+    转发HTTP请求到目标URL
+
+    :param request: FastAPI Request对象
+    :return: 目标接口的响应
+    """
+    try:
+        if request.method == "GET":
+            params = request.query_params
+        elif request.method == "POST":
+            params = json.dump(request.query_params)
+        else:
+            raise UnicornException("仅支持GET和POST方法")
+        target_url = request.target_url
+        method = request.method.upper()
+        headers = request.headers
+        if not target_url:
+            raise UnicornException("目标url地址不能为空")
+        # 调用 hlm_http_request 中的 forward_request 函数
+        response = forward_request(
+            target_url, params=params, method=method, headers=headers
+        )
+        return response
+    except requests.RequestException as e:
+        raise UnicornException(e)
+    except Exception as e:
+        raise UnicornException(e)
+
+
+@app.post("/handle_detail")
+async def handle_detail(request: Request):
+
+    image_dir = "{}/data".format(os.getcwd())
+    dealImage = DealImage(image_dir)
+    dealImage.header = request.headers
+    print(">>>>>>>>>>>>>>>>>请求参数")
+    print(dealImage.header)
+    result = dealImage.dealMoveImage(image_dir=image_dir, callback_func=None)
+    return result
+
+    #
+    # params = json.dump(request.query_params)
+    # #{'image_dir': 'D:/phpstudy_pro/WWW/auto_photo/output/2024-11-18', 'image_order': '俯视,侧视,后跟,鞋底,内里', 'is_check_number': True, 'resize_image_view': '后跟', 'cutout_mode': '1', 'logo_path': '', 'special_goods_art_no_folder_line': '', 'is_use_excel': True, 'excel_path': '', 'is_check_color_is_all': True, 'assigned_page_dict': {}, 'temp_class': {'huilima-2': <class 'detail_template.huilima.detail_huilima2.DetailPicGet'>, 'huilima-3': <class 'detail_template.huilima.detail_huilima3.DetailPicGet'>, 'huilima-4': <class 'detail_template.huilima.detail_huilima4.DetailPicGet'>, 'huilima-1': <class 'detail_template.huilima.detail_huilima1.DetailPicGet'>}, 'temp_name': 'huilima-2', 'temp_name_list': ['huilima-2', 'huilima-3', 'huilima-4', 'huilima-1'], 'target_error_folder': 'D:/phpstudy_pro/WWW/auto_photo/output/2024-11-18/软件-生成详情错误'}
+    #
+    # config_data = {
+    #     'image_dir': params['image_dir'],
+    #     'image_order': params['image_order'],
+    #     'is_check_number': params['is_check_number'],
+    #     'resize_image_view': params['resize_image_view'],
+    #     'cutout_mode': '1',
+    #     'logo_path': params['logo_path'],
+    #     'special_goods_art_no_folder_line': '',
+    #     'is_use_excel': params['is_use_excel'],
+    #     'excel_path': params['excel_path'],
+    #     'is_check_color_is_all': params['is_check_color_is_all'],
+    #     'assigned_page_dict': {},
+    #     'temp_class': {
+    #         'huilima-2': 'detail_template.huilima.detail_huilima2.DetailPicGet',
+    #         'huilima-3': 'detail_template.huilima.detail_huilima3.DetailPicGet',
+    #         'huilima-4': 'detail_template.huilima.detail_huilima4.DetailPicGet',
+    #         'huilima-1': 'detail_template.huilima.detail_huilima1.DetailPicGet'
+    #     },
+    #     'temp_name': 'huilima-2',
+    #     'temp_name_list': ['huilima-2', 'huilima-3', 'huilima-4', 'huilima-1'],
+    #     'target_error_folder': 'D:/phpstudy_pro/WWW/auto_photo/output/2024-11-18/软件-生成详情错误'
+    # }
+
+
+@app.post("/get_device_configs", description="获取可执行程序命令列表")
+def get_device_configs(params: ModelGetDeviceConfig):
+    mode_type = params.mode_type
+    session = SqlQuery()
+    configModel = CRUD(DeviceConfig)
+    configList = configModel.read_all(
+        session,
+        conditions={"mode_type": mode_type},
+        order_by="action_index",
+        ascending=True,
+    )
+    return {
+        "code": 0,
+        "msg": "",
+        "data": {"list": configList},
+    }
+
+
+@app.post("/device_config_detail", description="获取可执行程序详情")
+def get_device_configs(params: ModelGetDeviceConfigDetail):
+    action_id = params.id
+    session = SqlQuery()
+    configModel = CRUD(DeviceConfig)
+    model = configModel.read(session, conditions={"id": action_id})
+    if model == None:
+        return {"code": 1, "msg": "数据不存在", "data": None}
+    return {"code": 0, "msg": "", "data": model}
+
+
+@app.post("/remove_config", description="删除一条可执行命令")
+def get_device_configs(params: ModelGetDeviceConfigDetail):
+    action_id = params.id
+    session = SqlQuery()
+    configModel = CRUD(DeviceConfig)
+    model = configModel.read(session, conditions={"id": action_id})
+    if model == None:
+        return {"code": 1, "msg": "数据不存在", "data": None}
+    configModel.delete(session, obj_id=action_id)
+    return {"code": 0, "msg": "删除成功", "data": None}
+
+
+@app.post("/save_device_config", description="创建或修改一条可执行命令")
+def save_device_config(params: SaveDeviceConfig):
+    action_id = params.id
+    session = SqlQuery()
+    deviceConfig = CRUD(DeviceConfig)
+    if action_id == None or action_id == 0:
+        # 走新增逻辑
+        params.id = None
+        save_device_config = deviceConfig.create(session, obj_in=params)
+    else:
+        model = deviceConfig.read(session, conditions={"id": action_id})
+        if model == None:
+            return {"code": 1, "msg": "数据不存在", "data": None}
+        # 走编辑逻辑
+        kwargs = params.__dict__
+        save_device_config = deviceConfig.update(session, obj_id=action_id, **kwargs)
+    return {"code": 0, "msg": "操作成功", "data": save_device_config}
+
+
+@app.post("/reset_config", description="创建或修改一条可执行命令")
+def reset_config(params: ModelGetDeviceConfig):
+    mode_type = params.mode_type
+    if mode_type == None or mode_type == "":
+        return {"code": 1, "msg": "参数错误", "data": None}
+    session = SqlQuery()
+    deviceConfig = CRUD(DeviceConfig)
+    res = deviceConfig.deleteConditions(session, conditions={"mode_type": mode_type})
+    if res is False:
+        return {"code": 1, "msg": "操作失败", "data": None}
+    actions = json.load(open("action.json", encoding="utf-8"))
+    act = []
+    for item in actions:
+        if item.get("mode_type") == mode_type:
+            act.append(item)
+    batch_insert_device_configs(session, act)
+    return {"code": 0, "msg": "操作成功", "data": None}
+
+
+@app.get("/get_photo_records", description="获取拍照记录")
+def get_photo_records(page: int = 1, size: int = 5):
+
+    session = SqlQuery()
+    photos = CRUD(PhotoRecord)
+    statement = (
+        select(PhotoRecord)
+        .offset((page - 1) * size)
+        .limit(size)
+        .order_by(desc("id"))
+        .group_by("goods_art_no")
+    )
+    list = []
+    result = session.exec(statement).all()
+    for item in result:
+        list_item = photos.read_all(
+            session, conditions={"goods_art_no": item.goods_art_no}
+        )
+        list.append(
+            {
+                "goods_art_no": item.goods_art_no,
+                "action_time": item.create_time,
+                "items": list_item,
+            }
+        )
+    session.close()
+    return {
+        "code": 0,
+        "msg": "",
+        "data": {"list": list, "page": page, "size": size},
+    }
+
+
+@app.get("/get_photo_record_detail", description="通过货号获取拍照记录详情")
+def get_photo_records(goods_art_no: str = None):
+    if goods_art_no == None:
+        return {"code": 1, "msg": "参数错误", "data": None}
+    session = SqlQuery()
+    photos = CRUD(PhotoRecord)
+    items = photos.read_all(session, conditions={"goods_art_no": goods_art_no})
+    session.close()
+    return {
+        "code": 0,
+        "msg": "",
+        "data": {"list": items},
+    }
+
+
+@app.post("/delect_goods_arts", description="通过货号删除记录")
+def delect_goods_arts(params: PhotoRecordDelete):
+
+    session = SqlQuery()
+    photos = CRUD(PhotoRecord)
+    for item in params.goods_art_nos:
+        photos.deleteConditions(session, conditions={"goods_art_no": item})
+    session.close()
+    return {
+        "code": 0,
+        "msg": "操作成功",
+        "data": None,
+    }
+
+
+@app.get("/get_sys_config", description="查询系统配置")
+def get_sys_config(key: str = None):
+    if key == None:
+        return {"code": 1, "msg": "参数错误", "data": None}
+    session = SqlQuery()
+    photos = CRUD(SysConfigs)
+    item = photos.read(session, conditions={"key": key})
+    session.close()
+    return {
+        "code": 0,
+        "msg": "",
+        "data": json.loads(item.value),
+    }
+
+
+@app.post("/update_sys_configs", description="创建或修改系统配置")
+def save_sys_configs(params: SysConfigParams):
+    session = SqlQuery()
+    sysConfig = CRUD(SysConfigs)
+    model = sysConfig.read(session, conditions={"key": params.key})
+    if model == None:
+        return {"code": 1, "msg": "配置不存在", "data": None}
+    # 走编辑逻辑
+    kwargs = params.__dict__
+    save_device_config = sysConfig.updateConditions(session, conditions={"key":params.key}, **kwargs)
+    return {"code": 0, "msg": "操作成功", "data": save_device_config}

+ 42 - 0
python/config.ini

@@ -0,0 +1,42 @@
+[app]
+# 应用名称
+app_name=智慧拍-后端应用
+# 应用版本号
+version=1.0.0
+# 应用host地址
+host=10.56.42.176
+# 应用服务启动名称
+app_run=api:app
+# 端口号
+port=7074
+debug=true
+env=dev
+# 线程数
+works=1
+[log]
+# 日志相关
+log_file_name=app.log 
+#最大字节数
+max_bytes=102400 
+#备份数量
+backup_counts=3
+# 地址
+hlm_host=https://dev2.pubdata.cn
+
+project=红蜻蜓
+
+
+
+[mcu_config]
+left_foot_action = 1
+right_foot_action = 2
+move_up = 4
+move_down = 5
+next_step = 6
+left_foot_photograph = 3
+left_foot_action_1 = 99
+left_foot_action_2 = 99
+right_foot_photograph = 99
+right_foot_action_1 = 99
+right_foot_action_2 = 99
+stop = 9

+ 239 - 0
python/databases.py

@@ -0,0 +1,239 @@
+from networkx.algorithms.components import connected
+from sqlmodel import Field, Session, SQLModel, create_engine, select
+from typing import Dict
+from datetime import datetime
+from typing import Optional
+import json
+from sqlalchemy import and_, desc,asc
+
+from sqlalchemy.dialects import sqlite
+from model import DeviceConfig, PhotoRecord, SysConfigs
+
+
+# 创建SQLite数据库引擎
+sqlite_file_name = "database.db"
+sqlite_url = f"sqlite:///{sqlite_file_name}"
+engine = create_engine(
+    sqlite_url,
+    echo=False,
+    pool_size=10,
+    max_overflow=20,
+    pool_timeout=30,
+    pool_recycle=1800,
+)
+
+# 创建表
+def create_all_database():
+    SQLModel.metadata.create_all(engine)
+# 创建会话
+def __get_session():
+    with Session(engine) as session:
+        try:
+            yield session
+        finally:
+            session.close()
+
+
+def batch_insert_device_configs(session: Session, data_list: list):
+    '''批量插入数据到设备配置表'''
+    for data in data_list:
+        device_config = DeviceConfig(**data)
+        session.add(device_config)
+    session.commit()
+    session.close()
+
+
+def batch_insert_sys_configs(session: Session, data_list: list):
+    """批量插入数据到设备配置表"""
+    for data in data_list:
+        config = SysConfigs(**data)
+        session.add(config)
+    session.commit()
+    session.close()
+
+
+def insert_photo_records(image_deal_mode: int, goods_art_no: str, image_index:int):
+    session = SqlQuery()
+    """批量插入数据到照片记录"""
+    data = {
+        "image_deal_mode": image_deal_mode,
+        "goods_art_no": goods_art_no,
+        "image_index": image_index,
+    }
+    device_config = PhotoRecord(**data)
+    session.add(device_config)
+    session.commit()
+    session.close()
+    return True
+
+
+# 创建一个通用的 CRUD 类
+class CRUD:
+    def __init__(self, model):
+        self.model = model
+
+    def create(self, session: Session, obj_in):
+        obj_in_data = dict(obj_in)
+        db_obj = self.model(**obj_in_data)
+        session.add(db_obj)
+        session.commit()
+        session.refresh(db_obj)
+        session.close()
+        return db_obj
+
+    def read(
+            self,
+            session: Session,
+            conditions: Optional[Dict] = None,
+            order_by: Optional[str] = None,
+            ascending: bool = True,
+    ):
+        query = select(self.model)
+        if conditions:
+            query = query.where(and_(*(getattr(self.model, key) == value for key, value in conditions.items())))
+        if order_by:
+            if ascending:
+                query = query.order_by(asc(getattr(self.model, order_by)))
+            else:
+                query = query.order_by(desc(getattr(self.model, order_by)))
+        data =  session.exec(query).first()
+        session.close()
+        return data
+
+    def read_all(
+        self,
+        session: Session,
+        conditions: Optional[Dict] = None,
+        order_by: Optional[str] = None,
+        ascending: bool = True,
+    ):
+        query = select(self.model)
+        if conditions:
+            query = query.where(and_(*(getattr(self.model, key) == value for key, value in conditions.items())))
+        if order_by:
+            if ascending:
+                query = query.order_by(asc(getattr(self.model, order_by)))
+            else:
+                query = query.order_by(desc(getattr(self.model, order_by)))
+        data =  session.exec(query).all()
+        session.close()
+        return data
+
+    def update(self, session: Session, obj_id: int, **kwargs):
+        db_obj = session.get(self.model, obj_id)
+        for key, value in kwargs.items():
+            if value == None or value =="":
+                continue
+            setattr(db_obj, key, value)
+        session.commit()
+        session.refresh(db_obj)
+        session.close()
+        return db_obj
+
+    def updateConditions(
+        self,
+        session: Session,
+        conditions: Optional[Dict] = None,
+        **kwargs
+    ):
+        query = select(self.model)
+        if conditions:
+            query = query.where(
+                and_(
+                    *(
+                        getattr(self.model, key) == value
+                        for key, value in conditions.items()
+                    )
+                )
+            )
+        data = session.exec(query).first()
+        for key, value in kwargs.items():
+            if value == None or value == "":
+                continue
+            setattr(data, key, value)
+        session.commit()
+        session.refresh(data)
+        session.close()
+        return data
+
+    def deleteConditions(
+        self,
+        session: Session,
+        conditions: Optional[Dict] = None,
+    ):
+        query = select(self.model)
+        if conditions == None:
+            return False
+        query = query.where(
+            and_(
+                *(
+                    getattr(self.model, key) == value
+                    for key, value in conditions.items()
+                )
+            )
+        )
+        objects_to_delete = session.exec(query).all()
+        for obj in objects_to_delete:
+            session.delete(obj)
+        session.commit()
+        session.close()
+        return True
+
+    def delete(self, session: Session, obj_id: int):
+        db_obj = session.get(self.model, obj_id)
+        session.delete(db_obj)
+        session.commit()
+        session.close()
+
+
+def SqlQuery():
+    return next(__get_session())
+
+# 使用示例
+if __name__ == "__main__":
+    pass
+    # 使用 next 函数从生成器中获取 Session 对象
+    # session = SqlQuery()
+
+    # 创建 CRUD 实例
+    # device_config_crud = CRUD(DeviceConfig)
+
+    # 创建新记录
+    # new_device_config = DeviceConfig(
+    #     mode_type="example_mode",
+    #     execution_type="example_execution",
+    #     action_name="example_action",
+    #     action_index=1,
+    #     picture_index=1,
+    #     camera_height=100,
+    #     camera_angle=45.5,
+    #     number_focus=2,
+    #     take_picture=True,
+    #     turntable_position=10.0,
+    #     turntable_angle=30.5,
+    #     shoe_upturn=False,
+    #     pre_delay=1.5,
+    #     after_delay=2.5,
+    #     led_switch=True,
+    #     is_wait=False,
+    # )
+    # created_device_config = device_config_crud.create(session, new_device_config)
+    # print(f"Created Device Config: {created_device_config}")
+
+    # 读取记录
+    # read_device = device_config_crud.read(session, 1)
+    # print(f"Read Device Config: {read_device.model_dump()}")
+
+    # 读取所有记录
+    # all_devices = device_config_crud.read_all(session, conditions={"id": 2})
+    # print(f"All Device Configs: {[device.model_dump() for device in all_devices]}")
+
+    # # 更新记录
+    # updated_device = device_config_crud.update(
+    #     session, created_device_config.id, mode_type="updated_mode"
+    # )
+    # print(f"Updated Device Config: {updated_device}")
+
+    # # 删除记录
+    # device_config_crud.delete(session, created_device_config.id)
+    # print("Device Config deleted.")

+ 212 - 0
python/docs/socket命令.md

@@ -0,0 +1,212 @@
+## 智慧拍 socket 命令说明
+### Socket 连接信息
+    * 本地: ws://10.56.42.176:7074/ws
+    * 打包后: ws://127.0.0.1:7074/ws
+#### 统一响应示例说明
+* code:为0时代表成功操作
+* msg:为接口执行命令时的提示信息
+* status:-1连接失败  0未连接 1连接中  2链接成功  3端口占用
+* msg_type:mcu为设备控制,blue_tooth为蓝牙控制,blue_tooth_scan 为蓝牙扫码时发送的消息数据
+* data:为返回的数据信息,无实际交互需求时,前端可忽略
+```python
+{
+    "code": 0,
+    "msg": "开始识别接口:COM8",
+    "status": 1,
+    "data": {
+        "_type": "show_info",
+        "plugins_mode": "auto_select_com",
+        "data": {
+            "text": "开始识别接口:COM8"
+        }
+    },
+    "msg_type": "mcu"
+}
+```
+### 设备连接
+*  type 
+    * 值为connect_mcu时,为连接mcu设备操作
+    * 值为connect_bluetooth时,为连接蓝牙遥控器
+* data:可以忽略为null
+#### 请求示例
+```python
+{"data":null,"type":"connect_mcu"}
+```
+_<mark>以下操作前必须保证设备已连接!!!<mark>_
+### 初始化mcu设备
+_<mark>目前连接设备后,后端会自动进行设备初始化操作,如无特殊需求可忽略<mark>_
+*  type 固定为 init_mcu
+* data:可以忽略为null
+#### 请求示例
+```python
+{"data":null,"type":"init_mcu"}
+```
+### 单独控制设备得某一个功能运行命令
+_(该命令用于单独自定义配置中某一项的单独调整测试,不进行任何存储操作)_
+
+* type 为control_mcu时,为设备得单独控制 
+* data中的 device_name释义:
+  *  camera_high_motor:相机高度;步长1;最小0;最大400
+  *  camera_steering:相机倾角;步长0.1,最小-40;最大40
+  *  camera_steering:相机倾角
+  * turntable_steering:转盘角度;步长1;最小-720;最大720
+  * turntable_position_motor:转盘位置;步长1,最小0;最大800
+  * overturn_steering:翻转,当device_name为当前类型时,value可以忽略或传递任意值,后端不做任何处理
+  * take_picture:拍照测试;当device_name为当前类型时,value为0或者大于0的数值;提交对焦次数参数
+  * laser_position:激光开关;当device_name为当前类型时,value为0或者1;0为关   1为开
+* value为设备得控制值,参考上述的最大最小值进行处理
+#### 请求示例
+```python
+{
+    "data": {
+        "device_name": "camera_high_motor",
+        "value": 200
+    },
+    "type": "control_mcu"
+}
+```
+
+
+### 执行拍摄任务
+* type 为run_mcu时,为设备拍照动作连贯执行
+* data中的 action参数释义:
+    * 执行左脚程序 : 为用户配置【执行左脚程序】,其状态必须至少有一条为true,否则不会运行并且报错无可用参数
+    * 执行右脚程序 : 为用户配置【执行右脚程序】,其状态必须至少有一条为true,否则不会运行并且报错无可用参数
+* data中的 goods_art_no参数释义:
+    * 商品货号,必须存在且为字符串形式传递
+#### 请求示例
+```python
+{
+    "data": {
+        "action": "执行左脚程序",
+        "goods_art_no": "1234556"
+    },
+    "type": "run_mcu"
+}
+```
+### 执行拍摄任务命令发送完成后-服务端响应消息
+* msg_type 为image_process时,代表上述命令已发送给设备执行任务
+* data参数释义:
+    * image_counts : 执行拍照任务的数量,可理解为照片张数
+    * goods_art_no : 货号
+    * current_time : 当前时间
+* msg 消息提示
+#### 响应示例
+```python
+{
+    "msg":"MCU 命令已发送完成",
+    "data": {
+        "image_counts": 4,
+        "goods_art_no": "1234556",
+        "current_time": "年月日时分秒"
+    },
+    "msg_type":"image_process"
+}
+```
+### 单张照片拍照完成后-服务端响应消息
+* msg_type:
+    * 为photo_take时,代表拍照已完成
+    * 为photo_take_finish时,代表所有拍照任务已完成
+* data参数释义:
+    * image_counts : 执行拍照任务的数量,可理解为照片张数
+    * goods_art_no : 货号
+    * current_time : 当前时间
+* msg 消息提示
+#### 响应示例-单个任务拍照成功(前端需要刷新列表)
+```python
+{
+    "code": 0,
+    "msg": "拍鞋底 执行完成~",
+    "status": 2,
+    "data": null,
+    "msg_type":"photo_take"
+}
+```
+#### 响应示例-单个任务拍照失败
+```python
+{
+    "code": 1,
+    "msg": "拍鞋底 执行失败~",
+    "status": 2,
+    "data": null,
+    "msg_type":"photo_take"
+}
+```
+### 响应示例-整体任务拍照完成
+```python
+{
+    "code": 1,
+    "msg": "执行左脚程序 执行成功~",
+    "status": 2,
+    "data": null,
+    "msg_type":"photo_take_finish"
+}
+```
+### 接收遥控器扫码货号动作处理
+#### 消息示例
+* data中的子data参数释义:
+    * action:【执行左脚程序】或【执行右脚程序】
+    * goods_art_no :扫描到的货号,当货号为空时代表用户未扫码,直接按遥控器的左右操作,需要前端判断用户是否在前端页面填写了货号,或者提醒用户填写货号
+* msg_type:当该字段为blue_tooth_scan时,前端可直接解析得到data数据包,将数据直接发送给socket服务端即可【执行拍摄任务】
+```python
+{
+    "code": 0,
+    "msg": "准备执行[左脚程序]",
+    "status": 2,
+    "data": {
+            "data": {
+                    "action": "执行左脚程序",
+                    "goods_art_no": "货号信息",
+                    },
+                    "type": "run_mcu",
+    },
+    "msg_type": "blue_tooth_scan"
+}
+```
+### 接收遥控器点击拍照事件处理
+<mark>遥控器得拍照命令需要由后端转发到前端,再由前端将消息转发给后端对应得拍照命令<mark>
+当蓝牙遥控器点击拍照后,后端消息响应如下:
+* data中参数释义:
+    * type:handler_take_picture 为后端单拍任务得类型
+    * data :忽略
+* msg_type:当该字段为handler_take_picture时,前端可直接解析得到data数据包,将数据直接发送给socket服务端即可【执行单任务】
+#### 消息示例
+```python
+{
+    "code": 0,
+    "msg": "处理单拍消息",
+    "status": -1,
+    "data": {
+        "type": "handler_take_picture",
+        "data": null
+    },
+    "msg_type": "handler_take_picture"
+}
+```
+<mark>注:在多次按下拍照按钮时,前端需要在命令发送给后端时且拍照未完成之前处理拦截操作,否则会出现拍照任务在不断执行<mark>
+
+### 单拍任务命令
+#### 发送-消息示例
+```python
+{
+        "type": "handler_take_picture",
+        "data": null
+}
+```
+#### 响应-消息示例
+<mark>注意:当前响应可能会出现失败得情况,如:用户清空了所有货号数据;用户未打开拍照软件等其他错误消息响应;前端需要判断code是否为0;去释放拦截<mark>
+* data中参数释义:
+    * msg: 消息文本内容
+    * data :
+        * goods_art_no:货号,便于前端查询详情数据
+* msg_type:当该字段为photo_take时,代表照片已拍摄完成
+```python
+{
+    "code": 0,
+    "msg": "执行左脚程序 执行完成~",
+    "status": 2,
+    "data": {"goods_art_no": "执行货号"},
+    "msg_type": "photo_take"
+}
+```
+##### 未完待续.....

+ 52 - 0
python/index.py

@@ -0,0 +1,52 @@
+import asyncio
+import sys
+import uvicorn
+import signal
+from api import *
+from sockets.socket_server import *
+import uvicorn.loops.auto
+import uvicorn.protocols.http.auto
+import uvicorn.protocols.websockets.auto
+import uvicorn.lifespan.on
+from multiprocessing import Process, freeze_support
+from service.init_load_source import init_load_source
+
+
+def handle_shutdown(signum, frame):
+    """关闭系统应用服务"""
+     # 终止事件循环
+    loop = asyncio.get_event_loop()
+    loop.call_soon_threadsafe(loop.stop)
+    sys.exit(0)
+
+async def run_server():
+    # 启动uvicorn服务器
+    isDebug = True if IS_DEBUG == "true" else False
+    config = uvicorn.Config(
+        APP_RUN,
+        host=APP_HOST,
+        port=int(PORT),
+        reload=isDebug,
+        workers=int(APP_WORKS),
+    )
+    server = uvicorn.Server(config)
+    await server.serve()
+
+async def main():
+    await  asyncio.gather(run_server(), init_load_source().load_source())
+
+
+if __name__ == "__main__":
+    signal.signal(signal.SIGINT, handle_shutdown)
+    signal.signal(signal.SIGTERM, handle_shutdown)
+    print("python server is running at port:", PORT)
+    print("python server is running at port:", APP_RUN)
+    # asyncio.run(main())
+    isDebug = True if IS_DEBUG == "true" else False
+    uvicorn.run(
+        app=APP_RUN,
+        host=APP_HOST,
+        port=int(PORT),
+        reload=isDebug,
+        loop="auto",
+    )

+ 26 - 0
python/logger.py

@@ -0,0 +1,26 @@
+import logging.handlers
+import logging
+import sys
+# 导入设置
+from settings import LOG_FILE_NAME, MAX_BYTES, BACKUP_COUNTS
+
+# 日志相关配置
+logger = logging.getLogger()
+logger.setLevel(logging.INFO)
+ch = logging.StreamHandler()
+# ch.setLevel(logging.DEBUG) 
+fh = logging.handlers.RotatingFileHandler(
+    str(LOG_FILE_NAME),
+    mode="a",
+    encoding="utf-8",
+    maxBytes=int(MAX_BYTES),
+    backupCount=int(BACKUP_COUNTS),
+)
+formatter = logging.Formatter(
+    "%(asctime)s - %(module)s - %(funcName)s - line:%(lineno)d - %(levelname)s - %(message)s"
+)
+ch.setFormatter(formatter)
+fh.setFormatter(formatter)
+logger.addHandler(ch)  # 将日志输出至屏幕
+logger.addHandler(fh)  # 将日志输出至文件
+logger = logging.getLogger(__name__)

+ 97 - 0
python/mcu/BaseClass.py

@@ -0,0 +1,97 @@
+import asyncio
+from sockets import ConnectionManager
+from utils.common import message_queue
+
+class BaseClass:
+
+    def __init__(self, websocket_manager: ConnectionManager):
+        self.websocket_manager = websocket_manager
+        self.msg_type = ""
+        # -1连接失败  0未连接 1连接中  2连接成功  3端口占用
+        # self.device_status = 2
+
+    def sendSocketMessage(self, code=0, msg="", data=None,device_status=2):
+        data = {
+            "code": code,
+            "msg": msg,
+            "status": device_status,
+            "data": data,
+            "msg_type": self.msg_type,
+        }
+        loop = asyncio.get_event_loop()
+        loop.create_task(message_queue.put(data))
+    def change_hex_to_int(self,_bytearray):
+        return ' '.join([hex(x) for x in _bytearray])
+
+    def read_cmd(self, serial_handle, check=None):
+        n = 0
+        while 1:
+            try:
+                read_d = serial_handle.read_all()  # 读取接收到的数据
+                self.receive_data += read_d
+            except BaseException as e:
+                print("171串口接收报错", e)
+                self.serial_handle = None
+                return False
+
+            if len(self.receive_data) < 4:
+                break
+
+            if self.receive_data[0] == 0x55 and self.receive_data[1] == 0x55:
+                # print("read ori ", self.change_hex_to_int(self.receive_data))
+                data_len = self.receive_data[2]
+                if len(self.receive_data) < data_len + 4:
+                    # 此处需要超时机制
+                    # print("数据长度不够,等待下次读取")
+                    # 超时退出
+                    # if not self.serial_handle.txdone():
+                    #     return None
+                    # n += 1
+                    # if n > out_time_n:
+                    #     return None
+                    # time.sleep(0.01)
+                    continue
+                _data = self.receive_data[3 : data_len + 4]
+                # 更新缓存区
+                self.receive_data = self.receive_data[data_len + 4 :]
+                # 校验数据
+                if 0xFF & ~sum(_data[:-1]) == _data[-1]:
+                    # print("receive_data:", self.change_hex_to_int(self.receive_data[:-1]))
+                    return _data[:-1]
+                else:
+                    return None
+            else:
+                # print("起始位不是 55 55 进行移除", self.receive_data[0])
+                # 起始位不是 55 55 进行移除
+                while self.receive_data:
+                    if len(self.receive_data) == 1:
+                        if self.receive_data[0] == 0x55:
+                            break
+                        else:
+                            self.receive_data = b""
+                    else:
+                        if (
+                            self.receive_data[0] == 0x55
+                            and self.receive_data[1] == 0x55
+                        ):
+                            break
+                        else:
+                            self.receive_data = self.receive_data[1:]
+
+    def write_cmd(self, serial_handle,data: list):
+        if serial_handle:
+            # data = [(0xff & par1), (0xff & (par1 >> 8))]
+            # self.clearn_flush()
+            buf = bytearray(b"")
+            buf.extend([0x55, 0x55, (0xFF & len(data))])
+            buf.extend(data)
+            buf.extend([0xFF & ~sum(data)])
+            # 55 55 02 5a 01 a4
+            # print("send buf  {}".format(self.change_hex_to_int(buf)))
+            try:
+                serial_handle.write(buf)
+                return True
+            except:
+                serial_handle = None
+                _recv_data = b""
+                return False

+ 451 - 0
python/mcu/BlueToothMode.py

@@ -0,0 +1,451 @@
+import asyncio
+import time
+from bleak import BleakScanner, BleakClient
+import threading
+from collections import deque
+
+from networkx import is_connected
+from utils.SingletonType import SingletonType
+from .RemoteControlV2 import RemoteControlV2
+from .BaseClass import BaseClass
+from sockets.connect_manager import ConnectionManager
+
+class BlueToothMode(BaseClass,metaclass=SingletonType):
+    instance = None
+    init_flag = None
+
+    def __init__(self, websocket_manager: ConnectionManager):
+        super().__init__(websocket_manager)
+        """此处设计为,如果已经存在实例时,不再执行初始化"""
+        if self.init_flag:
+            return
+        else:
+            self.init_flag = True
+        self.msg_type = "blue_tooth"
+        self.remote_control_v2 = RemoteControlV2(self, websocket_manager)
+        # 用于存储找到的目标设备的地址
+        self.target_device_address = None
+
+        self._lock = threading.Lock()
+        self.connect_state = False
+        self.receive_data = b""
+        self.devices = {}
+        self.devices_name = {}
+        self.last_value = None
+        self.retry_num = 0
+        self.last_error = ""
+
+    def __new__(cls, *args, **kwargs):
+        """如果当前没有实例时,调用父类__new__方法,生成示例,有则返回保存的内存地址。"""
+        if not cls.instance:
+            cls.instance = super().__new__(cls)
+        return cls.instance
+
+    def print_error(self, text, *args):
+        if text != self.last_error:
+            self.last_error = text
+            print(text)
+
+    async def scan_for_esp32(self):
+        """扫描附近的BLE设备,并寻找ESP32"""
+        # print("打印连接状态", self.connect_state)
+        if self.connect_state == True:
+            return
+        # self.sendSocketMessage(
+        #     code=0, msg="遥控设备V2 未连接", data=None, device_status=-1
+        # )
+        # print("Scanning for ESP32 devices...*****************")
+        # print("打印连接状态2", self.connect_state)
+        try:
+            devices = await BleakScanner.discover()
+        except BaseException as e:
+            self.print_error("蓝牙疑似未打开,{}".format(e))
+            self.sendSocketMessage(
+                code=0, msg="蓝牙疑似未打开,{}".format(e), data=None, device_status=-1
+            )
+            return
+        for d in devices:
+            # print(f"Device: {d.name} - Address: {d.address}")
+            # 假设ESP32的广播名称包含"ESP32-S3"字符串
+            if d.name:
+                if "ESP32-S3" in d.name:
+                    if d.address not in self.devices:
+                        self.devices_name[d.name] = d.address
+                        self.devices[d.address] = {
+                            "name": d.name,
+                            "client": None,
+                            "send_queue": deque(maxlen=20),
+                            "recv_queue": deque(maxlen=20),
+                            "receive_data": b"",
+                            "connect_state": True,
+                        }
+                        self.print_error(
+                            f"Found device: {d.name} - Address: {d.address}"
+                        )
+                        asyncio.create_task(self.connect_and_listen(d.address))
+                        self.connect_state = True
+
+    # 注册到遥控器等上位机
+    def connect_device(self, address, name):
+        print("注册到遥控器等上位机", address, name)
+        if "remote" in name:
+            self.remote_control_v2.to_connect_bluetooth(address, is_test=False)
+
+    # 关闭连接
+    def disconnect_device(self, address, name):
+        print("关闭蓝牙连接", address, name)
+        if "remote" in name:
+            self.print_error("71  关闭蓝牙连接{}-{}".format(address, name))
+            self.remote_control_v2.close_bluetooth_connect()
+            self.connect_state = False
+            self.receive_data = b""
+            self.devices = {}
+            self.devices_name = {}
+            self.last_value = None
+            self.retry_num = 0
+            self.last_error = ""
+
+    async def handle_disconnect(self, client):
+        """处理断开连接事件"""
+        self.print_error("Device was disconnected.")
+        # 尝试重新连接
+        while not client.is_connected:
+            try:
+                self.print_error("Attempting to reconnect...")
+                await client.connect()
+            except Exception as e:
+                self.print_error(f"Failed to reconnect: {e}")
+                await asyncio.sleep(5)  # 等待一段时间再重试
+
+    async def notification_handler(self, address, characteristic, data: bytearray):
+        """处理接收到的通知数据"""
+        with self._lock:
+            #
+            # if data == bytearray(b'UU\x01c\x9c'):
+            #     self.devices[address]['send_queue'].append(bytearray(b'UU\x01c\x9c'))
+            #     pass
+            # else:
+            #     # print("60 data:", address, data)
+            self.devices[address]["recv_queue"].append(data)
+            self.devices[address]["connect_state"] = True
+
+    async def write_characteristic(self, client, characteristic_uuid, data):
+        """向指定特征写入数据"""
+        try:
+            await client.write_gatt_char(characteristic_uuid, data)
+        except BaseException as e:
+            self.print_error("write_characteristic error ", e)
+
+    async def connect_and_listen(self, address):
+        """连接到指定地址的ESP32设备并监听通知"""
+        self.print_error("""连接到指定地址的ESP32设备并监听通知""")
+        while True:
+            # try:
+            if len(self.devices) == 0:
+                break
+            async with BleakClient(address) as client:
+                if not client.is_connected:
+                    self.print_error("Failed to connect to the device.")
+                    self.devices[address]["connect_state"] = False
+                    self.disconnect_device(
+                            address=address, name=self.devices[address]["name"]
+                        )
+                    continue
+                if len(self.devices) == 0:
+                    break
+                self.devices[address]["connect_state"] = True
+                self.print_error(f"Connected to {address}")
+                # 获取服务和特征(假设你已经知道要监听的特征UUID)
+                services = await client.get_services()
+                for service in services:
+                    for char in service.characteristics:
+                        if "notify" in char.properties:
+                            self.print_error(
+                                    f"Subscribing to characteristic: {char.uuid}"
+                                )
+                            await client.start_notify(
+                                    char,
+                                    lambda char, data: asyncio.create_task(
+                                        self.notification_handler(address, char, data)
+                                    ),
+                                )
+
+                # 进入一个简单的循环,保持连接
+                self.print_error(
+                        "进入一个简单的循环  保持连接", self.devices[address]["name"]
+                    )
+                self.connect_device(
+                        address=address, name=self.devices[address]["name"]
+                    )
+                self.retry_num += 1
+                while True:
+                    if not client.is_connected:
+                        self.print_error(
+                                f"Device {address} disconnected unexpectedly."
+                            )
+                        with self._lock:
+                            self.disconnect_device(
+                                    address=address, name=self.devices[address]["name"]
+                                )
+                            if len(self.devices) == 0:
+                                break
+                            self.devices[address]["connect_state"] = False
+                        break
+                    if len(self.devices) == 0:
+                        break
+                    if self.devices[address]["send_queue"]:
+                        with self._lock:
+                            send_data = self.devices[address][
+                                    "send_queue"
+                                ].popleft()
+                            # print("-----------> send_data:", self.change_hex_to_10_int(send_data))
+                        await self.write_characteristic(
+                                client, char.uuid, send_data
+                            )
+                        await asyncio.sleep(0.01)
+
+                    await asyncio.sleep(0.02)
+        # except Exception as e:
+        #     with self._lock:
+        #         self.disconnect_device(
+        #             address=address, name=self.devices[address]["name"]
+        #         )
+        #         self.devices[address]["connect_state"] = False
+        #     print(f"Error during connection or listening: {e}")
+        #     await asyncio.sleep(2)  # 发生错误时等待一段时间再重试
+
+    async def main_func(self):
+        """主函数"""
+        # address = "24:EC:4A:26:4B:BE"
+        # _name = "ESP32-S3-remote"
+        # self.devices[address] = {'name': _name,
+        #                          'client': None,
+        #                          'send_queue': [],
+        #                          'recv_queue': [],
+        #                          "receive_data": b"",
+        #                          "connect_state": True,
+        #                          }
+        # self.devices_name[_name] = address
+        # asyncio.create_task(self.connect_and_listen(address))
+        if self.connect_state == True:
+            print('蓝牙已连接,不可重新连接')
+            message = {
+                "_type": "show_info",
+                "plugins_mode": "remote_control",
+                "data": "遥控设备V2 打开蓝牙成功",
+            }
+            self.sendSocketMessage(
+                code=0, msg="遥控设备V2 打开蓝牙成功", data=message, device_status=2
+            )
+            return
+        await self.scan_for_esp32()
+        # 定期重新扫描以发现新设备
+        while True:
+            if self.devices:
+                await asyncio.sleep(20)
+            else:
+                await asyncio.sleep(3)
+            await self.scan_for_esp32()
+    def run(self):
+        self.print_error("开启蓝牙扫描")
+        asyncio.run(self.main_func())
+
+    def write_cmd(self, address, data: list):
+        buf = []
+        buf.extend([0x55, 0x55, (0xFF & len(data))])
+        buf.extend(data)
+        buf.extend([0xFF & ~sum(data)])
+        self.send_data(address, bytes(buf))
+
+    def send_data(self, address, byte_list: bytes):
+        chunk_size = 20
+        chunks = [
+            byte_list[i : i + chunk_size] for i in range(0, len(byte_list), chunk_size)
+        ]
+        try:
+            with self._lock:
+                for chunk in chunks:
+                    self.devices[address]["send_queue"].append(chunk)
+        except Exception as e:
+            self.devices[address]["connect_state"] = False
+            self.print_error(f"Error sending notification: {address},{e}")
+
+    def change_hex_to_int(self, _bytearray):
+        return " ".join([hex(x)[2:].zfill(2) for x in _bytearray])
+
+    def change_hex_to_10_int(self, _bytearray):
+        return " ".join([str(int(x)) for x in _bytearray])
+
+    def read_cmd(self, time_out=10):
+        data_list = []
+        for device_name in self.devices_name:
+            address = self.devices_name[device_name]
+            # print(self.devices[address]['connect_state'])
+            while self.devices[address]["recv_queue"]:
+                receive_data = self.read_cmd_one(address)
+                if receive_data:
+                    data_list.append(
+                        {
+                            "device_name": device_name,
+                            "address": address,
+                            "receive_data": receive_data,
+                        }
+                    )
+        return data_list
+
+    def read_cmd_one(self, address):
+        # 获取所有缓冲区的字节
+        if not self.devices[address]["connect_state"]:
+            return
+        # print("读取数据....")
+        while 1:
+            receive_data = self.devices[address]["receive_data"]
+            # print("166 receive_data", receive_data)
+            if self.devices[address]["recv_queue"]:
+                with self._lock:
+                    read_d = self.devices[address]["recv_queue"].popleft()
+                    # print("170 read_d", read_d)
+                    receive_data += read_d
+                    self.devices[address]["receive_data"] = receive_data
+
+            if not receive_data:
+                return None
+
+            if len(receive_data) < 4:
+                break
+            # print("--------receive_data",self.change_hex_to_10_int(receive_data))
+            if receive_data[0] == 0x55 and receive_data[1] == 0x55:
+                data_len = receive_data[2]
+                if len(receive_data) < data_len + 4:
+                    # n += 1
+                    # if n > time_out:
+                    #     time.sleep(0.001)
+                    #     return False
+                    # continue
+                    return
+
+                _data = receive_data[3 : data_len + 4]
+                # 更新缓存区
+                with self._lock:
+                    self.devices[address]["receive_data"] = receive_data[data_len + 4 :]
+                    receive_data = receive_data[data_len + 4 :]
+                # 校验数据
+                if 0xFF & ~sum(_data[:-1]) == _data[-1]:
+                    return _data[:-1]
+                else:
+                    print("数据异常,丢弃")
+                    return False
+            else:
+                # 起始位不是 55 55 进行移除
+                while receive_data:
+                    if len(receive_data) == 1:
+                        if receive_data[0] == 0x55:
+                            break
+                        else:
+                            with self._lock:
+                                self.devices[address]["receive_data"] = b""
+                                receive_data = b""
+                    else:
+                        if receive_data[0] == 0x55 and receive_data[1] == 0x55:
+                            break
+                        else:
+                            with self._lock:
+                                self.devices[address]["receive_data"] = receive_data[1:]
+                            receive_data = receive_data[1:]
+
+    def analysis_received_data(self):
+        receive_data = self.read_cmd(time_out=5)
+        if not receive_data:
+            return
+        else:
+            # print("receive_data:", receive_data)
+            # print("read2 receive_data {}".format(self.change_hex_to_int(receive_data)))
+            pass
+        for data in receive_data:
+            address = data["address"]
+            rec_data = data["receive_data"]
+            # print("read address {}".format(address))
+            # print("read receive_data {}".format(self.change_hex_to_int(rec_data)))
+            # 数据 结构 command,按命令解析
+
+            if rec_data[0] == 1:
+                # 扫码数据
+                bar_code = rec_data[1:].decode()
+                bar_code = bar_code.replace("\r", "")
+                bar_code = bar_code.replace("\n", "")
+                print("bar_code:", bar_code)
+                return
+            if rec_data[0] == 2:
+                print("read receive_data {}".format(self.change_hex_to_int(rec_data)))
+                return
+            if rec_data[0] == 90:
+                print(
+                    "read receive_data-90 {}".format(self.change_hex_to_int(rec_data))
+                )
+                return
+
+            if rec_data[0] == 99:
+                blue_mode.write_cmd(address, [99])
+                # print("发送心跳包")
+                return
+            if rec_data[0] == 111:
+                print(
+                    "{} receive_data-111 {}".format(
+                        self.retry_num, self.change_hex_to_10_int(rec_data)
+                    )
+                )
+                value = (
+                    rec_data[1] << 24
+                    | rec_data[2] << 16
+                    | rec_data[3] << 8
+                    | rec_data[4]
+                )
+                if self.last_value is None:
+                    self.last_value = value
+                else:
+                    self.last_value += 1
+                if self.last_value == value:
+                    flag = True
+                else:
+                    flag = False
+                print(
+                    "{}  value:{},last_value;{},flag:{}\n".format(
+                        self.retry_num, value, self.last_value, flag
+                    )
+                )
+
+
+if __name__ == "__main__":
+    blue_mode = BlueToothMode(None)
+    threading.Thread(target=blue_mode.run, args=()).start()
+    print("=" * 50)
+    n = 0
+    time.sleep(1)
+    k = 0
+    last_t = time.time()
+    while 1:
+        time.sleep(0.001)
+        s = time.time()
+        address = "24:EC:4A:26:4B:BE"
+        blue_mode.analysis_received_data()
+
+        n += 1
+        if n == 500:
+            print(s - last_t)
+            for i in range(3):
+                k += 1
+                data = [
+                    111,
+                    0xFF & k >> 24,
+                    0xFF & k >> 16,
+                    0xFF & k >> 8,
+                    0xFF & k,
+                ]
+                data.extend([x for x in range(100, 130)])
+                blue_mode.write_cmd(address=address, data=data)
+                print("send_value:{}".format(k))
+
+            data = [90]
+            blue_mode.write_cmd(address=address, data=data)
+            n = 0
+        last_t = s

+ 1273 - 0
python/mcu/DeviceControl.py

@@ -0,0 +1,1273 @@
+import asyncio
+import datetime
+import serial.tools.list_ports
+import time, json
+from .SerialIns import SerialIns
+from utils.SingletonType import SingletonType
+from .BaseClass import BaseClass
+from sockets import ConnectionManager
+from collections import defaultdict
+import threading
+import settings
+from .ProgramItem import ProgramItem
+from .capture.module_digicam import DigiCam
+from databases import insert_photo_records
+
+# mcu命令
+class DeviceControl(BaseClass, metaclass=SingletonType):
+    lock = threading.Lock()
+
+    def __init__(self, websocket_manager: ConnectionManager):
+        super().__init__(websocket_manager)
+        self.msg_type = "mcu"
+        self.m_t = 1
+        # 0未开始  1进行中 2已结束  99异常
+        self.action_state = 2
+        self._mcu_move_state = 0
+        self.state_camera_motor = 3
+        self.state_camera_steering = 3
+        self.state_turntable_steering = 3
+        self.state_overturn_steering = 3
+        self.state_move_turntable_steering = 3
+
+        self.last_from_mcu_move_respond_data = None
+        self.camera_motor_speed = 0
+        self.camera_motor_value = 0
+        self.init_state = False
+        self.port_name = ""
+        self.t_n = 0
+        self.serial_ins = None
+        self.connected_ports_dict = {}  # 已连接的ports
+        self.p_list = []
+        self.temp_ports_dict = {}
+        self.is_running = False
+        self.connect_state = False
+        self.device_name_dict = {
+            "camera_steering": 0,
+            "camera_high_motor": 1,
+            "turntable_steering": 2,
+            "overturn_steering": 3,
+            "laser_position": 4,
+            "buzzer": 5,
+            "split": 6,
+            "turntable_position_motor": 7,
+            "mp3_player": 8,
+            "mcu": 99,
+        }
+
+        # 最近的mcu基础信息,用于获取数据状态检查
+        self.last_mcu_info_data = {
+            "num": 0,
+            "time": time.time(),
+            "data": None,
+        }
+        # 最近的mcu的其他配置
+        self.last_mcu_other_info_data = {
+            "num": 0,
+            "time": time.time(),
+            "data": {},
+        }
+
+        self.command = {
+            "to_device_move": 1,  # 设备运动
+            "to_init_device": 2,  # 初始化设备
+            "to_deal_other_device": 3,  # 处理其他设备
+            "get_all_info": 29,  # 获取所有信息
+            "set_deviation": 40,  # 设置偏移量
+            "get_deviation": 41,  # 读取偏移量
+            "signal_forwarding": 91,  # 信号转发处理
+            "signal_forwarding_return": 92,  # 信号转发返回
+            "get_other_info": 44,  # 获取其他信息
+            "open_rgb_led": 43,  ## RGB灯的处理与通讯
+            "set_other_info": 45,  # 设置其他信息
+            "query_remote_control_battery": 47,  # 查询遥控器电量
+            "set_turntable_mode": 48,  # 设置转盘通讯方式 1、串口、2、无线、3 混合
+            "stop_mcu": 93,  # 停止运行mcu
+        }
+        # self.window = window
+        self.last_push_time = defaultdict(float)
+        self.is_running = False
+        self.is_wait_connect = False  # 等待链接
+        self.send_data_queue = []  # 发送队列
+        # self.lock = Lock()
+        # 是否是刚进行完初始化;首次初始化,需要运动到指定第一个指定位置
+        self.is_just_init_time = False
+        # self.init()
+
+        # ===========注册命令函数============
+        self.deal_code_func_dict = {
+            29: self.get_from_mcu_base_info,  # 获取基本情况
+            32: self.get_from_mcu_button,  # 获取按键信息
+            42: self.get_from_mcu_deviation_info,  # 获取偏移量信息
+            44: self.get_from_mcu_other_info,  # 获取其他配置参数
+            90: self.get_from_mcu_connect_info,  # 获取链接电脑信号
+            92: self.get_from_mcu_move_respond_data,  # 获取MCU响应
+            100: self.print_mcu_error_data,  # 打印下位机的错误内容
+        }
+
+    async def initDevice(self):
+        if not self.is_running:
+            self.sendSocketMessage(
+                code=1, msg="mcu设备未连接,请先连接设备", device_status=0
+            )
+            return False
+        self.serial_ins.clearn_flush()
+        self.to_init_device_origin_point(device_name="mcu")
+        print("MCU 开始循环~")
+        while 1:
+            await asyncio.sleep(0.01)
+            if not self.serial_ins or not self.connect_state:
+                break
+            try:
+                # print("mcu   send_cmd")
+                self.send_cmd()
+                # time.sleep(0.01)
+                self.get_basic_info_mcu()
+            except BaseException as e:
+                print("121231298908", e)
+                break
+
+        self.is_running = False
+        self.connect_state = False
+        print("MCU 循环退出~")
+        # self.sign_data.emit(
+        #     {"_type": "show_info", "plugins_mode": "mcu", "data": "MCU 连接失败"}
+        # )
+        message = {"_type": "show_info", "plugins_mode": "mcu", "data": "MCU 连接失败"}
+        self.sendSocketMessage(code=1, msg="MCU 连接失败", data=message,device_status=-1)
+        self.close_connect()
+
+    def stop_mcu(self):
+        buf = [self.command["stop_mcu"]]
+        buf.extend(self.encapsulation_data(data=1, len_data=1))
+        self.add_send_data_queue(buf)
+
+    # 设置转盘通讯方式 1、串口、2、无线、3 混合
+    def to_set_turntable_mode(self, mode=1):
+        buf = [self.command["set_turntable_mode"]]
+        buf.extend(self.encapsulation_data(data=mode, len_data=1))
+        self.add_send_data_queue(buf)
+
+    def query_remote_control_battery(self):
+        """查询遥控器电量"""
+        buf = [self.command["query_remote_control_battery"]]
+        buf.extend(self.encapsulation_data(data=1, len_data=1))
+        buf.extend(self.encapsulation_data(data=1, len_data=1))
+        buf.extend(self.encapsulation_data(data=1, len_data=1))
+        self.add_send_data_queue(buf)
+
+    def encapsulation_data(self, data, len_data, data_magnification=1):
+        # data_magnification 数据放大倍数,或缩小倍数,默认为1
+        data = int(data * data_magnification)
+        if len_data == 1:
+            return [0xFF & data]
+        elif len_data == 2:
+            return [0xFF & data >> 8, 0xFF & data]
+        elif len_data == 4:
+            return [0xFF & data >> 24, 0xFF & data >> 16, 0xFF & data >> 8, 0xFF & data]
+
+    def open_rgb_led(
+        self,
+        color_name,
+        led_command=1,
+        brightness=80,
+        enable=True,
+        mode="loop",
+        times=2,
+        interval=0.1,
+    ):
+        color_name_value = {
+            "红色": (156, 6, 3),
+            "黄色": (255, 206, 25),
+            "绿色": (0, 128, 0),
+            "蓝色": (0, 25, 255),
+            "白色": (255, 250, 227),
+        }
+        if color_name in color_name_value:
+            buf = [self.command["open_rgb_led"]]
+            buf.append(1 if enable else 0)
+            buf.append(led_command)
+            buf.extend(color_name_value[color_name])
+            buf.extend(
+                [
+                    brightness,
+                    1 if mode == "loop" else 2,
+                    times,
+                    int(interval * 10),
+                ]
+            )
+            self.add_send_data_queue(buf)
+
+    def get_deviation(self):
+        # 发送获取偏移量
+        data = [self.command["get_deviation"], 1]
+        self.add_send_data_queue(data)
+        # if self.serial_ins:
+        #     self.serial_ins.write_cmd(data)
+        print("发送获取偏移量")
+
+    def get_other_info(self):
+        # 发送获取偏移量
+        data = [self.command["get_other_info"], 1]
+        self.add_send_data_queue(data)
+        print("发送获取其他信息")
+
+    def add_send_data_queue(self, data):
+        self.lock.acquire()
+        if self.serial_ins:
+            # print("send_data_queue  append  :{}".format(data))
+            self.send_data_queue.append(data)
+        self.lock.release()
+    def send_all_cmd(self):
+        while True:
+            if self.send_data_queue:
+                self.sendSocketMessage(msg="正在发送命令", device_status=1)
+                data = self.send_data_queue.pop(0)
+                self.serial_ins.write_cmd(data)
+                self.sendSocketMessage(msg="命令发送完成", device_status=2)
+            else:
+                break
+    def send_cmd(self):
+        self.lock.acquire()
+        if self.send_data_queue:
+            self.sendSocketMessage(msg="正在发送命令", device_status=1)
+            data = self.send_data_queue.pop(0)
+            self.serial_ins.write_cmd(data)
+            self.sendSocketMessage(msg="命令发送完成", device_status=2)
+        # else:
+        #     self.t_n += 1
+        #     # 加大发送获取基础数据的时间间隔
+        #     # 默认为0.01秒一个循环,每隔1.5秒发送数据
+        #     if self.t_n > 150:
+        #         self.t_n = 0
+        #         data = [self.command["get_all_info"], 1]
+        #         self.serial_ins.write_cmd(data)
+        self.lock.release()
+
+    def print_mcu_error_data(self, receive_data):
+        # 扫码数据
+        try:
+            data = receive_data[1:].decode()
+            if "设备初始化完成" in data:
+                self.init_state = True
+                self.sendSocketMessage(msg=data, device_status=2)
+            print("115  print_mcu_error_data:", data)
+        except BaseException as e:
+            print("117 error {}".format(e))
+        return
+
+    def get_from_mcu_move_respond_data(self, receive_data):
+        self.last_from_mcu_move_respond_data = receive_data
+
+    def get_from_mcu_connect_info(self, receive_data):
+        connect_flag = receive_data[1]
+        device_id = receive_data[2]
+        try:
+            mcu_has_been_set = receive_data[6]  # 设备是否有初始化 ,1 表示已初始化
+        except:
+            mcu_has_been_set = 99  # 未知状态
+
+        # self.self_sign.emit({"type": "connect_sign", "data": connect_flag})
+        message = {"type": "connect_sign", "data": connect_flag}
+        self.sendSocketMessage(msg="接收链接信息", data=message)
+        print("接收链接信息")
+        return
+
+    def to_init_device_origin_point(self, device_name, is_force=False):
+        device_id = self.device_name_dict[device_name]
+        cmd = 2
+        data = [cmd, device_id, 0 if is_force is False else 1]
+        self.open_rgb_led(color_name="红色")
+
+        self.add_send_data_queue(data)
+
+        if device_name == "mcu":
+            # 重置初始化标记为  从未初始化
+            self.is_just_init_time = False
+
+        return True
+    def cleanAllReceiveData(self):
+        while True:
+            receive_data = self.serial_ins.read_cmd(out_time=1)
+            if not receive_data:
+                break
+    def get_basic_info_mcu(self):
+        receive_data = self.serial_ins.read_cmd(out_time=1)
+        if receive_data is False:
+            print("------------------------------------------------4657564654")
+            self.connect_state = False
+            return False
+        if not receive_data:
+            return
+        # print("receive_data")
+        # 数据 结构 command,按命令解析
+        # command 0(9) 相机高度1-2  相机角度3-4  转盘角度5-6 灯光状态7  激光指示器状态8,运行状态9
+        command = receive_data[0]
+        if command in self.deal_code_func_dict:
+            self.deal_code_func_dict[command](receive_data)
+
+    def get_from_mcu_button(self, receive_data):
+        button_name = receive_data[1]
+        self.deal_mcu_button(button_name)
+
+    def deal_mcu_button(self, button_name):
+        # 防止重复点击
+        s = time.time()
+        if s - self.last_push_time[button_name] > 0.1:
+            self.last_push_time[button_name] = s
+            # print("button_name", button_name)
+        else:
+            self.last_push_time[button_name] = s
+            return
+        if button_name == 1:
+            # 自动执行全部
+            # self.sign_data.emit(
+            #     {"_type": "mcu_button", "plugins_mode": "mcu", "data": "todo_1"}
+            # )
+            message = {"_type": "mcu_button", "plugins_mode": "mcu", "data": "todo_1"}
+            self.sendSocketMessage(code=0, msg="", data=message)
+        if button_name == 2:
+            # self.sign_data.emit(
+            #     {"_type": "mcu_button", "plugins_mode": "mcu", "data": "todo_2"}
+            # )
+            message = {"_type": "mcu_button", "plugins_mode": "mcu", "data": "todo_2"}
+            self.sendSocketMessage(code=0, msg="", data=message)
+        if button_name == 3:
+            # self.sign_data.emit(
+            #     {"_type": "mcu_button", "plugins_mode": "mcu", "data": "todo_3"}
+            # )
+            message = {"_type": "mcu_button", "plugins_mode": "mcu", "data": "todo_3"}
+            self.sendSocketMessage(code=0, msg="", data=message)
+
+    # 获取偏移量信息
+    def get_from_mcu_deviation_info(self, receive_data):
+        if len(receive_data) == 18:
+            camera_high_motor_deviation_dir = receive_data[1]
+            camera_high_motor_deviation = receive_data[2] << 8 | receive_data[3]
+            camera_high_motor_deviation = (
+                camera_high_motor_deviation * -1
+                if camera_high_motor_deviation_dir == 0
+                else camera_high_motor_deviation
+            )
+
+            camera_steering_deviation_dir = receive_data[4]
+            camera_steering_deviation = (receive_data[5] << 8 | receive_data[6]) * 0.1
+            camera_steering_deviation = (
+                camera_steering_deviation * -1
+                if camera_steering_deviation_dir == 0
+                else camera_steering_deviation
+            )
+
+            turntable_steering_deviation_dir = receive_data[7]
+            turntable_steering_deviation = (
+                receive_data[8] << 8 | receive_data[9]
+            ) * 0.1
+            turntable_steering_deviation = (
+                turntable_steering_deviation * -1
+                if turntable_steering_deviation_dir == 0
+                else turntable_steering_deviation
+            )
+
+            overturn_steering_middle_dir = receive_data[10]
+            overturn_steering_middle = (receive_data[11] << 8 | receive_data[12]) * 0.1
+            overturn_steering_middle = (
+                overturn_steering_middle * -1
+                if overturn_steering_middle_dir == 0
+                else overturn_steering_middle
+            )
+
+            overturn_steering_high_dir = receive_data[13]
+            overturn_steering_high = (receive_data[14] << 8 | receive_data[15]) * 0.1
+            overturn_steering_high = (
+                overturn_steering_middle * -1
+                if overturn_steering_high_dir == 0
+                else overturn_steering_high
+            )
+
+            overturn_steering_up_speed = receive_data[16]
+            overturn_steering_down_speed = receive_data[17]
+
+            # self.sign_data.emit(
+            #     {
+            #         "_type": "get_deviation_data",
+            #         "plugins_mode": "mcu",
+            #         "data": {
+            #             "camera_high_motor_deviation": camera_high_motor_deviation,
+            #             "camera_steering_deviation": camera_steering_deviation,
+            #             "turntable_steering_deviation": turntable_steering_deviation,
+            #             "overturn_steering_middle": overturn_steering_middle,
+            #             "overturn_steering_high": overturn_steering_high,
+            #             "overturn_steering_up_speed": overturn_steering_up_speed,
+            #             "overturn_steering_down_speed": overturn_steering_down_speed,
+            #         },
+            #     }
+            # )
+            message = {
+                "_type": "get_deviation_data",
+                "plugins_mode": "mcu",
+                "data": {
+                    "camera_high_motor_deviation": camera_high_motor_deviation,
+                    "camera_steering_deviation": camera_steering_deviation,
+                    "turntable_steering_deviation": turntable_steering_deviation,
+                    "overturn_steering_middle": overturn_steering_middle,
+                    "overturn_steering_high": overturn_steering_high,
+                    "overturn_steering_up_speed": overturn_steering_up_speed,
+                    "overturn_steering_down_speed": overturn_steering_down_speed,
+                },
+            }
+            self.sendSocketMessage(msg="接收偏移量信息", data=message)
+            print("接收偏移量信息")
+        return
+
+    # 获取其他信息
+    def get_from_mcu_other_info(self, receive_data):
+        is_auto_send_base_info = self.get_data_from_receive_data(
+            receive_data=receive_data, start=1, len_data=1
+        )
+        is_move_retry = self.get_data_from_receive_data(
+            receive_data=receive_data, start=2, len_data=1
+        )
+        is_data_response = self.get_data_from_receive_data(
+            receive_data=receive_data, start=3, len_data=1
+        )
+        low_speed = self.get_data_from_receive_data(
+            receive_data=receive_data, start=4, len_data=2
+        )
+        is_test = self.get_data_from_receive_data(
+            receive_data=receive_data, start=6, len_data=1
+        )
+        to_init_mode = self.get_data_from_receive_data(
+            receive_data=receive_data, start=7, len_data=1
+        )
+        turntable_move_to_init_mode = self.get_data_from_receive_data(
+            receive_data=receive_data, start=8, len_data=1
+        )
+        led_count = self.get_data_from_receive_data(
+            receive_data=receive_data, start=9, len_data=2
+        )
+        turntable_steering_angle_ratio = self.get_data_from_receive_data(
+            receive_data=receive_data, start=11, len_data=2
+        )
+        is_manual_check = self.get_data_from_receive_data(
+            receive_data=receive_data, start=13, len_data=1
+        )
+        camera_steering_angle_ratio = self.get_data_from_receive_data(
+            receive_data=receive_data, start=14, len_data=4
+        )
+        is_auto_motor_to_disable = self.get_data_from_receive_data(
+            receive_data=receive_data, start=18, len_data=1
+        )
+        diff_dir = self.get_data_from_receive_data(
+            receive_data=receive_data, start=19, len_data=1
+        )
+        is_auto_send_pos_info = self.get_data_from_receive_data(
+            receive_data=receive_data, start=20, len_data=1
+        )
+        is_dog = self.get_data_from_receive_data(
+            receive_data=receive_data, start=21, len_data=1
+        )
+        has_been_set_motor_config = self.get_data_from_receive_data(
+            receive_data=receive_data, start=22, len_data=1
+        )
+
+        self.last_mcu_other_info_data["data"] = {
+            "is_auto_send_base_info": is_auto_send_base_info,
+            "is_move_retry": is_move_retry,
+            "is_data_response": is_data_response,
+            "low_speed": low_speed,
+            "is_test": is_test,
+            "to_init_mode": to_init_mode,
+            "turntable_move_to_init_mode": turntable_move_to_init_mode,
+            "led_count": led_count,
+            "turntable_steering_angle_ratio": turntable_steering_angle_ratio,
+            "is_manual_check": is_manual_check,
+            "camera_steering_angle_ratio": camera_steering_angle_ratio,
+            "is_auto_motor_to_disable": is_auto_motor_to_disable,
+            "diff_dir": diff_dir,
+            "is_auto_send_pos_info": is_auto_send_pos_info,
+            "is_dog": is_dog,
+            "has_been_set_motor_config": has_been_set_motor_config,
+        }
+        self.last_mcu_other_info_data["time"] = time.time()
+        self.last_mcu_other_info_data["num"] += 1
+        for k, v in self.last_mcu_other_info_data["data"].items():
+            print("k:{},v:{}".format(k, v))
+
+    def get_data_from_receive_data(
+        self, receive_data, start, len_data, data_magnification=1
+    ):
+        # data_magnification 数据放大倍数,或缩小倍数,默认为1
+        try:
+            if len_data == 1:
+                data = receive_data[start]
+                return data * data_magnification
+            elif len_data == 2:
+                data = receive_data[start] << 8 | receive_data[start + 1]
+                return data * data_magnification
+            elif len_data == 4:
+                data = (
+                    receive_data[start] << 24
+                    | receive_data[start + 1] << 16
+                    | receive_data[start + 2] << 8
+                    | receive_data[start + 3]
+                )
+                return data * data_magnification
+            return None
+        except:
+            return None
+
+    def get_from_mcu_base_info(self, receive_data):
+        # 数据缓存
+        self.last_mcu_info_data["time"] = time.time()
+        self.last_mcu_info_data["num"] += 1
+        # print("last_mcu_info_data:{}".format(self.last_mcu_info_data["time"]))
+
+        self.state_camera_motor = 3
+        self.state_camera_steering = 3
+        self.state_turntable_steering = 3
+        self.state_overturn_steering = 3
+        self.state_move_turntable_steering = 3
+        if len(receive_data) == 7:
+            self.m_t = 1
+            laser_state = receive_data[1]
+            self.state_camera_motor = receive_data[2]
+            self.state_camera_steering = receive_data[3]
+            self.state_turntable_steering = receive_data[4]
+            self.state_overturn_steering = receive_data[5]
+            flag = receive_data[6]
+            message = {
+                "_type": "show_mcu_info",
+                "plugins_mode": "mcu",
+                "data": "激光状态;{laser_state},相机高度状态:{state_camera_motor},相机角度状态:{state_camera_steering},转盘状态:{state_turntable_steering},翻板状态:{state_overturn_steering},flag:{flag}".format(
+                    laser_state=laser_state,
+                    state_camera_motor=self.state_camera_motor,
+                    state_camera_steering=self.state_camera_steering,
+                    state_turntable_steering=self.state_turntable_steering,
+                    state_overturn_steering=self.state_overturn_steering,
+                    flag=flag,
+                ),
+                "data_state": {
+                    "state_camera_motor": self.state_camera_motor,
+                    "state_camera_steering": self.state_camera_steering,
+                    "state_turntable_steering": self.state_turntable_steering,
+                    "state_overturn_steering": self.state_overturn_steering,
+                },
+            }
+            self.sendSocketMessage(msg="获取mcu设备运行状态信息", data=message)
+            # print("转盘:{},时间:{}".format(self.state_turntable_steering, time.time()))
+
+        if len(receive_data) == 8:
+            self.m_t = 2
+            laser_state = receive_data[1]
+            self.state_camera_motor = receive_data[2]
+            self.state_camera_steering = receive_data[3]
+            self.state_turntable_steering = receive_data[4]
+            self.state_overturn_steering = receive_data[5]
+            self.state_move_turntable_steering = receive_data[6]
+            flag = receive_data[7]
+            message = {
+                "_type": "show_mcu_info",
+                "plugins_mode": "mcu",
+                "data": "激光状态;{laser_state},相机高度状态:{state_camera_motor},相机角度状态:{state_camera_steering},转盘状态:{state_turntable_steering},转盘前后移动状态:{state_move_turntable_steering},翻板状态:{state_overturn_steering},flag:{flag}".format(
+                    laser_state=laser_state,
+                    state_camera_motor=self.state_camera_motor,
+                    state_camera_steering=self.state_camera_steering,
+                    state_turntable_steering=self.state_turntable_steering,
+                    state_overturn_steering=self.state_overturn_steering,
+                    state_move_turntable_steering=self.state_move_turntable_steering,
+                    flag=flag,
+                ),
+                "data_state": {
+                    "state_camera_motor": self.state_camera_motor,
+                    "state_camera_steering": self.state_camera_steering,
+                    "state_turntable_steering": self.state_turntable_steering,
+                    "state_overturn_steering": self.state_overturn_steering,
+                    "state_move_turntable_steering": self.state_move_turntable_steering,
+                },
+            }
+            # if self.state_camera_motor
+            if all(value == 2 for value in [self.state_camera_motor, self.state_camera_steering, self.state_turntable_steering, self.state_overturn_steering]):
+                self.init_state = True
+            self.sendSocketMessage(msg="获取mcu设备运行状态信息", data=message)
+        # 检查是否成功初始化
+        if self.is_just_init_time is False:
+            if self.mcu_move_state == 2:
+                self.is_just_init_time = True
+                print("is_just_init_time")
+                message = {
+                    "_type": "is_just_init_time",
+                    "plugins_mode": "mcu",
+                    "data": "",
+                }
+                self.sendSocketMessage(msg="检查设备初始化", data=message)
+        return
+
+    # def init(self, sign_data):
+    #     if sign_data["type"] == "connect_sign":
+    #         if sign_data["data"] == 0:
+    #             self.is_running = False
+    #         else:
+    #             self.is_running = True
+
+    def scan_serial_port(self) -> dict:
+        # 获取所有可用串口列表
+        ports_dict = {}
+        ports = serial.tools.list_ports.comports()
+        # 遍历所有端口并打印信息
+        for port in ports:
+            if "CH340" in port.description:
+                ports_dict[port.name] = {
+                    "name": port.name,
+                    "device": port.device,
+                    "description": port.description,
+                    "hwid": port.hwid,
+                    "manufacturer": port.manufacturer,
+                    "product": port.product,
+                    "serial_number": port.serial_number,
+                }
+        if len(ports_dict) <= 0:
+            return {}
+        return ports_dict
+
+    def remove_port(self, port_name):
+        """移除串口"""
+        print("remove", port_name)
+        data = {
+            "_type": "remove_port",
+            "plugins_mode": "auto_select_com",
+            "data": {"port_name": port_name},
+        }
+        self.sendSocketMessage(1, "串口被移除", data)
+
+    def add_port_by_linkage(self, port_name):
+        # port_value :串口基础信息
+        # todo 根据prot_value 信息自动进行连接
+        print("add", port_name)
+        # 对没有连接的设备进行尝试连接
+        message_data = {
+            "_type": "show_info",
+            "plugins_mode": "auto_select_com",
+            "data": {"text": "开始识别接口:{}".format(port_name)},
+        }
+        self.sendSocketMessage(
+            msg="开始识别接口:{}".format(port_name), data=message_data, device_status=1
+        )
+        time.sleep(1)
+        """
+        步骤:
+        1、进行临时连接,并发送命令,成功后,自动连接对应设备
+        """
+        try:
+            # 尝试使用115200波特率链接
+            serial_handle = serial.Serial(port=port_name, baudrate=115200, timeout=0.5)
+        except:
+            message_data = {
+                "_type": "show_info",
+                "plugins_mode": "auto_select_com",
+                "data": {"text": "串口:{} 被占用,或无法识别".format(port_name)},
+            }
+            self.sendSocketMessage(
+                1,
+                msg="串口:{} 被占用,或无法识别".format(port_name).format(port_name),
+                data=message_data,
+                device_status=3,
+            )
+            print("串口:{} 被占用".format(port_name))
+            return
+
+        time.sleep(2)
+        print("开始发送命令")
+        data = [90, 1]
+        try:
+            serial_handle.flushInput()  # 尝试重置输入缓冲区
+        except serial.SerialTimeoutException:
+            print("超时错误:无法在规定时间内重置输入缓冲区。")
+            self.sendSocketMessage(
+                1,
+                msg="超时错误:无法在规定时间内重置输入缓冲区。",
+                data=None,
+            )
+            serial_handle.close()
+            return
+        print("尝试写入数据")
+
+        buf = bytearray(b"")
+        buf.extend([0x55, 0x55, (0xFF & len(data))])
+        buf.extend(data)
+        buf.extend([0xFF & ~sum(data)])
+        try:
+            self.receive_data = b""
+            serial_handle.write(buf)
+        except serial.SerialTimeoutException:
+            print("写入数据错误")
+            serial_handle.close()
+            return
+
+        time.sleep(0.3)
+        print("尝试接收命令")
+        receive_data = self.read_cmd(serial_handle)
+        device_id = 0
+
+        if receive_data:
+            print("receive_data", receive_data)
+            if receive_data[0] == 90:
+                connect_flag = receive_data[1]
+                device_id = receive_data[2]
+
+        print("关闭串口:{}".format(port_name))
+        serial_handle.close()
+
+        if device_id > 0:
+            if device_id == 1:
+                self.to_connect_com(port_name)
+                message_data = {
+                    "_type": "show_info",
+                    "plugins_mode": "auto_select_com",
+                    "data": {"text": "MCU开始连接"},
+                }
+                self.sendSocketMessage(
+                    msg="MCU开始连接", data=message_data, device_status=1
+                )
+                self.connected_ports_dict[port_name] = "MCU"
+            message_data = {
+                "_type": "select_port_name",
+                "plugins_mode": "auto_select_com",
+                "data": {
+                    "device_name": "mcu" if device_id == 1 else "remote_control",
+                    "port_name": port_name,
+                },
+            }
+            self.sendSocketMessage(
+                msg="MCU连接成功", data=message_data, device_status=2
+            )
+            time.sleep(2)
+            loop = asyncio.get_event_loop()
+            loop.create_task(self.initDevice(), name="init_mcu")
+            # async def getBaseInfo():
+            #     while True:
+            #         await asyncio.sleep(1)
+            #         # 异步循环获取设备信息
+            #         self.to_get_mcu_base_info()
+            # asyncio.gather(getBaseInfo())
+        else:
+            print("串口无法识别")
+            # 走其他途径处理
+
+        # 检查当前MCU链接是否正常
+        # 正常跳过;记录为其他列表
+        # 不正常进行尝试连接
+        # 连接不上,记录为其他列表
+
+    def to_connect_com(self, port_name):
+        # 关闭串口
+        print("to_connect_com", port_name)
+        self.close_connect()
+        time.sleep(0.3)
+        self.connect_state = False
+        try:
+            self.serial_ins = SerialIns(port_name=port_name, baud=115200, timeout=0.1)
+            if not self.serial_ins.serial_handle:
+                message_data = {
+                    "_type": "show_info",
+                    "plugins_mode": "mcu",
+                    "data": "MCU 打开串口失败",
+                }
+                self.sendSocketMessage(
+                    msg="MCU 打开串口失败", data=message_data, device_status=-1
+                )
+                self.serial_ins = None
+                self.connect_state = False
+                return False
+
+        except:
+            message_data = {
+                "_type": "show_info",
+                "plugins_mode": "mcu",
+                "data": "MCU 打开串口失败",
+            }
+            self.sendSocketMessage(
+                msg="MCU 打开串口失败", data=message_data, device_status=-1
+            )
+            self.serial_ins = None
+            self.connect_state = False
+            return False
+
+        message_data = {
+            "_type": "show_info",
+            "plugins_mode": "mcu",
+            "data": "MCU 开始连接",
+        }
+        self.sendSocketMessage(msg="MCU 开始连接", data=message_data, device_status=1)
+        # =======================发送连接请求=================================
+        cmd = 90
+        data = [cmd, 1]
+        print("405  发送 连接请求  -----------------------------------------")
+        print(self.serial_ins)
+        # self.serial_ins.clearn_flush()
+        self.serial_ins.write_cmd(data)
+
+        # 延迟接收数据
+        time.sleep(0.3)
+        receive_data = self.serial_ins.read_cmd(out_time=1)
+        if receive_data:
+            print(
+                "409  receive_data--90:{}".format(self.change_hex_to_int(receive_data))
+            )
+        if receive_data:
+            # receive_data[2]=1 表示为MCU设备编号
+            if receive_data[0] == 90 and receive_data[2] == 1:
+                connect_flag = receive_data[1]
+                # 是否有初始化
+                try:
+                    mcu_has_been_set = receive_data[
+                        6
+                    ]  # 设备是否有初始化 ,1 表示已初始化
+                except:
+                    mcu_has_been_set = 99  # 未知状态
+                print("MCU初始化信息{}".format(mcu_has_been_set))
+                message_data = {
+                    "_type": "show_info",
+                    "plugins_mode": "mcu",
+                    "data": "MCU 已连接",
+                }
+                self.sendSocketMessage(
+                    msg="MCU 已连接", data=message_data, device_status=1
+                )
+                self.connect_state = True
+                self.is_running = True
+                print("MCU 已连接")
+                self.port_name = port_name
+                return
+
+        print("MCU 连接失败")
+        message_data = {
+            "_type": "show_info",
+            "plugins_mode": "mcu",
+            "data": "MCU 连接失败",
+        }
+        self.sendSocketMessage(msg="MCU 连接失败", data=message_data, device_status=-1)
+        self.close_connect()
+
+    def close_connect(self):
+        self.port_name = ""
+        if self.serial_ins:
+            self.serial_ins.close_serial_port()
+            self.is_running = False
+            self.connect_state = False
+            self.connected_ports_dict = {}  # 已连接的ports
+            self.p_list = []
+            self.temp_ports_dict = {}
+            self.init_state = False
+            print("关闭MCU")
+
+    @property
+    def mcu_move_state(self):
+        if self.m_t == 1:
+            if (
+                self.state_camera_motor == 2
+                and self.state_camera_steering == 2
+                and self.state_turntable_steering == 2
+                and self.state_overturn_steering == 2
+            ):
+                self._mcu_move_state = 2
+            else:
+                self._mcu_move_state = 1
+        else:
+            if (
+                self.state_camera_motor == 2
+                and self.state_camera_steering == 2
+                and self.state_turntable_steering == 2
+                and self.state_overturn_steering == 2
+                and self.state_move_turntable_steering == 2
+            ):
+                self._mcu_move_state = 2
+            else:
+                self._mcu_move_state = 1
+
+        # self._mcu_move_state = 2
+        return self._mcu_move_state
+
+    def to_deal_device(self, device_name, value=1, _type=0, times=1, delay=0):
+        """
+        value 激光0关 1开
+        mp3_player  value 表示0表示关,1表示开,_type 表示歌曲切换到指定歌曲
+        delay:延迟处理,单位为0.1秒,即delay=100时,表示延迟10秒
+        """
+        device_id = self.device_name_dict[device_name]
+        if device_name == "buzzer":
+            value = int(value)
+
+        cmd = 3
+        data = [
+            cmd,
+            device_id,
+            value,
+            _type,
+            times,
+            delay,
+        ]
+
+        self.add_send_data_queue(data)
+        # if self.serial_ins:
+        #     self.serial_ins.write_cmd(data)
+        return True
+
+    def to_device_move(
+        self,
+        device_name,
+        value=0,
+        max_speed=None,
+        up_speed=None,
+        down_speed=None,
+        _is_debug=0,
+        is_relative=0,
+        is_deviation=1,
+    ):
+        """
+        此处输入单位为 毫米,以及度  需要先缩小,再放大
+        """
+        print("移动",time.time())
+        speed = settings.moveSpeed()
+
+        cmd = 1
+        device_id = self.device_name_dict[device_name]
+        match device_name:
+            case "camera_high_motor":
+                # value 单位毫米
+                # max_speed = 10000 if max_speed is None else max_speed
+                # up_speed = 800 if up_speed is None else up_speed
+                # down_speed = 700 if down_speed is None else down_speed
+
+                max_speed = (
+                    speed[device_name]["max_speed"] if max_speed is None else max_speed
+                )
+                up_speed = (
+                    speed[device_name]["up_speed"] if up_speed is None else up_speed
+                )
+                down_speed = (
+                    speed[device_name]["down_speed"]
+                    if down_speed is None
+                    else down_speed
+                )
+
+                value = value / 10  # value 单位毫米
+                assert 0 <= value <= 40
+                assert 0 <= max_speed <= 10000
+            case "camera_steering":
+                # 角度为度 未放大 精确到0.1度
+                max_speed = 6000 if max_speed is None else max_speed
+                up_speed = 500 if up_speed is None else up_speed
+                down_speed = 500 if down_speed is None else down_speed
+                assert -360 <= value <= 360
+            case "turntable_steering":
+                # 角度为度 未放大 精确到0.1度
+                # max_speed = 6000 if max_speed is None else max_speed
+                # up_speed = 500 if up_speed is None else up_speed
+                # down_speed = 400 if down_speed is None else down_speed
+                max_speed = (
+                    speed[device_name]["max_speed"] if max_speed is None else max_speed
+                )
+                up_speed = (
+                    speed[device_name]["up_speed"] if up_speed is None else up_speed
+                )
+                down_speed = (
+                    speed[device_name]["down_speed"]
+                    if down_speed is None
+                    else down_speed
+                )
+
+                assert -720 <= value <= 720
+
+            case "overturn_steering":
+                # 角度为度 未放大 精确到0.1度
+                max_speed = 2 if max_speed is None else max_speed
+                up_speed = 1 if up_speed is None else up_speed
+                down_speed = 1 if down_speed is None else down_speed
+                assert 0 <= value <= 360
+
+            case "turntable_position_motor":
+                # value 单位毫米
+                max_speed = 11000 if max_speed is None else max_speed
+                up_speed = 900 if up_speed is None else up_speed
+                down_speed = 900 if down_speed is None else down_speed
+                value = value / 10  # value 单位毫米
+                assert 0 <= value <= 900
+                assert 0 <= max_speed <= 15000
+
+        _dir = True if value >= 0 else False
+
+        value = int(abs(value * 10))  # 此处value赋值后,单位为mm以及0.1度
+        print("准备执行",device_name, value)
+        data = [
+            cmd,
+            device_id,
+            1 if _dir else 0,
+            0xFF & value >> 8,
+            0xFF & value,
+            0xFF & max_speed >> 8,
+            0xFF & max_speed,
+            0xFF & up_speed >> 8,
+            0xFF & up_speed,
+            0xFF & down_speed >> 8,
+            0xFF & down_speed,
+            _is_debug,
+            is_deviation,
+            is_relative,
+        ]
+        self.add_send_data_queue(data)
+
+    def to_get_mcu_base_info(self):
+        if self.connect_state:
+            self.lock.acquire()
+            # print('==========================>1111')
+            # print("-------------------to_get_mcu_base_info--------------------------")
+            data = [self.command["get_all_info"], 1]
+            f = True
+            try:
+                self.serial_ins.write_cmd(data)
+            except:
+                f = False
+                pass
+
+            self.lock.release()
+            if not f:
+                self.connect_state = False
+                return False
+            else:
+                return True
+
+    def check_before_action(self):
+        if self.state != 2:
+            print("check_before_action 设备正在运行中~")
+            self.sendSocketMessage(
+                code=1, msg="设备正在运行中", device_status=1
+            )
+            return False
+
+        if self.mcu_move_state != 2:
+            if settings.IS_LIN_SHI_TEST:
+                return True
+            # self.show_info("mcu 非停止状态")
+            self.sendSocketMessage(code=1, msg="mcu 非停止状态", device_status=1)
+        return True
+
+    def controlDevice(self, device_name, value):
+        '''控制设备移动等'''
+        if not self.is_running:
+            self.sendSocketMessage(
+                code=1, msg="mcu设备未连接,请先连接设备", device_status=0
+            )
+            return False
+        if not self.init_state:
+            self.sendSocketMessage(
+                code=1, msg="mcu设备未初始化", device_status=4
+            )
+            return False
+        _is_debug = 1
+        match device_name:
+            case "camera_high_motor":
+                # 相机电机
+                print(device_name, value)
+                self.to_device_move(
+                    device_name=device_name,
+                    value=float(value),
+                    max_speed=1400,
+                    up_speed=400,
+                    down_speed=100,
+                    _is_debug=_is_debug,
+                    is_deviation=0,
+                )
+            case "camera_steering":
+                print(device_name, value)
+                # 相机舵机
+                self.to_device_move(
+                    device_name=device_name,
+                    value=float(value),
+                    _is_debug=_is_debug,
+                    is_deviation=0,
+                )
+            case "turntable_steering":
+                # 转盘舵机
+                self.to_device_move(
+                    device_name=device_name,
+                    value=float(value),
+                    _is_debug=_is_debug,
+                    is_deviation=0,
+                )
+            case "turntable_position_motor":
+                # 转盘舵机
+                self.to_device_move(
+                    device_name=device_name,
+                    value=float(value),
+                    max_speed=1400,
+                    up_speed=400,
+                    down_speed=100,
+                    _is_debug=_is_debug,
+                    is_deviation=0,
+                )
+            case "overturn_steering":
+                # 翻板舵机中位
+                self.to_deal_device(
+                    device_name="overturn_steering",
+                )
+            case "laser_position":
+                self.to_deal_device(
+                    device_name="laser_position", value=0 if int(value) <= 0 else 1
+                )
+            case "take_picture":
+                capture_one = DigiCam()
+                try:
+                    camera_is_connect = capture_one.checkCameraConnect()
+                    if camera_is_connect is not True:
+                        self.sendSocketMessage(1,"相机未连接,请检查",device_status=-1)
+                        return
+                    capture_one.getCaptureFolderPath()
+                    if value > 0:
+                        capture_one.auto_focus()
+                    capture_one.run_capture_action("Capture")
+                except:
+                    self.sendSocketMessage(1,"digicam未初始化,请检查",device_status=-1)
+            case "to_deal_device":
+                self.to_deal_device(device_name, value=value, _type=0, times=1)
+            case _:
+                pass
+            # case "photograph":
+            #     self.photograph(goods_art_no=None)
+    def checkDevice(self):
+        if not self.is_running:
+            self.sendSocketMessage(
+                code=1, msg="mcu设备未连接,请先连接设备", device_status=0
+            )
+            return False
+        if self.init_state is not True:
+            self.sendSocketMessage(code=1, msg="mcu设备未初始化", device_status=4)
+            return False
+        if self.action_state != 2:
+            self.sendSocketMessage(
+                code=1,
+                msg="当前有未完成的任务,请稍后再试",
+                device_status=0,
+            )
+            return False
+
+    async def run_mcu_config(self, config_list, goods_art_no, action_info):
+        if self.checkDevice() == False:
+            return
+        image_counts = 0
+        if config_list:
+            for idx, item in enumerate(config_list):
+                is_take_picture = item["take_picture"]
+                if is_take_picture:
+                    image_counts += 1
+                    # 批量插入
+                    image_deal_mode = 0 if action_info == "执行左脚程序" else 1
+                    insert_photo_records(image_deal_mode=image_deal_mode, goods_art_no=goods_art_no, image_index=idx)
+            total_len = len(config_list)
+            self.action_state = 1
+            self.msg_type = "image_process"
+            self.sendSocketMessage(
+                code=0,
+                msg="MCU 命令已发送完成",
+                device_status=2,
+                data={
+                    "goods_art_no": goods_art_no,
+                    "image_counts": image_counts,
+                    "current_time":datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+                },
+            )
+            self.msg_type = "mcu"
+            for index, action in enumerate(config_list):
+                await asyncio.sleep(0.1)
+                action_is_take_picture = action["take_picture"]
+                image_index = -1
+                if action_is_take_picture:
+                    image_index = index
+                program_item = ProgramItem(
+                    websocket_manager=self.websocket_manager,
+                    action_data=action,
+                    mcu=self,
+                    goods_art_no=goods_art_no,
+                    image_index=image_index,
+                )
+                print("self.action_state===>", self.action_state)
+                if self.action_state != 1:
+                    # 异常终止
+                    print("action异常终止")
+                    break
+                self.msg_type = "photo_take"
+                if not program_item.run(total_len):
+                    self.sendSocketMessage(
+                        code=1,
+                        msg="{} 执行失败~".format(program_item.action_name),
+                        device_status=0,
+                    )
+                    self.to_deal_device(device_name="buzzer", times=3)
+                    break
+                else:
+                    # self.show_info("{}执行完成~".format(action.action_name))
+                    self.sendSocketMessage(
+                        code=0,
+                        msg="{} 执行完成~".format(program_item.action_name),
+                        data={"goods_art_no": goods_art_no},
+                        device_status=2,
+                    )
+                self.msg_type = "mcu"
+                # 在第三张图时检查是否有对应图片生成
+                # if index == 3:
+                #     # if not self.image_process_data.check_photo_is_get():
+                #     self.sendSocketMessage(
+                #             code=1,
+                #             msg="未获取到图片数据",
+                #             device_status=0,
+                #         )
+                #     self.action_state = 2
+                #     return
+                if index == total_len - 1:
+                    #  最后一个初始化处理
+                    pass
+                # self.action_state = 2
+            self.action_state = 2
+            self.msg_type = "photo_take_finish"
+            self.sendSocketMessage(
+                code=0,
+                msg=f"货号:{goods_art_no},执行完成",
+                device_status=2,
+            )
+            self.msg_type = "mcu"
+
+
+async def checkMcuConnection(device_ctrl: DeviceControl):
+    if device_ctrl.is_running == True:
+        message = {
+            "_type": "select_port_name",
+            "plugins_mode": "auto_select_com",
+            "data": device_ctrl.temp_ports_dict,
+        }
+        device_ctrl.device_status = 2
+        device_ctrl.sendSocketMessage(code=0, msg="MCU连接成功", data=message)
+        return
+    """实时检测串口是否连接"""
+    while True:
+        await asyncio.sleep(0.5)
+        ports_dict = device_ctrl.scan_serial_port()
+        device_ctrl.temp_ports_dict = ports_dict
+        if not ports_dict:
+            # 全部清空 移除所有串口
+            if device_ctrl.p_list:
+                _p = device_ctrl.p_list.pop()
+                device_ctrl.remove_port(_p)
+            print("串口未连接,请检查")
+            device_ctrl.sendSocketMessage(code=1, msg="串口未连接,请检查",device_status=-1)
+            continue
+        if ports_dict:
+            for index, _i in enumerate(device_ctrl.p_list):
+                if _i not in ports_dict:
+                    _p = device_ctrl.p_list.pop(index)
+                    device_ctrl.remove_port(_p)
+            for _port_name, _port_value in ports_dict.items():
+                if _port_name not in device_ctrl.p_list:
+                    try:
+                        device_ctrl.p_list.append(_port_name)
+                        device_ctrl.add_port_by_linkage(_port_name)
+                    except BaseException as e:
+                        print("串口不存在{} {}".format(_port_name, e))

+ 136 - 0
python/mcu/DeviceHandler.py

@@ -0,0 +1,136 @@
+import time
+from mcu.SerialIns import SerialIns
+
+class DeviceHandler:
+
+    def __init__(self, windows=None):
+        super().__init__()
+        self.windows = windows
+        self.serial_ins = None
+        self.port_name = ""
+        self.connect_state = False
+        self.is_running = False
+
+    def to_connect_com(self, port_name, is_test=False):
+        self.close_connect()
+        time.sleep(0.5)
+        try:
+            # 原值为9600
+            self.serial_ins = SerialIns(port_name=port_name, baud=115200)
+            # self.serial_ins = SerialIns(port_name=port_name, baud=9600)
+            if self.serial_ins.serial_handle:
+                self.sign_data.emit(
+                    {
+                        "_type": "show_info",
+                        "plugins_mode": "remote_control",
+                        "data": "遥控设备 打开串口成功",
+                    }
+                )
+
+                self.connect_state = True
+                self.sign_data.emit(
+                    {
+                        "_type": "remote_control_connect",
+                        "plugins_mode": "remote_control",
+                        "data": port_name,
+                    }
+                )
+                self.port_name = port_name
+                if is_test is False:
+                    self.start()
+                return True
+
+            else:
+                self.sign_data.emit(
+                    {
+                        "_type": "show_info",
+                        "plugins_mode": "remote_control",
+                        "data": "遥控设备 打开串口失败",
+                    }
+                )
+
+                self.serial_ins = None
+                self.connect_state = False
+        except:
+            self.sign_data.emit(
+                {
+                    "_type": "show_info",
+                    "plugins_mode": "remote_control",
+                    "data": "遥控设备 打开串口失败",
+                }
+            )
+            self.serial_ins = None
+            self.connect_state = False
+            return False
+
+    def close_connect(self):
+        self.port_name = ""
+        if self.connect_state:
+            self.serial_ins.close_serial_port()
+            self.connect_state = False
+
+    def __del__(self):
+        self.close_connect()
+
+    def analysis_received_data(self):
+        if not self.connect_state:
+            return
+        receive_data = self.serial_ins.read_cmd(out_time=1, check=None)
+        if receive_data is False:
+            self.connect_state = False
+            return False
+        if not receive_data:
+            return
+        else:
+            print(
+                "read receive_data {}".format(
+                    self.serial_ins.change_hex_to_int(receive_data)
+                )
+            )
+            pass
+        # 数据 结构 command,按命令解析
+        if receive_data[0] == 1:
+            # 扫码数据
+            bar_code = receive_data[1:].decode()
+            bar_code = bar_code.replace("\r", "")
+            bar_code = bar_code.replace("\n", "")
+
+            self.sign_data.emit(
+                {"_type": 0, "plugins_mode": "remote_control", "data": bar_code}
+            )
+            return
+        if receive_data[0] == 9:
+            button_value = receive_data[1]
+            data = {"button_value": button_value}
+            self.sign_data.emit(
+                {"_type": 9, "plugins_mode": "remote_control", "data": data}
+            )
+            return
+        pass
+
+    def run(self):
+        # self.show_info.emit("未连接")
+        # self.data_command_sign.emit(data)
+        self.is_running = True
+        while 1:
+            time.sleep(0.05)
+            if not self.connect_state:
+                self.sign_data.emit(
+                    {
+                        "_type": "show_info",
+                        "plugins_mode": "remote_control",
+                        "data": "遥控设备 未连接",
+                    }
+                )
+                break
+            self.analysis_received_data()
+
+        self.is_running = False
+        if not self.connect_state:
+            self.sign_data.emit(
+                {
+                    "_type": "show_info",
+                    "plugins_mode": "remote_control",
+                    "data": "遥控设备 未连接",
+                }
+            )

+ 1066 - 0
python/mcu/Mcu.py

@@ -0,0 +1,1066 @@
+from .BaseClass import BaseClass
+from utils.SingletonType import SingletonType
+import settings
+from .SerialIns import SerialIns
+import time
+from threading import Lock
+from collections import defaultdict
+# from threading import Thread
+
+
+class Mcu(BaseClass, metaclass=SingletonType):
+    instance = None
+    init_flag = None
+    # sign_data = Signal(dict)
+    # self_sign = Signal(dict)
+
+    def __init__(self, window, port_name=None):
+        super().__init__(BaseClass)
+        if self.init_flag:
+            return
+        else:
+            self.init_flag = True
+        # 终端设备是否处于运动中 1运动中,2已停止  3未初始化
+        self.msg_type = "mcu"
+        self.m_t = 1
+        self._mcu_move_state = 0
+        self.state_camera_motor = 3
+        self.state_camera_steering = 3
+        self.state_turntable_steering = 3
+        self.state_overturn_steering = 3
+        self.state_move_turntable_steering = 3
+
+        self.last_from_mcu_move_respond_data = None
+        self.camera_motor_speed = 0
+        self.camera_motor_value = 0
+        self.init_state = False
+
+        self.connect_state = False
+        self.port_name = ""
+        self.serial_ins = None
+
+        self.connect_turn_state = False
+        self.serial_turn_ins = None
+        self.t_n = 0
+
+        if port_name:
+            self.serial_ins = SerialIns(port_name=port_name, baud=115200)
+            if self.serial_ins.serial_handle:
+                self.connect_state = True
+                self.port_name = port_name
+
+        self.device_name_dict = {
+            "camera_steering": 0,
+            "camera_high_motor": 1,
+            "turntable_steering": 2,
+            "overturn_steering": 3,
+            "laser_position": 4,
+            "buzzer": 5,
+            "split": 6,
+            "turntable_position_motor": 7,
+            "mp3_player": 8,
+            "mcu": 99,
+        }
+
+        # 最近的mcu基础信息,用于获取数据状态检查
+        self.last_mcu_info_data = {
+            "num": 0,
+            "time": time.time(),
+            "data": None,
+        }
+        # 最近的mcu的其他配置
+        self.last_mcu_other_info_data = {
+            "num": 0,
+            "time": time.time(),
+            "data": {},
+        }
+
+        self.command = {
+            "to_device_move": 1,  # 设备运动
+            "to_init_device": 2,  # 初始化设备
+            "to_deal_other_device": 3,  # 处理其他设备
+            "get_all_info": 29,  # 获取所有信息
+            "set_deviation": 40,  # 设置偏移量
+            "get_deviation": 41,  # 读取偏移量
+            "signal_forwarding": 91,  # 信号转发处理
+            "signal_forwarding_return": 92,  # 信号转发返回
+            "get_other_info": 44,  # 获取其他信息
+            "open_rgb_led": 43,  ## RGB灯的处理与通讯
+            "set_other_info": 45,  # 设置其他信息
+            "query_remote_control_battery": 47,  # 查询遥控器电量
+            "set_turntable_mode": 48,  # 设置转盘通讯方式 1、串口、2、无线、3 混合
+            "stop_mcu": 93,  # 停止运行mcu
+        }
+        self.window = window
+        self.last_push_time = defaultdict(float)
+        self.is_running = False
+        self.is_wait_connect = False  # 等待链接
+        self.send_data_queue = []  # 发送队列
+        self.lock = Lock()
+        # 是否是刚进行完初始化;首次初始化,需要运动到指定第一个指定位置
+        self.is_just_init_time = False
+        self.init()
+
+        # ===========注册命令函数============
+        self.deal_code_func_dict = {
+            29: self.get_from_mcu_base_info,  # 获取基本情况
+            32: self.get_from_mcu_button,  # 获取按键信息
+            42: self.get_from_mcu_deviation_info,  # 获取偏移量信息
+            44: self.get_from_mcu_other_info,  # 获取其他配置参数
+            90: self.get_from_mcu_connect_info,  # 获取链接电脑信号
+            92: self.get_from_mcu_move_respond_data,  # 获取MCU响应
+            100: self.print_mcu_error_data,  # 打印下位机的错误内容
+        }
+
+    # 打印下位机的错误内容
+    def print_mcu_error_data(self, receive_data):
+        # 扫码数据
+        try:
+            data = receive_data[1:].decode()
+            if data == "设备初始化完成":
+                self.init_state = False
+                self.sendSocketMessage(msg=data,device_status=2)
+            print("115  print_mcu_error_data:", data)
+        except BaseException as e:
+            print("117 error {}".format(e))
+        return
+
+    # 停止运行mcu
+    def stop_mcu(self):
+        buf = [self.command["stop_mcu"]]
+        buf.extend(self.encapsulation_data(data=1, len_data=1))
+        self.add_send_data_queue(buf)
+
+    # 设置转盘通讯方式 1、串口、2、无线、3 混合
+    def to_set_turntable_mode(self, mode=1):
+        buf = [self.command["set_turntable_mode"]]
+        buf.extend(self.encapsulation_data(data=mode, len_data=1))
+        self.add_send_data_queue(buf)
+
+    # 查询遥控器电量
+    def query_remote_control_battery(self):
+        buf = [self.command["query_remote_control_battery"]]
+        buf.extend(self.encapsulation_data(data=1, len_data=1))
+        buf.extend(self.encapsulation_data(data=1, len_data=1))
+        buf.extend(self.encapsulation_data(data=1, len_data=1))
+        self.add_send_data_queue(buf)
+
+    # 获取MCU响应
+    def get_from_mcu_move_respond_data(self, receive_data):
+        self.last_from_mcu_move_respond_data = receive_data
+
+    def change_hex_to_int(self, _bytearray):
+        return " ".join([hex(x) for x in _bytearray])
+
+    # 获取基本情况
+    def get_from_mcu_base_info(self, receive_data):
+        # 数据缓存
+        self.last_mcu_info_data["time"] = time.time()
+        self.last_mcu_info_data["num"] += 1
+        # print("last_mcu_info_data:{}".format(self.last_mcu_info_data["time"]))
+
+        self.state_camera_motor = 3
+        self.state_camera_steering = 3
+        self.state_turntable_steering = 3
+        self.state_overturn_steering = 3
+        self.state_move_turntable_steering = 3
+        if len(receive_data) == 7:
+            self.m_t = 1
+            laser_state = receive_data[1]
+            self.state_camera_motor = receive_data[2]
+            self.state_camera_steering = receive_data[3]
+            self.state_turntable_steering = receive_data[4]
+            self.state_overturn_steering = receive_data[5]
+            flag = receive_data[6]
+            message = {
+                "_type": "show_mcu_info",
+                "plugins_mode": "mcu",
+                "data": "激光状态;{laser_state},相机高度状态:{state_camera_motor},相机角度状态:{state_camera_steering},转盘状态:{state_turntable_steering},翻板状态:{state_overturn_steering},flag:{flag}".format(
+                    laser_state=laser_state,
+                    state_camera_motor=self.state_camera_motor,
+                    state_camera_steering=self.state_camera_steering,
+                    state_turntable_steering=self.state_turntable_steering,
+                    state_overturn_steering=self.state_overturn_steering,
+                    flag=flag,
+                ),
+                "data_state": {
+                    "state_camera_motor": self.state_camera_motor,
+                    "state_camera_steering": self.state_camera_steering,
+                    "state_turntable_steering": self.state_turntable_steering,
+                    "state_overturn_steering": self.state_overturn_steering,
+                },
+            }
+            self.sendSocketMessage(msg="获取mcu设备运行状态信息", data=message)
+            # print("转盘:{},时间:{}".format(self.state_turntable_steering, time.time()))
+
+        if len(receive_data) == 8:
+            self.m_t = 2
+            laser_state = receive_data[1]
+            self.state_camera_motor = receive_data[2]
+            self.state_camera_steering = receive_data[3]
+            self.state_turntable_steering = receive_data[4]
+            self.state_overturn_steering = receive_data[5]
+            self.state_move_turntable_steering = receive_data[6]
+            flag = receive_data[7]
+            message = {
+                "_type": "show_mcu_info",
+                "plugins_mode": "mcu",
+                "data": "激光状态;{laser_state},相机高度状态:{state_camera_motor},相机角度状态:{state_camera_steering},转盘状态:{state_turntable_steering},转盘前后移动状态:{state_move_turntable_steering},翻板状态:{state_overturn_steering},flag:{flag}".format(
+                    laser_state=laser_state,
+                    state_camera_motor=self.state_camera_motor,
+                    state_camera_steering=self.state_camera_steering,
+                    state_turntable_steering=self.state_turntable_steering,
+                    state_overturn_steering=self.state_overturn_steering,
+                    state_move_turntable_steering=self.state_move_turntable_steering,
+                    flag=flag,
+                ),
+                "data_state": {
+                    "state_camera_motor": self.state_camera_motor,
+                    "state_camera_steering": self.state_camera_steering,
+                    "state_turntable_steering": self.state_turntable_steering,
+                    "state_overturn_steering": self.state_overturn_steering,
+                    "state_move_turntable_steering": self.state_move_turntable_steering,
+                },
+            }
+            self.sendSocketMessage(msg="获取mcu设备运行状态信息", data=message)
+        # 检查是否成功初始化
+        if self.is_just_init_time is False:
+            if self.mcu_move_state == 2:
+                self.is_just_init_time = True
+                print("is_just_init_time")
+                message = {
+                    "_type": "is_just_init_time",
+                    "plugins_mode": "mcu",
+                    "data": "",
+                }
+                self.sendSocketMessage(msg="检查设备初始化", data=message)
+        return
+
+    # 获取其他信息
+    def get_from_mcu_other_info(self, receive_data):
+        is_auto_send_base_info = self.get_data_from_receive_data(
+            receive_data=receive_data, start=1, len_data=1
+        )
+        is_move_retry = self.get_data_from_receive_data(
+            receive_data=receive_data, start=2, len_data=1
+        )
+        is_data_response = self.get_data_from_receive_data(
+            receive_data=receive_data, start=3, len_data=1
+        )
+        low_speed = self.get_data_from_receive_data(
+            receive_data=receive_data, start=4, len_data=2
+        )
+        is_test = self.get_data_from_receive_data(
+            receive_data=receive_data, start=6, len_data=1
+        )
+        to_init_mode = self.get_data_from_receive_data(
+            receive_data=receive_data, start=7, len_data=1
+        )
+        turntable_move_to_init_mode = self.get_data_from_receive_data(
+            receive_data=receive_data, start=8, len_data=1
+        )
+        led_count = self.get_data_from_receive_data(
+            receive_data=receive_data, start=9, len_data=2
+        )
+        turntable_steering_angle_ratio = self.get_data_from_receive_data(
+            receive_data=receive_data, start=11, len_data=2
+        )
+        is_manual_check = self.get_data_from_receive_data(
+            receive_data=receive_data, start=13, len_data=1
+        )
+        camera_steering_angle_ratio = self.get_data_from_receive_data(
+            receive_data=receive_data, start=14, len_data=4
+        )
+        is_auto_motor_to_disable = self.get_data_from_receive_data(
+            receive_data=receive_data, start=18, len_data=1
+        )
+        diff_dir = self.get_data_from_receive_data(
+            receive_data=receive_data, start=19, len_data=1
+        )
+        is_auto_send_pos_info = self.get_data_from_receive_data(
+            receive_data=receive_data, start=20, len_data=1
+        )
+        is_dog = self.get_data_from_receive_data(
+            receive_data=receive_data, start=21, len_data=1
+        )
+        has_been_set_motor_config = self.get_data_from_receive_data(
+            receive_data=receive_data, start=22, len_data=1
+        )
+
+        self.last_mcu_other_info_data["data"] = {
+            "is_auto_send_base_info": is_auto_send_base_info,
+            "is_move_retry": is_move_retry,
+            "is_data_response": is_data_response,
+            "low_speed": low_speed,
+            "is_test": is_test,
+            "to_init_mode": to_init_mode,
+            "turntable_move_to_init_mode": turntable_move_to_init_mode,
+            "led_count": led_count,
+            "turntable_steering_angle_ratio": turntable_steering_angle_ratio,
+            "is_manual_check": is_manual_check,
+            "camera_steering_angle_ratio": camera_steering_angle_ratio,
+            "is_auto_motor_to_disable": is_auto_motor_to_disable,
+            "diff_dir": diff_dir,
+            "is_auto_send_pos_info": is_auto_send_pos_info,
+            "is_dog": is_dog,
+            "has_been_set_motor_config": has_been_set_motor_config,
+        }
+        self.last_mcu_other_info_data["time"] = time.time()
+        self.last_mcu_other_info_data["num"] += 1
+        for k, v in self.last_mcu_other_info_data["data"].items():
+            print("k:{},v:{}".format(k, v))
+
+    # 设置其他信息
+    def set_to_mcu_other_info(self, data):
+        is_auto_send_base_info = data["is_auto_send_base_info"]
+        is_move_retry = data["is_move_retry"]
+        is_data_response = data["is_data_response"]
+        low_speed = data["low_speed"]
+        is_test = data["is_test"]
+        to_init_mode = data["to_init_mode"]
+        turntable_move_to_init_mode = data["turntable_move_to_init_mode"]
+        led_count = data["led_count"]
+        turntable_steering_angle_ratio = data["turntable_steering_angle_ratio"]
+        is_manual_check = data["is_manual_check"]
+        camera_steering_angle_ratio = data["camera_steering_angle_ratio"]
+        is_auto_motor_to_disable = data["is_auto_motor_to_disable"]
+        diff_dir = data["diff_dir"]
+        is_auto_send_pos_info = data["is_auto_send_pos_info"]
+        is_dog = data["is_dog"]
+        has_been_set_motor_config = data["has_been_set_motor_config"]
+
+        buf = [self.command["set_other_info"]]
+        buf.extend(self.encapsulation_data(data=is_auto_send_base_info, len_data=1))
+        buf.extend(self.encapsulation_data(data=is_move_retry, len_data=1))
+        buf.extend(self.encapsulation_data(data=is_data_response, len_data=1))
+        buf.extend(self.encapsulation_data(data=low_speed, len_data=2))
+        buf.extend(self.encapsulation_data(data=is_test, len_data=1))
+        buf.extend(self.encapsulation_data(data=to_init_mode, len_data=1))
+        buf.extend(
+            self.encapsulation_data(data=turntable_move_to_init_mode, len_data=1)
+        )
+        buf.extend(self.encapsulation_data(data=led_count, len_data=2))
+        buf.extend(
+            self.encapsulation_data(data=turntable_steering_angle_ratio, len_data=2)
+        )
+        buf.extend(self.encapsulation_data(data=is_manual_check, len_data=1))
+        buf.extend(
+            self.encapsulation_data(data=camera_steering_angle_ratio, len_data=4)
+        )
+        buf.extend(self.encapsulation_data(data=is_auto_motor_to_disable, len_data=1))
+        buf.extend(self.encapsulation_data(data=diff_dir, len_data=1))
+        buf.extend(self.encapsulation_data(data=is_auto_send_pos_info, len_data=1))
+        buf.extend(self.encapsulation_data(data=is_dog, len_data=1))
+        buf.extend(self.encapsulation_data(data=has_been_set_motor_config, len_data=1))
+        self.add_send_data_queue(buf)
+
+    # 获取按键信息
+    def get_from_mcu_button(self, receive_data):
+        button_name = receive_data[1]
+        self.deal_mcu_button(button_name)
+
+    # 获取偏移量信息
+    def get_from_mcu_deviation_info(self, receive_data):
+        if len(receive_data) == 18:
+            camera_high_motor_deviation_dir = receive_data[1]
+            camera_high_motor_deviation = receive_data[2] << 8 | receive_data[3]
+            camera_high_motor_deviation = (
+                camera_high_motor_deviation * -1
+                if camera_high_motor_deviation_dir == 0
+                else camera_high_motor_deviation
+            )
+
+            camera_steering_deviation_dir = receive_data[4]
+            camera_steering_deviation = (receive_data[5] << 8 | receive_data[6]) * 0.1
+            camera_steering_deviation = (
+                camera_steering_deviation * -1
+                if camera_steering_deviation_dir == 0
+                else camera_steering_deviation
+            )
+
+            turntable_steering_deviation_dir = receive_data[7]
+            turntable_steering_deviation = (
+                receive_data[8] << 8 | receive_data[9]
+            ) * 0.1
+            turntable_steering_deviation = (
+                turntable_steering_deviation * -1
+                if turntable_steering_deviation_dir == 0
+                else turntable_steering_deviation
+            )
+
+            overturn_steering_middle_dir = receive_data[10]
+            overturn_steering_middle = (receive_data[11] << 8 | receive_data[12]) * 0.1
+            overturn_steering_middle = (
+                overturn_steering_middle * -1
+                if overturn_steering_middle_dir == 0
+                else overturn_steering_middle
+            )
+
+            overturn_steering_high_dir = receive_data[13]
+            overturn_steering_high = (receive_data[14] << 8 | receive_data[15]) * 0.1
+            overturn_steering_high = (
+                overturn_steering_middle * -1
+                if overturn_steering_high_dir == 0
+                else overturn_steering_high
+            )
+
+            overturn_steering_up_speed = receive_data[16]
+            overturn_steering_down_speed = receive_data[17]
+
+            # self.sign_data.emit(
+            #     {
+            #         "_type": "get_deviation_data",
+            #         "plugins_mode": "mcu",
+            #         "data": {
+            #             "camera_high_motor_deviation": camera_high_motor_deviation,
+            #             "camera_steering_deviation": camera_steering_deviation,
+            #             "turntable_steering_deviation": turntable_steering_deviation,
+            #             "overturn_steering_middle": overturn_steering_middle,
+            #             "overturn_steering_high": overturn_steering_high,
+            #             "overturn_steering_up_speed": overturn_steering_up_speed,
+            #             "overturn_steering_down_speed": overturn_steering_down_speed,
+            #         },
+            #     }
+            # )
+            message = {
+                "_type": "get_deviation_data",
+                "plugins_mode": "mcu",
+                "data": {
+                    "camera_high_motor_deviation": camera_high_motor_deviation,
+                    "camera_steering_deviation": camera_steering_deviation,
+                    "turntable_steering_deviation": turntable_steering_deviation,
+                    "overturn_steering_middle": overturn_steering_middle,
+                    "overturn_steering_high": overturn_steering_high,
+                    "overturn_steering_up_speed": overturn_steering_up_speed,
+                    "overturn_steering_down_speed": overturn_steering_down_speed,
+                },
+            }
+            self.sendSocketMessage(msg="接收偏移量信息", data=message)
+            print("接收偏移量信息")
+        return
+
+    # 设备链接信息
+    def get_from_mcu_connect_info(self, receive_data):
+        connect_flag = receive_data[1]
+        device_id = receive_data[2]
+        try:
+            mcu_has_been_set = receive_data[6]  # 设备是否有初始化 ,1 表示已初始化
+        except:
+            mcu_has_been_set = 99  # 未知状态
+
+        # self.self_sign.emit({"type": "connect_sign", "data": connect_flag})
+        message = {"type": "connect_sign", "data": connect_flag}
+        self.sendSocketMessage(msg="接收链接信息", data=message)
+        print("接收链接信息")
+        return
+
+    def init(self):
+        self.self_sign.connect(self.deal_self_sign_data)
+
+    # LED 灯光处理
+    def open_rgb_led(
+        self,
+        color_name,
+        led_command=1,
+        brightness=80,
+        enable=True,
+        mode="loop",
+        times=2,
+        interval=0.1,
+    ):
+        color_name_value = {
+            "红色": (156, 6, 3),
+            "黄色": (255, 206, 25),
+            "绿色": (0, 128, 0),
+            "蓝色": (0, 25, 255),
+            "白色": (255, 250, 227),
+        }
+        if color_name in color_name_value:
+            buf = [self.command["open_rgb_led"]]
+            buf.append(1 if enable else 0)
+            buf.append(led_command)
+            buf.extend(color_name_value[color_name])
+            buf.extend(
+                [
+                    brightness,
+                    1 if mode == "loop" else 2,
+                    times,
+                    int(interval * 10),
+                ]
+            )
+            self.add_send_data_queue(buf)
+
+    def deal_self_sign_data(self, sign_data):
+        if sign_data["type"] == "connect_sign":
+            if sign_data["data"] == 0:
+                self.is_running = False
+            else:
+                self.is_running = True
+
+    def to_connect_com(self, port_name, is_test=False):
+        # 关闭串口
+        print("to_connect_com", port_name)
+        self.close_connect()
+        time.sleep(0.3)
+        self.connect_state = False
+        try:
+            self.serial_ins = SerialIns(port_name=port_name, baud=115200, timeout=0.1)
+            if not self.serial_ins.serial_handle:
+                # self.sign_data.emit(
+                #     {
+                #         "_type": "show_info",
+                #         "plugins_mode": "mcu",
+                #         "data": "MCU 打开串口失败",
+                #     }
+                # )
+                message = {
+                    "_type": "show_info",
+                    "plugins_mode": "mcu",
+                    "data": "MCU 打开串口失败",
+                }
+                self.sendSocketMessage(code=1,msg="接收链接信息", data=message)
+                self.serial_ins = None
+                self.connect_state = False
+                return False
+
+        except:
+            # self.sign_data.emit(
+            #     {
+            #         "_type": "show_info",
+            #         "plugins_mode": "mcu",
+            #         "data": "MCU 打开串口失败",
+            #     }
+            # )
+            message = {
+                    "_type": "show_info",
+                    "plugins_mode": "mcu",
+                    "data": "MCU 打开串口失败",
+                }
+            self.sendSocketMessage(code=1,msg="MCU 打开串口失败", data=message)
+            self.serial_ins = None
+            self.connect_state = False
+            return False
+
+        # self.sign_data.emit(
+        #     {"_type": "show_info", "plugins_mode": "mcu", "data": "MCU 开始连接"}
+        # )
+        message = {"_type": "show_info", "plugins_mode": "mcu", "data": "MCU 开始连接"}
+        self.sendSocketMessage(
+            code=1, msg="MCU 开始连接", data=message, device_status=1
+        )
+        # =======================发送连接请求=================================
+        cmd = 90
+        data = [cmd, 1]
+        print("405  发送 连接请求  -----------------------------------------")
+        print(self.serial_ins)
+        # self.serial_ins.clearn_flush()
+        self.serial_ins.write_cmd(data)
+
+        # 延迟接收数据
+        time.sleep(0.3)
+        receive_data = self.serial_ins.read_cmd(out_time=1)
+        if receive_data:
+            print(
+                "409  receive_data--90:{}".format(self.change_hex_to_int(receive_data))
+            )
+        if receive_data:
+            # receive_data[2]=1 表示为MCU设备编号
+            if receive_data[0] == 90 and receive_data[2] == 1:
+                connect_flag = receive_data[1]
+                # 是否有初始化
+                try:
+                    mcu_has_been_set = receive_data[
+                        6
+                    ]  # 设备是否有初始化 ,1 表示已初始化
+                except:
+                    mcu_has_been_set = 99  # 未知状态
+                print("MCU初始化信息{}".format(mcu_has_been_set))
+
+                # self.sign_data.emit(
+                #     {"_type": "show_info", "plugins_mode": "mcu", "data": "MCU 已连接"}
+                # )
+                message = {
+                    "_type": "show_info",
+                    "plugins_mode": "mcu",
+                    "data": "MCU 已连接",
+                }
+                self.sendSocketMessage(code=1, msg="MCU 开始连接", data=message)
+                self.connect_state = True
+                if not self.is_running:
+                    print("MCU start")
+                    self.start()
+                self.self_sign.emit({"type": "connect_sign", "data": connect_flag})
+                print("MCU 已连接")
+                self.port_name = port_name
+                return
+
+        print("MCU 连接失败")
+        self.sign_data.emit(
+            {"_type": "show_info", "plugins_mode": "mcu", "data": "MCU 连接失败"}
+        )
+        self.close_connect()
+
+    def close_connect(self):
+        self.port_name = ""
+        if self.serial_ins:
+            self.serial_ins.close_serial_port()
+            self.connect_state = False
+            self.init_state = False
+
+    @property
+    def mcu_move_state(self):
+        if self.m_t == 1:
+            if (
+                self.state_camera_motor == 2
+                and self.state_camera_steering == 2
+                and self.state_turntable_steering == 2
+                and self.state_overturn_steering == 2
+            ):
+                self._mcu_move_state = 2
+            else:
+                self._mcu_move_state = 1
+        else:
+            if (
+                self.state_camera_motor == 2
+                and self.state_camera_steering == 2
+                and self.state_turntable_steering == 2
+                and self.state_overturn_steering == 2
+                and self.state_move_turntable_steering == 2
+            ):
+                self._mcu_move_state = 2
+            else:
+                self._mcu_move_state = 1
+
+        # self._mcu_move_state = 2
+        return self._mcu_move_state
+
+    def get_basic_info_mcu(self):
+        receive_data = self.serial_ins.read_cmd(out_time=1)
+        if receive_data is False:
+            print("------------------------------------------------4657564654")
+            self.connect_state = False
+            return False
+        if not receive_data:
+            return
+        # print("receive_data")
+        # 数据 结构 command,按命令解析
+        # command 0(9) 相机高度1-2  相机角度3-4  转盘角度5-6 灯光状态7  激光指示器状态8,运行状态9
+        command = receive_data[0]
+        if command in self.deal_code_func_dict:
+            self.deal_code_func_dict[command](receive_data)
+
+    def run(self):
+        self.is_running = True
+        self.serial_ins.clearn_flush()
+        self.to_init_device_origin_point(device_name="mcu")
+        print("MCU 开始循环~")
+        while 1:
+            time.sleep(0.01)
+            if not self.serial_ins or not self.connect_state:
+                break
+            try:
+                # print("mcu   send_cmd")
+                self.send_cmd()
+                # time.sleep(0.01)
+                self.get_basic_info_mcu()
+            except BaseException as e:
+                print("121231298908", e)
+                break
+
+        self.is_running = False
+        self.connect_state = False
+        print("MCU 循环退出~")
+        # self.sign_data.emit(
+        #     {"_type": "show_info", "plugins_mode": "mcu", "data": "MCU 连接失败"}
+        # )
+        message = {"_type": "show_info", "plugins_mode": "mcu", "data": "MCU 连接失败"}
+        self.sendSocketMessage(code=1,msg="MCU 连接失败",data=message)
+        self.close_connect()
+
+    def __del__(self):
+        self.close_connect()
+
+    def send_cmd(self):
+        self.lock.acquire()
+        if self.send_data_queue:
+            data = self.send_data_queue.pop(0)
+            self.serial_ins.write_cmd(data)
+        else:
+            self.t_n += 1
+            # 加大发送获取基础数据的时间间隔
+            # 默认为0.01秒一个循环,每隔1.5秒发送数据
+            if self.t_n > 150:
+                self.t_n = 0
+                data = [self.command["get_all_info"], 1]
+                self.serial_ins.write_cmd(data)
+        self.lock.release()
+
+    def add_send_data_queue(self, data):
+        self.lock.acquire()
+        if self.serial_ins:
+            # print("send_data_queue  append  :{}".format(data))
+            self.send_data_queue.append(data)
+        self.lock.release()
+
+    def get_deviation(self):
+        # 发送获取偏移量
+        data = [self.command["get_deviation"], 1]
+        self.add_send_data_queue(data)
+        # if self.serial_ins:
+        #     self.serial_ins.write_cmd(data)
+        print("发送获取偏移量")
+
+    def get_other_info(self):
+        # 发送获取偏移量
+        data = [self.command["get_other_info"], 1]
+        self.add_send_data_queue(data)
+        print("发送获取其他信息")
+
+    def set_deviation(self, device_name, _type=0, deviation=0):
+        # turntable----0 angle_ratio   1 turntable_steering_deviation
+        # overturn ----0 middle   1 high
+        if device_name == "camera_high_motor":
+            deviation = deviation / 10  # deviation 原单位为mm
+        if device_name == "turntable_position_motor":
+            deviation = deviation / 10  # deviation 原单位为mm
+        if device_name == "camera_steering":
+            pass
+        if device_name == "turntable_steering":
+            pass
+        if device_name == "overturn_steering":
+            pass
+
+        device_id = self.device_name_dict[device_name]
+        _dir = 1 if deviation >= 0 else 0
+        deviation = int(abs(deviation * 10))
+        data = [
+            self.command["set_deviation"],
+            device_id,
+            _type,
+            _dir,
+            0xFF & deviation >> 8,
+            0xFF & deviation,
+        ]
+        self.add_send_data_queue(data)
+
+    def deal_mcu_button(self, button_name):
+        # 防止重复点击
+        s = time.time()
+        if s - self.last_push_time[button_name] > 0.1:
+            self.last_push_time[button_name] = s
+            # print("button_name", button_name)
+        else:
+            self.last_push_time[button_name] = s
+            return
+        if button_name == 1:
+            # 自动执行全部
+            # self.sign_data.emit(
+            #     {"_type": "mcu_button", "plugins_mode": "mcu", "data": "todo_1"}
+            # )
+            message = {"_type": "mcu_button", "plugins_mode": "mcu", "data": "todo_1"}
+            self.sendSocketMessage(code=0, msg="", data=message)
+        if button_name == 2:
+            # self.sign_data.emit(
+            #     {"_type": "mcu_button", "plugins_mode": "mcu", "data": "todo_2"}
+            # )
+            message = {"_type": "mcu_button", "plugins_mode": "mcu", "data": "todo_2"}
+            self.sendSocketMessage(code=0, msg="", data=message)
+        if button_name == 3:
+            # self.sign_data.emit(
+            #     {"_type": "mcu_button", "plugins_mode": "mcu", "data": "todo_3"}
+            # )
+            message = {"_type": "mcu_button", "plugins_mode": "mcu", "data": "todo_3"}
+            self.sendSocketMessage(code=0, msg="", data=message)
+
+    def get_mcu_state(self):
+        """
+        获取mcu 设备状态,当状态为停止时,其他程序才可正常执行
+        :return:
+        """
+        return True
+
+    def to_get_mcu_base_info(self):
+        if self.connect_state:
+            self.lock.acquire()
+            # print('==========================>1111')
+            # print("-------------------to_get_mcu_base_info--------------------------")
+            data = [self.command["get_all_info"], 1]
+            f = True
+            try:
+                self.serial_ins.write_cmd(data)
+            except:
+                f = False
+                pass
+
+            self.lock.release()
+            if not f:
+                self.connect_state = False
+                return False
+            else:
+                return True
+
+    def to_deal_device(self, device_name, value=1, _type=0, times=1, delay=0):
+        """
+        value 激光0关 1开
+        mp3_player  value 表示0表示关,1表示开,_type 表示歌曲切换到指定歌曲
+        delay:延迟处理,单位为0.1秒,即delay=100时,表示延迟10秒
+        """
+        device_id = self.device_name_dict[device_name]
+        if device_name == "buzzer":
+            value = int(value)
+
+        cmd = 3
+        data = [
+            cmd,
+            device_id,
+            value,
+            _type,
+            times,
+            delay,
+        ]
+
+        self.add_send_data_queue(data)
+        # if self.serial_ins:
+        #     self.serial_ins.write_cmd(data)
+        return True
+
+    # 让设备运动到原点 并设置MCU需要进行初始化
+    def to_init_device_origin_point(self, device_name, is_force=False):
+        device_id = self.device_name_dict[device_name]
+        cmd = 2
+        data = [cmd, device_id, 0 if is_force is False else 1]
+        self.open_rgb_led(color_name="红色")
+
+        self.add_send_data_queue(data)
+
+        if device_name == "mcu":
+            # 重置初始化标记为  从未初始化
+            QTimer.singleShot(5000, self.just_init)
+
+        return True
+
+    def just_init(self, *args):
+        # 重置初始化标记为  从未初始化
+        self.is_just_init_time = False
+
+    def to_device_move(
+        self,
+        device_name,
+        value=0,
+        max_speed=None,
+        up_speed=None,
+        down_speed=None,
+        _is_debug=0,
+        is_relative=0,
+        is_deviation=1,
+    ):
+        """
+        此处输入单位为 毫米,以及度  需要先缩小,再放大
+        """
+
+        speed = settings.moveSpeed()
+
+        cmd = 1
+        device_id = self.device_name_dict[device_name]
+
+        if device_name == "camera_high_motor":
+            # value 单位毫米
+            # max_speed = 10000 if max_speed is None else max_speed
+            # up_speed = 800 if up_speed is None else up_speed
+            # down_speed = 700 if down_speed is None else down_speed
+
+            max_speed = (
+                speed[device_name]["max_speed"] if max_speed is None else max_speed
+            )
+            up_speed = speed[device_name]["up_speed"] if up_speed is None else up_speed
+            down_speed = (
+                speed[device_name]["down_speed"] if down_speed is None else down_speed
+            )
+
+            value = value / 10  # value 单位毫米
+            assert 0 <= value <= 40
+            assert 0 <= max_speed <= 10000
+
+        if device_name == "camera_steering":
+            # 角度为度 未放大 精确到0.1度
+            max_speed = 6000 if max_speed is None else max_speed
+            up_speed = 500 if up_speed is None else up_speed
+            down_speed = 500 if down_speed is None else down_speed
+            assert -360 <= value <= 360
+
+        if device_name == "turntable_steering":
+            # 角度为度 未放大 精确到0.1度
+
+            # max_speed = 6000 if max_speed is None else max_speed
+            # up_speed = 500 if up_speed is None else up_speed
+            # down_speed = 400 if down_speed is None else down_speed
+
+            max_speed = (
+                speed[device_name]["max_speed"] if max_speed is None else max_speed
+            )
+            up_speed = speed[device_name]["up_speed"] if up_speed is None else up_speed
+            down_speed = (
+                speed[device_name]["down_speed"] if down_speed is None else down_speed
+            )
+
+            assert -720 <= value <= 720
+
+        if device_name == "overturn_steering":
+            # 角度为度 未放大 精确到0.1度
+            max_speed = 2 if max_speed is None else max_speed
+            up_speed = 1 if up_speed is None else up_speed
+            down_speed = 1 if down_speed is None else down_speed
+            assert 0 <= value <= 360
+
+        if device_name == "turntable_position_motor":
+            # value 单位毫米
+            max_speed = 11000 if max_speed is None else max_speed
+            up_speed = 900 if up_speed is None else up_speed
+            down_speed = 900 if down_speed is None else down_speed
+            value = value / 10  # value 单位毫米
+            assert 0 <= value <= 900
+            assert 0 <= max_speed <= 15000
+
+        _dir = True if value >= 0 else False
+
+        value = int(abs(value * 10))  # 此处value赋值后,单位为mm以及0.1度
+        data = [
+            cmd,
+            device_id,
+            1 if _dir else 0,
+            0xFF & value >> 8,
+            0xFF & value,
+            0xFF & max_speed >> 8,
+            0xFF & max_speed,
+            0xFF & up_speed >> 8,
+            0xFF & up_speed,
+            0xFF & down_speed >> 8,
+            0xFF & down_speed,
+            _is_debug,
+            is_deviation,
+            is_relative,
+        ]
+        self.add_send_data_queue(data)
+        # if self.serial_ins:
+        #     self.serial_ins.write_cmd(data)
+
+    # 通用串口数据解析器
+    def get_data_from_receive_data(
+        self, receive_data, start, len_data, data_magnification=1
+    ):
+        # data_magnification 数据放大倍数,或缩小倍数,默认为1
+        try:
+            if len_data == 1:
+                data = receive_data[start]
+                return data * data_magnification
+            elif len_data == 2:
+                data = receive_data[start] << 8 | receive_data[start + 1]
+                return data * data_magnification
+            elif len_data == 4:
+                data = (
+                    receive_data[start] << 24
+                    | receive_data[start + 1] << 16
+                    | receive_data[start + 2] << 8
+                    | receive_data[start + 3]
+                )
+                return data * data_magnification
+            return None
+        except:
+            return None
+
+    def encapsulation_data(self, data, len_data, data_magnification=1):
+        # data_magnification 数据放大倍数,或缩小倍数,默认为1
+        data = int(data * data_magnification)
+        if len_data == 1:
+            return [0xFF & data]
+        elif len_data == 2:
+            return [0xFF & data >> 8, 0xFF & data]
+        elif len_data == 4:
+            return [0xFF & data >> 24, 0xFF & data >> 16, 0xFF & data >> 8, 0xFF & data]
+
+    def __new__(cls, *args, **kwargs):
+        """如果当前没有实例时,调用父类__new__方法,生成示例,有则返回保存的内存地址。"""
+        if not cls.instance:
+            cls.instance = super().__new__(cls)
+        return cls.instance
+
+
+class McuDebug(object):
+    def __init__(self, windows, is_debug=True, is_deviation=False):
+        self.windows = windows
+        self.is_debug = 1 if is_debug else 0
+        self.is_deviation = 1 if is_deviation else 0
+
+    def camera_high_motor(self, value):
+        # 相机电机
+        self.windows.mcu.to_device_move(
+            device_name="camera_high_motor",
+            value=value,
+            max_speed=1400,
+            up_speed=400,
+            down_speed=100,
+            _is_debug=self.is_debug,
+            is_deviation=self.is_deviation,
+        )
+
+    def camera_steering(self, value):
+        # 相机舵机
+        self.windows.mcu.to_device_move(
+            device_name="camera_steering",
+            value=value,
+            _is_debug=self.is_debug,
+            is_deviation=self.is_deviation,
+        )
+
+    def turntable_steering(self, value):
+        # 转盘舵机
+        self.windows.mcu.to_device_move(
+            device_name="turntable_steering",
+            value=value,
+            _is_debug=self.is_debug,
+            is_deviation=self.is_deviation,
+        )
+
+    def turntable_position_motor(self, value):
+        # 转盘舵机
+        self.windows.mcu.to_device_move(
+            device_name="turntable_position_motor",
+            value=value,
+            max_speed=1400,
+            up_speed=400,
+            down_speed=100,
+            _is_debug=self.is_debug,
+            is_deviation=self.is_deviation,
+        )
+
+    def overturn_steering(self, value):
+        # 翻板舵机中位
+        self.windows.mcu.to_device_move(
+            device_name="overturn_steering",
+            value=value,
+            _is_debug=self.is_debug,
+            is_deviation=self.is_deviation,
+        )
+
+    def to_deal_device(self, device_name, value=1, _type=0, times=1):
+        self.windows.mcu.to_deal_device(
+            device_name, value=value, _type=_type, times=times
+        )
+
+    def photograph(self, goods_art_no=None):
+        self.windows.photograph(goods_art_no=goods_art_no)
+
+    def __move_equipment(self, data):
+        func_class = {
+            "相机电机": self.camera_high_motor,
+            "相机舵机": self.camera_steering,
+            "转盘舵机": self.turntable_steering,
+            "转盘前后电机": self.turntable_position_motor,
+        }
+        for key, value in data.items():
+            if key in func_class:
+                func_class[key](value=value)
+                time.sleep(0.1)
+
+    def move_equipment(self, data: dict):
+        Thread(target=self.__move_equipment, args=(data,)).start()

+ 79 - 0
python/mcu/McuDebug.py

@@ -0,0 +1,79 @@
+import time
+
+class McuDebug(object):
+
+    def __init__(self, mcu, is_debug=True, is_deviation=False):
+        self.mcu = mcu
+        self.is_debug = 1 if is_debug else 0
+        self.is_deviation = 1 if is_deviation else 0
+
+    def camera_high_motor(self, value):
+        # 相机电机
+        self.mcu.to_device_move(
+            device_name="camera_high_motor",
+            value=value,
+            max_speed=1400,
+            up_speed=400,
+            down_speed=100,
+            _is_debug=self.is_debug,
+            is_deviation=self.is_deviation,
+        )
+
+    def camera_steering(self, value):
+        # 相机舵机
+        self.mcu.to_device_move(
+            device_name="camera_steering",
+            value=value,
+            _is_debug=self.is_debug,
+            is_deviation=self.is_deviation,
+        )
+
+    def turntable_steering(self, value):
+        # 转盘舵机
+        self.mcu.to_device_move(
+            device_name="turntable_steering",
+            value=value,
+            _is_debug=self.is_debug,
+            is_deviation=self.is_deviation,
+        )
+
+    def turntable_position_motor(self, value):
+        # 转盘舵机
+        self.mcu.to_device_move(
+            device_name="turntable_position_motor",
+            value=value,
+            max_speed=1400,
+            up_speed=400,
+            down_speed=100,
+            _is_debug=self.is_debug,
+            is_deviation=self.is_deviation,
+        )
+
+    def overturn_steering(self, value):
+        # 翻板舵机中位
+        self.mcu.to_device_move(
+            device_name="overturn_steering",
+            value=value,
+            _is_debug=self.is_debug,
+            is_deviation=self.is_deviation,
+        )
+
+    def to_deal_device(self, device_name, value=1, _type=0, times=1):
+        self.mcu.to_deal_device(
+            device_name, value=value, _type=_type, times=times
+        )
+
+    def photograph(self, goods_art_no=None):
+        self.mcu.photograph(goods_art_no=goods_art_no)
+
+    def __move_equipment(self, data):
+        func_class = {
+            "相机电机": self.camera_high_motor,
+            "相机舵机": self.camera_steering,
+            "转盘舵机": self.turntable_steering,
+            "转盘前后电机": self.turntable_position_motor,
+        }
+        for key, value in data.items():
+            if key in func_class:
+                func_class[key](value=value)
+                time.sleep(0.1)

+ 292 - 0
python/mcu/McuDeviationSet.py

@@ -0,0 +1,292 @@
+from collections import defaultdict
+from .McuDebug import McuDebug
+from functools import partial
+import time
+import settings
+
+
+class McuDeviationSet():
+    # 调试处理
+    # data_sign = Signal(dict)
+    # info_sign = Signal(str)
+    # is_get_offset = Signal(dict)
+
+    def __init__(self, mcu):
+        super().__init__()
+        self.mcu = mcu
+        # self.setupUi(self)
+        # self.init()
+        # self.set_enable_by_mcu()
+        # self.show()
+        self.last_value = defaultdict(float)
+        self.mcu_debug = McuDebug(mcu, is_debug=True, is_deviation=False)
+        self.get_mcu_deviation()
+        # # 运动到设定位
+        # QTimer.singleShot(2500, self.init_pos)
+
+    # def set_enable_by_mcu(self):
+    #     if self.mcu.state_camera_motor != 2:
+    #         self.doubleSpinBox.setEnabled(False)
+    #         self.pushButton_3.setEnabled(False)
+
+    #     if self.mcu.state_camera_steering != 2:
+    #         self.doubleSpinBox_2.setEnabled(False)
+    #         self.pushButton_16.setEnabled(False)
+
+    #     if self.mcu.state_turntable_steering != 2:
+    #         self.doubleSpinBox_3.setEnabled(False)
+    #         self.pushButton_22.setEnabled(False)
+    #         self.doubleSpinBox_7.setEnabled(False)
+
+    def init(self):
+        self.is_get_offset.connect(self.init_pos)
+
+        self.windows.data_info.connect(self.get_mcu_deviation_info)
+        # 相机电机
+        self.doubleSpinBox.setSingleStep(1)
+        self.doubleSpinBox.setMinimum(0)
+        self.doubleSpinBox.setMaximum(400)
+        self.doubleSpinBox.valueChanged.connect(
+            lambda x: self.change_value(x, "相机电机")
+        )
+
+        # 相机舵机
+        self.doubleSpinBox_2.setSingleStep(0.1)
+        self.doubleSpinBox_2.setMinimum(-40)
+        self.doubleSpinBox_2.setMaximum(40)
+        self.doubleSpinBox_2.valueChanged.connect(
+            lambda x: self.change_value(x, "相机舵机")
+        )
+
+        # 转盘电机
+        self.doubleSpinBox_3.setSingleStep(1)
+        self.doubleSpinBox_3.setMinimum(-720)
+        self.doubleSpinBox_3.setMaximum(720)
+        self.doubleSpinBox_3.valueChanged.connect(
+            lambda x: self.change_value(x, "转盘舵机")
+        )
+
+        # 转盘前后电机
+        self.doubleSpinBox_7.setSingleStep(1)
+        self.doubleSpinBox_7.setMinimum(0)
+        self.doubleSpinBox_7.setMaximum(950)
+        self.doubleSpinBox_7.setValue(300)
+        self.doubleSpinBox_7.valueChanged.connect(
+            lambda x: self.change_value(x, "转盘前后电机")
+        )
+
+        # 翻板舵机中位
+        self.doubleSpinBox_4.setSingleStep(0.5)
+        self.doubleSpinBox_4.setValue(90)
+        self.doubleSpinBox_4.setMinimum(0)
+        self.doubleSpinBox_4.setMaximum(180)
+        self.doubleSpinBox_4.valueChanged.connect(
+            lambda x: self.change_value(x, "翻板舵机中位")
+        )
+
+        # 翻板舵机高位
+        self.doubleSpinBox_5.setSingleStep(0.5)
+        self.doubleSpinBox_5.setValue(10)
+        self.doubleSpinBox_5.setMinimum(0)
+        self.doubleSpinBox_5.setMaximum(180)
+        self.doubleSpinBox_5.valueChanged.connect(
+            lambda x: self.change_value(x, "翻板舵机高位")
+        )
+
+        # 翻板舵机上升速度
+        self.doubleSpinBox_6.setSingleStep(1)
+        self.doubleSpinBox_6.setValue(5)
+        self.doubleSpinBox_6.setMinimum(1)
+        self.doubleSpinBox_6.setMaximum(10)
+        self.doubleSpinBox_6.valueChanged.connect(
+            lambda x: self.change_value(x, "翻板舵机上升速度")
+        )
+
+        # 翻板舵机下降速度
+        self.doubleSpinBox_8.setSingleStep(1)
+        self.doubleSpinBox_8.setValue(5)
+        self.doubleSpinBox_8.setMinimum(1)
+        self.doubleSpinBox_8.setMaximum(10)
+        self.doubleSpinBox_8.valueChanged.connect(
+            lambda x: self.change_value(x, "翻板舵机下降速度")
+        )
+
+        # 获取偏移量
+        self.pushButton_2.clicked.connect(self.get_mcu_deviation)
+        # 设定相机高度偏移量
+        self.pushButton_3.clicked.connect(lambda *args: self.set_deviation("相机电机"))
+        # 设定相机舵机偏移量
+        self.pushButton_16.clicked.connect(lambda *args: self.set_deviation("相机舵机"))
+        # 设定转盘偏移量
+        self.pushButton_22.clicked.connect(lambda *args: self.set_deviation("转盘舵机"))
+        # 设定翻版舵机偏移量
+        self.pushButton_8.clicked.connect(
+            lambda *args: self.set_deviation("翻板舵机中位")
+        )
+        # 设定翻版舵机偏移量
+        self.pushButton_9.clicked.connect(
+            lambda *args: self.set_deviation("翻板舵机高位")
+        )
+        self.pushButton_10.clicked.connect(
+            lambda *args: self.set_deviation("翻板舵机上升速度")
+        )
+        self.pushButton_12.clicked.connect(
+            lambda *args: self.set_deviation("翻板舵机下降速度")
+        )
+
+        # 转盘重新获取转速比
+        # self.pushButton_7.clicked.connect(self.set_turntable_to_ratio_init)
+        # 转盘重新定位到原点
+        self.pushButton_11.clicked.connect(self._to_init_all)
+
+    def init_pos(self, data):
+        time.sleep(0.6)
+        func = partial(self._move_equipment, data=data)
+        self.do_thread_run(func, call_back=None, time_out=30)
+
+    def _move_equipment(self, data):
+        self.mcu_debug.move_equipment(data=data)
+        # MCU运动是否有停止检查,设定超时时间
+        time.sleep(1.5)
+        if self.check_mcu_move_is_stop() is False:
+            return
+
+    def check_mcu_move_is_stop(self, out_time=15):
+        _s = time.time()
+        while 1:
+            time.sleep(0.1)
+            if time.time() - _s > out_time:
+                return True
+            if self.mcu.mcu_move_state == 2:
+                return True
+            if settings.IS_LIN_SHI_TEST:
+                time.sleep(3)
+                return True
+
+    def change_value(self, value, name):
+        value = round(value, 2)
+        if self.last_value[name] - 1 <= value <= self.last_value[name] + 1:
+            if name == "相机电机":
+                self.mcu_debug.camera_high_motor(value=value)
+            if name == "相机舵机":
+                self.mcu_debug.camera_steering(value=value)
+            if name == "转盘舵机":
+                self.mcu_debug.turntable_steering(value=value)
+            if name == "转盘前后电机":
+                self.mcu_debug.turntable_position_motor(value=value)
+            if name == "翻板舵机中位":
+                self.mcu_debug.overturn_steering(value=value)
+            if name == "翻板舵机高位":
+                self.mcu_debug.overturn_steering(value=value)
+            if name == "翻板舵机上升速度":
+                pass
+            print(value, name)
+        self.last_value[name] = value
+
+    def set_deviation(self, name):
+        if name == "相机电机":
+            # 设定相机高度偏移量 单位mm
+            camera_high_motor_deviation = int(self.doubleSpinBox.value())
+            device_name = "camera_high_motor"
+            self.mcu.set_deviation(
+                device_name=device_name, _type=0, deviation=camera_high_motor_deviation
+            )
+
+        if name == "相机舵机":
+            # 设定相机舵机偏移量
+            camera_steering_deviation = self.doubleSpinBox_2.value()
+            device_name = "camera_steering"
+            self.mcu.set_deviation(
+                device_name=device_name, _type=0, deviation=camera_steering_deviation
+            )
+            self.doubleSpinBox_2.setValue(0.0)
+
+        if name == "转盘舵机":
+            # 设定转盘舵机偏移角度  单位 度
+            turntable_steering_deviation = self.doubleSpinBox_3.value()
+            device_name = "turntable_steering"
+            self.mcu.set_deviation(
+                device_name=device_name, _type=1, deviation=turntable_steering_deviation
+            )
+
+        if name == "翻板舵机中位":
+            # 设定翻版舵机偏移量
+            value = self.doubleSpinBox_4.value()
+            device_name = "overturn_steering"
+            self.mcu.set_deviation(
+                device_name=device_name, _type=0, deviation=value
+            )
+        if name == "翻板舵机高位":
+            # 设定翻版舵机偏移量
+            value = self.doubleSpinBox_5.value()
+            device_name = "overturn_steering"
+            self.mcu.set_deviation(
+                device_name=device_name, _type=1, deviation=value
+            )
+        if name == "翻板舵机上升速度":
+            # 设定翻版舵机偏移量
+            value = self.doubleSpinBox_6.value()
+            device_name = "overturn_steering"
+            self.mcu.set_deviation(
+                device_name=device_name, _type=2, deviation=value
+            )
+        if name == "翻板舵机下降速度":
+            # 设定翻版舵机偏移量
+            value = self.doubleSpinBox_8.value()
+            device_name = "overturn_steering"
+            self.mcu.set_deviation(
+                device_name=device_name, _type=3, deviation=value
+            )
+
+    def _to_init_all(self, *args):
+        self.mcu.to_init_device_origin_point(device_name="mcu", is_force=True)
+
+    def get_mcu_deviation(self):
+        self.mcu.get_deviation()
+
+    def get_mcu_deviation_info(self, data):
+        if "_type" not in data:
+            return
+        if data["_type"] == "get_deviation_data":
+            data = data["data"]
+            print("偏移量信息")
+            # overturn_steering_up_speed  overturn_steering_down_speed
+            # 相机电机 单位mm
+            if "camera_high_motor_deviation" in data:
+                self.doubleSpinBox.setValue(float(data["camera_high_motor_deviation"]))
+            # 相机舵机
+            if "camera_steering_deviation" in data:
+                self.doubleSpinBox_2.setValue(float(data["camera_steering_deviation"]))
+            # 转盘偏移位
+            if "turntable_steering_deviation" in data:
+                self.doubleSpinBox_3.setValue(
+                    float(data["turntable_steering_deviation"])
+                )
+            # 翻板舵机中位
+            if "overturn_steering_middle" in data:
+                self.doubleSpinBox_4.setValue(float(data["overturn_steering_middle"]))
+            # 翻板舵机高位
+            if "overturn_steering_high" in data:
+                self.doubleSpinBox_5.setValue(float(data["overturn_steering_high"]))
+
+            # 翻板舵机
+            if "overturn_steering_up_speed" in data:
+                self.doubleSpinBox_6.setValue(float(data["overturn_steering_up_speed"]))
+
+            if "overturn_steering_down_speed" in data:
+                self.doubleSpinBox_8.setValue(
+                    float(data["overturn_steering_down_speed"])
+                )
+
+            # 初始化位置
+            data = {
+                "相机电机": self.doubleSpinBox.value(),
+                "相机舵机": self.doubleSpinBox_2.value(),
+                "转盘舵机": self.doubleSpinBox_3.value(),
+                "转盘前后电机": self.doubleSpinBox_7.value(),
+            }
+            self.is_get_offset.emit(data)
+
+    def __del__(self):
+        self.state = 2
+        self.t = None

+ 395 - 0
python/mcu/ProgramItem.py

@@ -0,0 +1,395 @@
+import json
+import os
+
+from .BaseClass import BaseClass
+import settings
+import time
+from .capture.module_digicam import DigiCam
+from .capture.module_watch_dog import FileEventHandler
+
+class ProgramItem(BaseClass):
+    # program_sign = Signal(dict)
+    # program_refresh_photo_list_sign = Signal()
+
+    def __init__(self,websocket_manager, action_data:any, mcu, goods_art_no:str=None,image_index:int=-1):
+        super().__init__(BaseClass)
+        # 1 表示等待中,2表示没有等待
+        self.wait_state = 2
+        self.data = action_data
+        self.capture_one = DigiCam()
+        captrure_folder_path = self.capture_one.getCaptureFolderPath()
+        self.watch_dog = FileEventHandler()
+        self.watch_dog.goods_art_no = goods_art_no
+        self.watch_dog.image_index = image_index
+        self.watch_dog.start_observer(captrure_folder_path)
+        print("21 =========ProgramItem=======action_data=====")
+        print(action_data)
+        self.action_id = self.get_value(action_data, "id")
+        self.mode_type = self.get_value(action_data, "mode_type")
+        # self.action_type = self.get_value(action_data, "execution_type", "程序1")
+        self.action_name = self.get_value(action_data, "action_name", "")
+        self.is_wait = self.get_value(action_data, "is_wait", False)
+        self.is_need_confirm = self.get_value(action_data, "is_need_confirm", False)
+        self.image_index = self.get_value(action_data, "picture_index", 99)
+        self.camera_height = self.get_value(action_data, "camera_height", 0)
+        self.camera_angle = float(self.get_value(action_data, "camera_angle", 0.0))
+        self.af_times = self.get_value(action_data, "number_focus", 0)
+        self.shoe_overturn = self.get_value(action_data, "shoe_upturn", False)
+        self.is_photograph = self.get_value(action_data, "take_picture", True)
+        self.turntable_position = float(
+            self.get_value(action_data, "turntable_position", 0.0)
+        )
+        self.turntable_angle = float(
+            self.get_value(action_data, "turntable_angle", 0.0)
+        )
+        self.delay_time = self.get_value(action_data, "pre_delay", 0.0)
+        self.after_delay_time = self.get_value(action_data, "after_delay", 0.0)
+        self.is_led = self.get_value(action_data, "led_switch", False)
+
+        self.last_photograph_time = None  # 最近一次拍照时间
+        self.goods_art_no = goods_art_no  # 货号
+
+        self.set_other()
+        self.error_info_text = ""  # 错误提示信息
+
+        # self.setParent(parent)
+        self.mcu = mcu
+
+        # if is_show:
+        #     self.parent = parent
+        #     self.setupUi(self)
+        #     self.init()
+        #     self.show()
+
+    def set_other(self):
+        if self.mode_type == "其他配置":
+            self.turntable_position = None
+            self.turntable_angle = None
+            self.delay_time = 0
+            self.after_delay_time = 0
+            self.is_led = False
+
+    def get_value(self, data, key, default=None):
+        if key not in data:
+            data[key] = default
+            return default
+        return data[key]
+
+    def reset(self):
+        self.set_state(state_value=0)
+        self.error_info_text = ""  # 错误提示信息
+
+    def init(self):
+        self.icon_dict = {
+            0: "resources/other_icon/0weikaishi.png",
+            1: "resources/other_icon/1jinxingzhong.png",
+            2: "resources/other_icon/2yijieshu.png",
+            99: "resources/other_icon/11yichang.png",
+        }
+        if self.is_wait:
+            msg = "{}--等待".format(self.action_name)
+            self.sendSocketMessage(msg=msg,device_status=0)
+        else:
+            msg = "{}".format(self.action_name)
+            self.sendSocketMessage(msg=msg, device_status=2)
+        tips_text = "程序:{},对焦:{}".format(1, 1)
+        self.sendSocketMessage(msg=tips_text, device_status=2)
+        self.set_state(state_value=0)
+
+    # def next_step_clicked(self, *args, **kwargs):
+    #     self.windows.event.set()
+
+    def set_state(self, state_value):
+        self.state = state_value
+        # icon = QPixmap(self.icon_dict[self.state])
+        # icon = icon.scaled(
+        #     self.ui_icon.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation
+        # )
+        # self.ui_icon.setPixmap(icon)
+        # if self.state == 0:
+        #     self.ui_retry.setText("")
+        #     self.ui_retry.setEnabled(False)
+        # if self.state == 1:
+        #     self.ui_retry.setText("")
+        #     self.ui_retry.setEnabled(False)
+        # if self.state == 2:
+        #     self.ui_retry.setText("单步")
+        #     self.ui_retry.setEnabled(True)
+        # if self.state == 99:
+        #     self.ui_retry.setText("重试")
+        #     self.ui_retry.setEnabled(True)
+        #     self.ui_action_name.setToolTip(self.error_info_text)
+
+    def check_mcu_move_is_stop(self, re_check=False):
+        self.error_info_text = ""
+        # 发送基础数据信息
+        # self.mcu.to_get_mcu_base_info()
+        _s = time.time()
+        last_num_1 = self.mcu.last_mcu_info_data["num"]
+        self.mcu.cleanAllReceiveData()
+        while 1:
+            if self.mcu.action_state != 1:
+                # 外部终止,停止运行
+                return False
+            cr_time = time.time()
+            print(cr_time - _s, cr_time,_s)
+            if cr_time - _s > 8:
+                self.error_info_text = "MCU检测运动未停止,自动退出"
+                self.set_state(state_value=99)  # 标记异常
+                print("MCU检测运动未停止,自动退出")
+                self.sendSocketMessage(msg=self.error_info_text,device_status=-1)
+                return False
+                # return True
+            # 存在时间间隙,导致误认为所有设备已完成运动
+            if self.mcu.mcu_move_state == 2:
+                return True
+            else:
+                self.mcu.to_get_mcu_base_info()
+                self.mcu.send_all_cmd()
+                time.sleep(0.5)
+                self.mcu.get_basic_info_mcu()
+                # return True
+
+            time.sleep(0.1)
+            # self.mcu.to_get_mcu_base_info()
+
+    def run(self, total_len=5, *args):
+        if total_len == 1:
+            self.mode_type = "其他配置"
+            self.set_other()
+
+        print("1{}  - is run".format(self.action_name))
+        self.set_state(state_value=1)
+        if settings.IS_TEST:
+            self.do_run()
+        else:
+            try:
+                self.do_run()
+            except BaseException as e:
+                # print("p_item 错误:{}".format(e))
+                self.sendSocketMessage(
+                    msg="p_item 错误:{}".format(e), device_status=-1
+                )
+                self.set_state(state_value=99)
+
+        self.set_state(state_value=2)
+        return True
+
+    def run_only_mcu(self, *args):
+        # ============连接MCU 处理步进电机与舵机等
+        if settings.IS_MCU:
+            if self.shoe_overturn:
+                self.mcu.to_deal_device(device_name="overturn_steering")
+                time.sleep(0.1)
+            if self.camera_height is not None:
+                self.mcu.to_device_move(
+                    device_name="camera_high_motor", value=self.camera_height
+                )
+                time.sleep(0.1)
+            if self.camera_angle is not None:
+                self.mcu.to_device_move(
+                    device_name="camera_steering", value=self.camera_angle
+                )
+                time.sleep(0.1)
+            if self.turntable_position is not None:
+                self.mcu.to_device_move(
+                    device_name="turntable_position_motor",
+                    value=self.turntable_position,
+                )
+                time.sleep(0.1)
+            if self.turntable_angle is not None:
+                self.mcu.to_device_move(
+                    device_name="turntable_steering", value=self.turntable_angle
+                )
+                time.sleep(0.1)
+
+    def do_run(self, *args):
+        if not self.goods_art_no:  # and self.action_name != "初始化位置"
+            return False
+        start_time = time.time()
+        # ============连接MCU 处理步进电机与舵机等
+        if settings.IS_MCU:
+            if self.mode_type != "其他配置" and self.check_mcu_move_is_stop() is False:
+                # MCU运动是否有停止检查,设定超时时间
+                return
+            print("{} 检查停止时间1:{}".format(self.action_name, time.time() - start_time))
+            if self.is_led:
+                self.mcu.to_deal_device(device_name="laser_position", value=1)
+            else:
+                self.mcu.to_deal_device(device_name="laser_position", value=0)
+
+            if self.shoe_overturn:
+                self.mcu.to_deal_device(device_name="overturn_steering")
+                # time.sleep(0.1)
+            if self.camera_height is not None:
+                self.mcu.to_device_move(
+                    device_name="camera_high_motor", value=self.camera_height
+                )
+                # time.sleep(0.1)
+            if self.camera_angle is not None:
+                self.mcu.to_device_move(
+                    device_name="camera_steering", value=self.camera_angle
+                )
+                # time.sleep(0.1)
+
+            if self.turntable_position is not None:
+                self.mcu.to_device_move(
+                    device_name="turntable_position_motor",
+                    value=self.turntable_position,
+                )
+                # time.sleep(0.1)
+
+            if self.turntable_angle is not None:
+                self.mcu.to_device_move(
+                    device_name="turntable_steering", value=self.turntable_angle
+                )
+                # time.sleep(0.1)
+
+            # MCU运动是否有停止检查,设定超时时间
+            self.mcu.send_all_cmd()
+            if self.mode_type != "其他配置":
+                time.sleep(1.2)
+                print("二次检查")
+                if self.check_mcu_move_is_stop(re_check=True) is False:
+                    print("MCU检测运动未停止,自动退出,   提前退出")
+                    return
+
+        if self.delay_time:
+            # print("拍照前延时:{}".format(self.delay_time))
+            time.sleep(self.delay_time)
+
+        if self.is_photograph:
+            print("拍照==>", time.time())
+            # print("photograph==================")
+            self.mcu.to_deal_device(device_name="buzzer", times=1)
+            # 用于临时拍照计数
+            is_af = True if self.af_times > 0 else False
+            self.capture_one.photograph(is_af=is_af)
+            self.last_photograph_time = time.time()  # 记录最近一次拍照时间
+        # print("{} 拍照时间:{}".format(self.action_name, time.time() - start_time))
+        print("{}-{}已完成".format(self.mode_type, self.action_name))
+
+        if True:
+            if self.after_delay_time:
+                print("拍照后延时:{}".format(self.after_delay_time))
+                time.sleep(self.after_delay_time)
+        return True
+
+    def rephotograph_one_pic(self, *args):
+        """
+        1、获取最近一张照片
+        2、判断拍照时间距离当前节点的最近拍照时间是否小于3秒
+        3、删除该照片
+        4、重新触发进行拍照,并更新最近拍照时间
+        """
+        if settings.RUNNING_MODE == "普通模式":
+            return
+
+        print("-----100-2--self", self)
+        # 需要删除最近一张照片
+        # if self.last_photograph_time is not None:
+        #     record = self.windows.image_process_data.photo_todo_list
+        #     if record:
+        #         last_record = record[-1]
+        #         if self.goods_art_no == last_record["goods_art_no"]:
+        #             if last_record["is_photo"]:
+        #                 photo_create_time_formate = last_record[
+        #                     "photo_create_time_formate"
+        #                 ]
+        #                 image_path = last_record["image_path"]
+        #                 print(
+        #                     "photo_create_time_formate,self.last_photograph_time",
+        #                     photo_create_time_formate,
+        #                     self.last_photograph_time,
+        #                 )
+        #                 # 如果照片的时间大于触发时间,且小于3秒,则认定是当前触发
+        #                 if (
+        #                     self.last_photograph_time
+        #                     < photo_create_time_formate
+        #                     < self.last_photograph_time
+        #                     + settings.PHOTO_TRANSFER_TIME_INTERVAL
+        #                 ):
+        #                     if os.path.exists(image_path):
+        #                         os.remove(image_path)
+        #                         self.windows.add_goods_images_count(
+        #                             self.goods_art_no, flag=False
+        #                         )
+        #                         if (
+        #                             self.windows.image_process_data.goods_art_no_times_record
+        #                         ):
+        #                             goods_art_no = self.windows.image_process_data.goods_art_no_times_record[
+        #                                 0
+        #                             ][
+        #                                 "goods_art_no"
+        #                             ]
+        #                             if goods_art_no == self.goods_art_no:
+        #                                 self.windows.image_process_data.goods_art_no_times_record[
+        #                                     0
+        #                                 ][
+        #                                     "number"
+        #                                 ] -= 1
+        #                         self.program_refresh_photo_list_sign.emit()
+
+        # if self.is_photograph:
+        #     # 重新拍照
+        #     self.last_photograph_time = time.time()  # 记录最近一次拍照时间
+        #     print("last_photograph_time:", self.last_photograph_time)
+        #     print("photograph==================")
+        #     self.mcu.to_deal_device(device_name="buzzer", times=1)
+        #     # 用于临时拍照计数
+        #     self.windows.add_goods_images_count(self.goods_art_no)
+        #     if self.af_times > 0:
+        #         self.windows.capture_one.photograph(is_af=True)
+        #     else:
+        #         self.windows.capture_one.photograph(is_af=False)
+
+    def do_retry(self, *args, **kwargs):
+        """
+        重试操作,注意事项:
+        1、需要根据当前系统的角度位置,重新进行返回并拍摄
+        :param args:
+        :param kwargs:
+        :return:
+        """
+        # self.program_sign.emit({})
+        # print("do_retry")
+        self.reset()
+        self.mcu.action_state = 1
+        self.run()
+        self.mcu.action_state = 2
+
+    def get_photo_node_name_and_sound(self, photo_take_time):
+        data = {
+            "action_name": self.action_name,
+            "flag": False,
+            "is_sound_play": False,
+        }
+        if self.state == 0:
+            return data
+        if self.last_photograph_time is None:
+            return data
+        # if (
+        #     self.last_photograph_time
+        #     < photo_take_time
+        #     < self.last_photograph_time + settings.PHOTO_TRANSFER_TIME_INTERVAL
+        # ):
+        #     # 认定为当节点拍摄
+        #     # 如果为待用户确认则播放声音
+        #     data["flag"] = True
+        #     if settings.RUNNING_MODE == "待用户确认模式":
+        #         _f = False
+        #         if settings.RUNNING_MODE_DETAIL == "所有节点待确认":
+        #             _f = True
+        #         else:
+        #             print("self.is_need_confirm", self.is_need_confirm)
+        #             if self.is_need_confirm:
+        #                 _f = True
+        #             else:
+        #                 _f = False
+        #         if _f:
+        #             data["is_sound_play"] = True
+        #             print("========is_sound_play===========")
+        #             self.windows.playsound.tips_type = "photo_confirm"
+        #             self.windows.playsound.start()
+        #     return data
+        return data

+ 500 - 0
python/mcu/RemoteControlV2.py

@@ -0,0 +1,500 @@
+# module_remote_control_v2
+
+
+import json
+import time, asyncio
+import settings
+from .SerialIns import SerialIns
+from .BaseClass import BaseClass
+from sockets.connect_manager import ConnectionManager
+from databases import SqlQuery, PhotoRecord, CRUD, insert_photo_records
+from .capture.module_digicam import DigiCam
+from .capture.module_watch_dog import FileEventHandler
+
+
+# from .BlueToothMode import BlueToothMode
+class RemoteControlV2(BaseClass):
+    # sign_data = Signal(dict)
+
+    def __init__(self, bluetooth_ins, websocket_manager: ConnectionManager):
+        # 遥控设备处理--新版遥控器;硅胶按钮
+        super().__init__(websocket_manager)
+        self.msg_type = "blue_tooth"
+        self.websocket_manager = websocket_manager
+        # self.windows = windows
+        self.serial_ins = None
+        self.bluetooth_ins = bluetooth_ins
+
+        self.port_name = ""
+        self.bluetooth_address = ""
+        self.connect_state = False
+        self.is_running = False
+        self.goods_art_no = None
+        # 0 闲置;1进行中;2已完成;
+        self.photo_take_state = 0
+
+    def to_connect_com(self, port_name, is_test=False):
+        if self.connect_state:
+            return
+
+        self.close_connect()
+        time.sleep(0.5)
+        try:
+            # 原值为9600
+            self.serial_ins = SerialIns(port_name=port_name, baud=115200)
+            # self.serial_ins = SerialIns(port_name=port_name, baud=9600)
+            if self.serial_ins.serial_handle:
+                message = {
+                    "_type": "show_info",
+                    "plugins_mode": "remote_control",
+                    "data": "遥控设备V2 打开串口成功",
+                }
+                self.sendSocketMessage(
+                    code=0,
+                    msg="遥控设备V2 打开串口成功",
+                    data=message,
+                    device_status=1,
+                )
+                # print(message)
+                self.connect_state = True
+                message = {
+                    "_type": "remote_control_connect",
+                    "plugins_mode": "remote_control",
+                    "data": port_name,
+                }
+                self.sendSocketMessage(code=0, msg="", data=message, device_status=2)
+                print(message)
+                self.port_name = port_name
+
+                self.is_running = True
+                self.set_voltage_value(0)
+                self.run()
+                return True
+
+            else:
+                message = {
+                    "_type": "show_info",
+                    "plugins_mode": "remote_control",
+                    "data": "遥控设备V2 打开串口失败",
+                }
+                self.sendSocketMessage(
+                    code=1,
+                    msg="遥控设备V2 打开串口失败",
+                    data=message,
+                    device_status=-1,
+                )
+                print(message)
+                self.serial_ins = None
+                self.connect_state = False
+                self.set_voltage_value(None)
+        except:
+            message = {
+                "_type": "show_info",
+                "plugins_mode": "remote_control",
+                "data": "遥控设备V2 打开串口失败",
+            }
+            print(message)
+            self.sendSocketMessage(
+                code=1, msg="遥控设备V2 打开串口失败", data=message, device_status=-1
+            )
+            self.serial_ins = None
+            self.connect_state = False
+            self.set_voltage_value(None)
+            return False
+
+    def to_connect_bluetooth(self, address, is_test=False):
+        print("to_connect_bluetooth", self.connect_state)
+        if self.connect_state:
+            return
+        # if self.bluetooth_ins == None:
+        #     print("bluetooth_ins 未初始化", bluetooth_mode)
+        #     self.bluetooth_ins = BlueToothMode()
+        # else:
+        #     self.bluetooth_ins = bluetooth_mode
+        self.close_connect()
+        if self.bluetooth_ins == None:
+            print("bluetooth_ins 未初始化", self.bluetooth_ins)
+            # self.bluetooth_ins = self.
+        self.bluetooth_address = address
+        message = {
+            "_type": "show_info",
+            "plugins_mode": "remote_control",
+            "data": "遥控设备V2 打开蓝牙成功",
+        }
+        print(message)
+        self.sendSocketMessage(
+            code=0, msg="遥控设备V2 打开蓝牙成功", data=message, device_status=2
+        )
+        self.connect_state = True
+        self.is_running = True
+        self.set_voltage_value(0)
+        if is_test is False:
+            loop = asyncio.get_event_loop()
+            loop.create_task(self.run())
+
+    def close_bluetooth_connect(self):
+        if self.bluetooth_address:
+            print("蓝牙断开")
+            message = {
+                "_type": "show_info",
+                "plugins_mode": "remote_control",
+                "data": "遥控设备V2 蓝牙断开",
+            }
+            print(message)
+            self.sendSocketMessage(
+                code=1, msg="遥控设备V2 蓝牙断开", data=message, device_status=-1
+            )
+            self.close_connect()
+            print("关闭蓝牙")
+
+    def set_voltage_value(self, voltage_value=None, voltage_text=None):
+        device_status = 2
+        if self.is_running:
+            flag = "接收器已连接 {}".format(
+                "蓝牙" if self.bluetooth_address else "串口"
+            )
+            self.sendSocketMessage(
+                code=0, msg="遥控设备V2 打开蓝牙成功", data=None, device_status=2
+            )
+            self.connect_state = True
+            self.is_running = True
+        else:
+            flag = "接收器未连接"
+            device_status = -1
+
+        if voltage_value is None:
+            if voltage_text:
+                print(voltage_text)
+                # 发送电量剩余消息
+                self.sendSocketMessage(msg=voltage_text, device_status=device_status)
+            else:
+                self.sendSocketMessage(msg=flag, device_status=device_status)
+        else:
+            if voltage_value == 0:
+                print(flag)
+                self.sendSocketMessage(msg=flag, device_status=device_status)
+            else:
+                print("电量:{}%".format(voltage_value))
+                self.sendSocketMessage(
+                    msg="电量:{}%".format(voltage_value), device_status=device_status
+                )
+        # print("打印===>", flag)
+
+    def close_connect(self):
+        self.port_name = ""
+        self.bluetooth_address = ""
+        # self.bluetooth_ins = None
+        if self.serial_ins:
+            self.serial_ins.close_serial_port()
+        self.connect_state = False
+
+    def __del__(self):
+        self.close_connect()
+
+    def play_sound(self, tip="sound_tips_3"):
+        self.windows.playsound.tips_type = tip
+        self.windows.playsound.start()
+
+    def handlerAction(self, button_value):
+        """处理拍照动作按键[左 右]"""
+        control_program = "执行左脚程序" if button_value == 1 else "执行右脚程序"
+        if self.goods_art_no == None or self.goods_art_no == "":
+            input_data = {
+                "data": {
+                    "action": control_program,
+                    "goods_art_no": "",
+                },
+                "type": "run_mcu",
+            }
+            self.msg_type = "blue_tooth_scan"
+            self.sendSocketMessage(
+                code=0,
+                msg=f"准备执行[{control_program}]",
+                data=input_data,
+                device_status=2,
+            )
+            self.msg_type = "blue_tooth"
+            return
+        self.photo_take_state = 1
+        input_data = {
+            "data": {
+                "action": control_program,
+                "goods_art_no": self.goods_art_no,
+            },
+            "type": "run_mcu",
+        }
+        self.msg_type = "blue_tooth_scan"
+        self.sendSocketMessage(
+            code=0,
+            msg=f"准备执行[{control_program}]",
+            data=input_data,
+            device_status=2,
+        )
+        self.goods_art_no = None
+        self.msg_type = "blue_tooth"
+        self.photo_take_state = 2
+
+    async def handlerTakePhoto(self):
+        """处理单独拍照"""
+        await asyncio.sleep(0.1)
+        print("开始单拍0")
+        session = SqlQuery()
+        crud = CRUD(PhotoRecord)
+        record = crud.read(session=session, order_by="id", ascending=False)
+        print("开始单拍0-读取数据库")
+        if record == None:
+            # 发送失败消息
+            self.sendSocketMessage(
+                code=1,
+                msg="单拍失败,请先输入货号或扫码进行组合拍摄",
+                data=None,
+                device_status=2,
+            )
+        else:
+            print("开始单拍1")
+            if record.image_index == 19:
+                self.sendSocketMessage(
+                    code=1,
+                    msg="单拍失败,单个货号最多允许拍摄20张产品图",
+                    data=None,
+                    device_status=2,
+                )
+                return
+            image_index = record.image_index + 1
+            self.photo_take_state = 1
+            insert_photo_records(
+                record.image_deal_mode, record.goods_art_no, image_index
+            )
+            print("开始单拍1-插入数据")
+            capture_one = DigiCam()
+            try:
+                captrure_folder_path = capture_one.getCaptureFolderPath()
+                watch_dog = FileEventHandler()
+                watch_dog.goods_art_no = record.goods_art_no
+                watch_dog.image_index = image_index
+                watch_dog.start_observer(captrure_folder_path)
+                print("开始单拍1-检查相机")
+                camera_is_connect = capture_one.checkCameraConnect()
+                if camera_is_connect is not True:
+                    self.sendSocketMessage(1, "相机未连接,请检查", device_status=-1)
+                    return
+                capture_one.run_capture_action("Capture")
+                print("开始单拍1-完成拍照")
+                time.sleep(1)
+                self.msg_type = "photo_take"
+                self.sendSocketMessage(
+                    code=0,
+                    msg="{} 执行完成~".format(
+                        "执行右脚程序"
+                        if record.image_deal_mode == 1
+                        else "执行左脚程序"
+                    ),
+                    data={"goods_art_no": record.goods_art_no},
+                    device_status=2,
+                )
+                self.msg_type = "blue_tooth"
+            except Exception as e:
+                self.sendSocketMessage(1, "digicam未初始化,请检查", device_status=-1)
+        self.photo_take_state = 0
+
+    async def analysis_received_data(self):
+        if not self.connect_state:
+            return
+        await asyncio.sleep(0.01)
+        if self.bluetooth_address:
+            receive_data = self.bluetooth_ins.read_cmd_one(
+                address=self.bluetooth_address
+            )
+            # print("received data", 1)
+        else:
+            receive_data = self.serial_ins.read_cmd(out_time=1, check=None)
+        # print("received data", 2)
+        # print("self.bluetooth_ins", receive_data)
+        if receive_data is False:
+            self.connect_state = False
+            return False
+
+        if not receive_data:
+            return
+        if self.photo_take_state != 0:
+            print("正在拍照", self.photo_take_state)
+            return
+        receive_data_parser = receive_data[0]
+        # 数据 结构 command,按命令解析
+        if receive_data_parser == 1:
+            # self.play_sound("get_qr_code")
+            # 扫码数据
+            bar_code = receive_data[1:].decode()
+            bar_code = bar_code.replace("\r", "")
+            bar_code = bar_code.replace("\n", "")
+            self.goods_art_no = bar_code
+            message = {"_type": 0, "plugins_mode": "remote_control", "data": bar_code}
+            print(message)
+            self.sendSocketMessage(code=0, msg="", data=message, device_status=2)
+            return
+        if receive_data_parser == 9:
+            # 播放声音
+            button_value = receive_data[1]
+            data = {"button_value": button_value}
+            message = {"_type": 9, "plugins_mode": "remote_control", "data": data}
+            print(message)
+            if button_value in [1, 2]:
+                # 扫描货号
+                if self.photo_take_state != 0:
+                    self.sendSocketMessage(1, "前置拍照未完成,请稍后", device_status=-1)
+                    return
+                print("收到货号信息", self.goods_art_no)
+                self.handlerAction(button_value)
+                self.photo_take_state = 0
+            if button_value in [3]:
+                # 处理遥控器单拍
+                self.msg_type = "handler_take_picture"
+                # 0 闲置;1进行中;2已完成;
+                _data = {"type": self.msg_type,"data":None}
+                self.sendSocketMessage(0, "处理单拍消息", data=_data, device_status=-1)
+                self.msg_type = "blue_tooth"
+            self.sendSocketMessage(code=0, msg="", data=message, device_status=2)
+            if settings.IS_DEBUG:
+                print("收到按键", button_value)
+            return
+
+        if receive_data_parser == 10:
+            voltage_value = receive_data[1]
+            self.set_voltage_value(voltage_value)
+            if settings.IS_TEST:
+                print("遥控器V2电量:{}".format(voltage_value))
+            return
+
+        # 使用Max17048 查看电量
+        if receive_data_parser == 12:
+            chg_status = self.get_data_from_receive_data(
+                receive_data=receive_data, start=1, len_data=1
+            )
+            soc_percentage = self.get_data_from_receive_data(
+                receive_data=receive_data, start=2, len_data=4
+            )
+            soc_percentage = soc_percentage / 100
+
+            voltage = self.get_data_from_receive_data(
+                receive_data=receive_data, start=6, len_data=4
+            )
+            voltage = voltage / 100
+
+            current = self.get_data_from_receive_data(
+                receive_data=receive_data, start=10, len_data=4
+            )
+            current = current / 10000
+
+            temperature = self.get_data_from_receive_data(
+                receive_data=receive_data, start=14, len_data=4
+            )
+            temperature = temperature / 100
+
+            adjusted_soc = self.get_data_from_receive_data(
+                receive_data=receive_data, start=18, len_data=4
+            )
+            adjusted_soc = adjusted_soc / 100
+
+            soft_vision = self.get_data_from_receive_data(
+                receive_data=receive_data, start=22, len_data=1
+            )
+            full_status = self.get_data_from_receive_data(
+                receive_data=receive_data, start=23, len_data=1
+            )
+
+            # print("is_charging:{}".format(chg_status))
+            # print("Battery SOC: {:.2f}%".format(soc_percentage))
+            # print("Battery Voltage: {:.3f}V".format(voltage))
+            # print("Average Current: {:.3f}A".format(current))
+            # print("Chip Temperature: {:.1f}°C".format(temperature))
+            # print("adjusted Battery soc: {:.2f}%".format(adjusted_soc))
+            # print("soft_vision:{}".format(soft_vision))
+            # print("chg_status:{}  full_status:{}".format(chg_status, full_status))
+            if chg_status:
+                t1 = "充电中"
+            else:
+                t1 = ""
+
+            if full_status:
+                t2 = "已充满"
+            else:
+                t2 = ""
+
+            self.set_voltage_value(
+                voltage_text="遥控器:{:.1f}%  {:.2f}V {}{}".format(
+                    min(adjusted_soc / 92.5 * 100, 100), voltage, t1, t2
+                )
+            )
+            return
+
+        if receive_data[0] == 111:
+            value = (
+                receive_data[1] << 24
+                | receive_data[2] << 16
+                | receive_data[3] << 8
+                | receive_data[4]
+            )
+            print("遥控器-测试   value:{}".format(value))
+            # self.windows.show_label.setText("--------{}".format(value))
+        if receive_data[0] == 112:
+            bar_code = receive_data[1:].decode()
+            bar_code = bar_code.replace("\r", "")
+            bar_code = bar_code.replace("\n", "")
+            print("read bar_code {}".format(bar_code))
+
+    # 通用串口数据解析器
+    def get_data_from_receive_data(
+        self, receive_data, start, len_data, data_magnification=1
+    ):
+        # data_magnification 数据放大倍数,或缩小倍数,默认为1
+        try:
+            if len_data == 1:
+                data = receive_data[start]
+                return data * data_magnification
+            elif len_data == 2:
+                data = receive_data[start] << 8 | receive_data[start + 1]
+                return data * data_magnification
+            elif len_data == 4:
+                data = (
+                    receive_data[start] << 24
+                    | receive_data[start + 1] << 16
+                    | receive_data[start + 2] << 8
+                    | receive_data[start + 3]
+                )
+                return data * data_magnification
+            return None
+        except:
+            return None
+
+    async def run(self):
+        # self.show_info.emit("未连接")
+        # self.data_command_sign.emit(data)
+        self.is_running = True
+        print("Running")
+        while 1:
+            await asyncio.sleep(0.01)
+            if not self.connect_state:
+                message = {
+                    "_type": "show_info",
+                    "plugins_mode": "remote_control",
+                    "data": "遥控设备V2 未连接",
+                }
+                print(message)
+                self.sendSocketMessage(
+                    code=1, msg="遥控设备V2 未连接", data=message, device_status=-1
+                )
+                break
+            await self.analysis_received_data()
+
+        self.is_running = False
+        if not self.connect_state:
+            message = {
+                "_type": "show_info",
+                "plugins_mode": "remote_control",
+                "data": "遥控设备V2 未连接",
+            }
+            print(message)
+            self.sendSocketMessage(
+                code=1, msg="遥控设备V2 未连接", data=message, device_status=-1
+            )
+            self.set_voltage_value(None)

+ 321 - 0
python/mcu/SerialIns.py

@@ -0,0 +1,321 @@
+#!usr/bin/python3
+import serial.tools.list_ports
+import time
+
+
+# https://blog.csdn.net/weixin_44289254/article/details/121583562
+
+
+class SerialIns(object):
+    def __init__(self, port_name=None, baud=9600, timeout=0.01):
+        self.port_name = port_name
+        self.baud = baud
+        self.serial_handle = None
+        # self.serial_handle = self.check_connect()
+        try:
+            self.serial_handle = serial.Serial(
+                port=self.port_name, baudrate=self.baud, timeout=timeout
+            )
+            print("{}打开成功".format(self.port_name))
+        except Exception as e:
+            print(e)
+            self.serial_handle = None
+            print("{}打开失败".format(self.port_name))
+
+        self.receive_data = b""
+        # self.receive_data = bytearray(b'\x00\UU\x02d\x00\x9b\x9b')
+
+    def read_line(self):
+        c = self.serial_handle.inWaiting()
+        if c:
+            time.sleep(0.02)
+            # 确保缓存写入完成
+            if c == self.serial_handle.inWaiting():
+                return self.serial_handle.readline()
+        return None
+
+    def read_all(self):
+        return self.serial_handle.read_all()
+
+    def check_connect(self):
+        if not self.port_name:
+            self.scan_serial_port()
+            if self.port_name:
+                self.serial_handle = serial.Serial(self.port_name, self.baud)
+        else:
+            try:
+                # if self.scan_serial_port(self.port_name):
+                self.serial_handle = serial.Serial(self.port_name, self.baud)
+                print("{}打开成功".format(self.port_name))
+            except:
+                print("{}打开失败".format(self.port_name))
+
+        return self.serial_handle
+
+    def clearn_flush(self):  # 清空接收缓存
+        if self.serial_handle:
+            self.serial_handle.flushInput()
+            self.receive_data = b""
+
+    def write_cmd(self, data: list):
+        if self.serial_handle:
+            # data = [(0xff & par1), (0xff & (par1 >> 8))]
+            # self.clearn_flush()
+            buf = bytearray(b"")
+            buf.extend([0x55, 0x55, (0xFF & len(data))])
+            buf.extend(data)
+            buf.extend([0xFF & ~sum(data)])
+            # 55 55 02 5a 01 a4
+            print("send buf  {}".format(self.change_hex_to_int(buf)))
+            try:
+                self.serial_handle.write(buf)
+                return True
+            except:
+                self.serial_handle = None
+                _recv_data = b""
+                return False
+
+    def read_cmd_out_time(self, scan_interval=0.1, out_time=1):
+        if self.serial_handle:
+            """
+            获取数据,设计超时机制
+            :param scan_interval:
+            :param out_time:
+            :return:
+            """
+            n = 0
+            while 1:
+                n += 1
+                time.sleep(scan_interval)
+                if out_time <= n * scan_interval:
+                    return False
+                receive_data = self.read_cmd()
+                if receive_data:
+                    return receive_data
+        return False
+
+    def read_cmd1(self, out_time=1, check=None):
+        s = time.time()
+        while 1:
+            try:
+                _recv_data = self.serial_handle.read_all()  # 读取接收到的数据
+            except:
+                self.serial_handle = None
+                _recv_data = b""
+                return False
+
+            self.receive_data += _recv_data  # 写入所有缓存数据
+            # print(self.receive_data)
+            if not self.receive_data or len(self.receive_data) < 4:
+                return
+            if self.receive_data[0] == 0x55 and self.receive_data[1] == 0x55:
+                # print("read ori ", self.change_hex_to_int(self.receive_data))
+                data_len = self.receive_data[2]
+                if len(self.receive_data) < data_len + 4:
+                    # 此处需要超时机制
+                    print("1数据长度不够,等待下次读取")
+                    # 超时退出
+                    if time.time() - s > out_time:
+                        time.sleep(0.01)
+                        return
+                _data = self.receive_data[3 : data_len + 4]
+                # 更新缓存区
+                self.receive_data = self.receive_data[data_len + 4 :]
+                # 校验数据
+                if check is None:
+                    if 0xFF & ~sum(_data[:-1]) == _data[-1]:
+                        # print("data:", self.change_hex_to_int(data[:-1]))
+                        return _data[:-1]
+                    else:
+                        print("数据异常,丢弃")
+                        return
+                else:
+                    if _data[-1] == check:
+                        return _data[:-1]
+                    else:
+                        print("数据异常,丢弃")
+                        return
+            else:
+                # print("起始位不是 55 55 进行移除", self.receive_data[0])
+                # 起始位不是 55 55 进行移除
+                index = 0
+                for index, r_data in enumerate(self.receive_data):
+                    if r_data == 0x55:
+                        break
+
+                index += 1
+                if index >= len(self.receive_data):
+                    self.receive_data = b""
+                else:
+                    self.receive_data = self.receive_data[index - 1 :]
+
+    def read_cmd111(self, out_time=1, check=None):
+        while 1:
+            try:
+                _recv_data = self.serial_handle.read_all()  # 读取接收到的数据
+            except BaseException as e:
+                print("串口接收报错", e)
+                self.serial_handle = None
+                _recv_data = b""
+                return False
+
+            # print("read_cmd", _recv_data)
+            if not _recv_data or len(_recv_data) < 4:
+                # print("数据长度不够")
+                return
+
+            print("2   _recv_data", self.change_hex_to_int(_recv_data))
+            if _recv_data[0] == 0x55 and _recv_data[1] == 0x55:
+                # print("read ori ", self.change_hex_to_int(self.receive_data))
+                data_len = _recv_data[2]
+                if len(_recv_data) < data_len + 4:
+                    # 此处需要超时机制
+                    print("2数据长度不够,等待下次读取")
+                    # if time.time() - s > out_time:
+                    #     time.sleep(0.01)
+                    #     return
+                    return
+
+                _data = _recv_data[3 : data_len + 4]
+                # 校验数据
+                if check is None:
+                    if 0xFF & ~sum(_data[:-1]) == _data[-1]:
+                        # print("data:", self.change_hex_to_int(data[:-1]))
+                        return _data[:-1]
+                    else:
+                        print("数据异常,丢弃")
+                        return
+                else:
+                    if _data[-1] == check:
+                        return _data[:-1]
+                    else:
+                        print("数据异常,丢弃")
+                        return
+            else:
+                # 起始位不是 55 55 进行移除
+                while self.receive_data:
+                    if len(self.receive_data) == 1:
+                        if self.receive_data[0] == 0x55:
+                            break
+                        else:
+                            self.receive_data = b""
+                    else:
+                        if (
+                            self.receive_data[0] == 0x55
+                            and self.receive_data[1] == 0x55
+                        ):
+                            break
+                        else:
+                            self.receive_data = self.receive_data[1:]
+
+    def read_cmd(self, out_time=1, check=None, out_time_n=5):
+        n = 0
+        while 1:
+            try:
+                read_d = self.serial_handle.read_all()  # 读取接收到的数据
+                self.receive_data += read_d
+            except BaseException as e:
+                print("串口接收报错", e)
+                self.serial_handle = None
+                return False
+
+            if len(self.receive_data) < 4:
+                break
+
+            if self.receive_data[0] == 0x55 and self.receive_data[1] == 0x55:
+                # print("read ori ", self.change_hex_to_int(self.receive_data))
+                data_len = self.receive_data[2]
+                if len(self.receive_data) < data_len + 4:
+                    # 此处需要超时机制
+                    # print("数据长度不够,等待下次读取")
+                    # 超时退出
+                    # if not self.serial_handle.txdone():
+                    #     return None
+                    # n += 1
+                    # if n > out_time_n:
+                    #     return None
+                    # time.sleep(0.01)
+                    continue
+                _data = self.receive_data[3 : data_len + 4]
+                # 更新缓存区
+                self.receive_data = self.receive_data[data_len + 4 :]
+                # 校验数据
+                if 0xFF & ~sum(_data[:-1]) == _data[-1]:
+                    # print("receive_data:", self.change_hex_to_int(self.receive_data[:-1]))
+                    return _data[:-1]
+                else:
+                    return None
+            else:
+                # print("起始位不是 55 55 进行移除", self.receive_data[0])
+                # 起始位不是 55 55 进行移除
+                while self.receive_data:
+                    if len(self.receive_data) == 1:
+                        if self.receive_data[0] == 0x55:
+                            break
+                        else:
+                            self.receive_data = b""
+                    else:
+                        if (
+                            self.receive_data[0] == 0x55
+                            and self.receive_data[1] == 0x55
+                        ):
+                            break
+                        else:
+                            self.receive_data = self.receive_data[1:]
+
+    def change_hex_to_int(self, _bytearray):
+        return " ".join([hex(x)[2:].zfill(2) for x in _bytearray])
+
+    def scan_serial_port(self, port_name=None):
+        plist = list(serial.tools.list_ports.comports())
+        if len(plist) <= 0:
+            return None
+        if port_name:
+            for i in plist:
+                # print("port", i)
+                if i.name == port_name:
+                    return i
+            return None
+        else:
+            for i in plist:
+                print("串口列表:", i.description)
+                print("串口列表:", i.name)
+                if "CH340" in i.description:
+                    print("CH340:", i.name)
+                    self.port_name = i.name
+                    print("----------", i)
+            return plist
+
+    def open_serial_port(self):
+        if self.serial_handle:
+            if not s.check_connect():
+                self.serial_handle.open()
+        return True
+
+    def close_serial_port(self):
+        if self.serial_handle:
+            self.serial_handle.close()
+            print("{}串口已关闭".format(self.port_name))
+            self.serial_handle = None
+
+    def __del__(self):
+        self.close_serial_port()
+
+if __name__ == "__main__":
+    s = SerialIns(port_name="COM5", baud=115200, timeout=0.1)
+    s.scan_serial_port()
+    s.clearn_flush()
+    print("-" * 30)
+    for i in range(2):
+        data = [2,99,1]
+        
+        s.write_cmd(data)
+
+        time.sleep(0.1)
+        data = s.read_cmd()
+        # print("data-->", data)
+        if data:
+            print("data--> {}".format(s.change_hex_to_int(data)))
+        time.sleep(1)
+    print("-" * 30)
+    s.close_serial_port()

+ 0 - 0
python/mcu/__init__.py


+ 0 - 0
python/mcu/capture/__init__.py


+ 199 - 0
python/mcu/capture/capture_basic_mode.py

@@ -0,0 +1,199 @@
+import time
+import settings
+from sys import platform as sys_platform
+from utils.utils_func import httpGetHandler
+from PIL import Image
+from io import BytesIO
+import requests
+
+if settings.IS_TEST:
+    import piexif
+    from PIL import Image
+    import threading
+
+
+class CaptureBasic():
+    # program_sign = Signal(dict)0
+    base_host = "http://localhost:5513"
+    _get_image_index = 0
+    _get_folder_index = 0
+    is_first_init = 0
+
+    def __init__(self):
+        # self.window = window
+        # 状态 1 进行中 2 已停止
+        self.state = 2
+        # 总拍照数量
+        self.total_num = 0
+        # 是否获取照片
+        self.is_get_photo = False
+        # self.keyboard = Controller()
+        self.hwnd = None
+    # def getPreviewImage(self)->Image:
+    #     '''获取预览图'''
+    #     yzmdata = requests.get(self.base_host + "/preview.jpg")
+    #     tempIm = BytesIO(yzmdata.content)
+    #     im = Image.open(tempIm)
+    #     return im
+
+    def auto_focus(self):
+        # 执行拍照
+        time.sleep(0.2)
+        self.run_capture_action("LiveView_Focus")
+
+    def getCaptureFolderPath(self):
+        """获取预览图"""
+        response = httpGetHandler(
+            url=self.base_host,
+            params={"slc": "get", "param1": "session.folder"},
+            headers={},
+            )
+        return response.text
+
+    def checkCameraConnect(self):
+        """检查相机是否连接"""
+        response = httpGetHandler(
+            url=self.base_host,
+            params={"slc": "get", "param1": "iso"},
+            headers={},
+        )
+        if response.text == "未将对象引用设置到对象的实例。":
+            return False
+        return True
+
+    def run_capture_action(self,command):
+        '''执行capture动作'''
+        response = httpGetHandler(url=self.base_host, params={"CMD":command},headers={})
+        return response
+    # def photograph(self):
+    #     """
+    #     Args:
+    #         data:
+    #         info_show: 信息输出
+    #         image_deal_mode:0 表示左脚,1表示右脚
+    #     Returns:
+    #     """
+    #     print("142----------------------------photograph")
+    #     # 执行拍照
+    #     if not self.hwnd:
+    #         return False, {"flag": "{} 未打开".format(settings.CaptureSoftwareName)}
+    #     if self.state == 1:
+    #         print("当前有未完成的任务")
+    #         return False, {"flag": "当前有未完成的任务"}
+    #     print("开始拍照")
+    #     self.state = 1
+    #     if not self.set_foreground_window():
+    #         return False, {"flag": "no {}".format(settings.CaptureSoftwareName)}
+    #     time.sleep(0.1)
+    #     # 自动对焦
+    #     self.auto_focus()
+    #     # self.send_win32_key(self.hwnd, key_1=win32con.VK_CONTROL, key_2=win32con.VK_F2)
+
+    #     if settings.IS_TEST:
+    #         # print("56-----测试拍照")
+    #         threading.Thread(target=self.test_add_photo, args=()).start()
+    #     # self.program_sign.emit({"type": "add_to_photo_todo_list",
+    #     #                         "data": data})
+    #     time.sleep(float(settings.PhotographSeconds))
+
+    # def test_add_photo(self):
+    #     def copy_file(srcfile, dstpath, dstfile):  # 复制函数
+    #         if not os.path.isfile(srcfile):
+    #             # print("%s not exist!" % (srcfile))
+    #             return
+    #         else:
+    #             if int(os.path.getsize(srcfile) / 1024) < 10:
+    #                 return
+    #             # fpath, fname = os.path.split(srcfile)  # 分离文件名和路径
+    #             if not os.path.exists(dstpath):
+    #                 os.makedirs(dstpath)  # 创建路径
+    #             shutil.copy(srcfile, dstfile)  # 复制文件
+
+    #     print("模拟拍照  test_add_photo")
+    #     time.sleep(0.9)
+    #     s = time.time()
+    #     source_path = r"{}\PhotoImageDir\test_source".format(os.getcwd())
+    #     check_path(source_path)
+
+    #     # 整理所有测试源照片
+    #     images_path_list = []
+    #     folder_list = get_folder(source_path)
+    #     for folder_dict in folder_list:
+    #         folder_path = folder_dict["folder_path"]
+    #         _pics = [x["file_path"] for x in get_images(folder_path)]
+    #         if _pics:
+    #             images_path_list.append(_pics)
+
+    #     if len(images_path_list) <= CaptureBasic._get_folder_index:
+    #         CaptureBasic._get_folder_index = 0
+    #         CaptureBasic._get_image_index = 0
+    #     srcfile = images_path_list[CaptureBasic._get_folder_index][CaptureBasic._get_image_index]
+
+    #     # 新文件夹
+    #     CaptureBasic._get_image_index += 1
+    #     if len(images_path_list[CaptureBasic._get_folder_index]) <= CaptureBasic._get_image_index:
+    #         CaptureBasic._get_folder_index += 1
+    #         CaptureBasic._get_image_index = 0
+
+    #     # print("pic:", srcfile)
+    #     e = os.path.splitext(os.path.split(srcfile)[1])[1]
+
+    #     # 修改图片时间
+    #     if e != ".CR2":
+    #         _time = time.strftime("%Y:%m:%d %H:%M:%S", time.localtime(time.time()))
+    #         exif_ifd = {piexif.ExifIFD.DateTimeOriginal: _time}
+    #         exif_dict = {"Exif": exif_ifd}
+    #         exif_bytes = piexif.dump(exif_dict)
+    #         im = Image.open(srcfile)
+    #         im.save(srcfile, exif=exif_bytes)
+
+    #     dst_path = settings.PhotoOutputDir
+    #     # dst_path = "{}/data".format(os.getcwd())
+    #     e = os.path.splitext(srcfile)[-1]
+    #     new_name = "~{}{}".format(int(time.time() * 10), e)
+    #     dst_file = "{}/{}".format(dst_path, new_name)
+    #     print("测试数据   dst_file", dst_file)
+    #     copy_file(srcfile, dst_path, dst_file)
+    #     # print("修改图片耗时:{}".format(time.time() - s))
+
+    def last_images(self):
+        if not self.hwnd:
+            return
+        # win32gui.SetForegroundWindow(self.hwnd)
+        # self.keyboard.press(Key.ctrl)
+        # self.keyboard.press(Key.left)
+        # self.keyboard.release(Key.left)
+        # self.keyboard.release(Key.ctrl)
+
+    def next_images(self):
+        # 执行拍照
+        if not self.hwnd:
+            return
+        # win32gui.SetForegroundWindow(self.hwnd)
+        # self.keyboard.press(Key.ctrl)
+        # self.keyboard.press(Key.right)
+        # self.keyboard.release(Key.right)
+        # self.keyboard.release(Key.ctrl)
+
+    def test_1(self):
+        if not self.hwnd:
+            return
+        # 执行照片全选与导出
+        # win32gui.SetForegroundWindow(self.hwnd)
+        # self.keyboard.press(Key.ctrl)
+        # self.keyboard.press("a")
+        # self.keyboard.release("a")
+        # self.keyboard.release(Key.ctrl)
+        # time.sleep(1)
+        # # 照片导出
+        # self.keyboard.press(Key.ctrl)
+        # self.keyboard.press("d")
+        # self.keyboard.release("d")
+        # self.keyboard.release(Key.ctrl)
+if __name__ == "__main__":
+    cap = CaptureBasic()
+    # cap.auto_focus()
+    # cap.auto_focus()
+    # cap.run_capture_action("Capture")
+    camera_conn = cap.checkCameraConnect()
+    print("cccc", camera_conn)

+ 50 - 0
python/mcu/capture/module_digicam.py

@@ -0,0 +1,50 @@
+"""
+使用digiCamControl进行处理
+1、使用cmd命令检查相机是否有链接电脑
+2、句柄检查digiCamControl软件是否有打开
+3、注意没有单独对焦的命令,不对焦拍摄的话取另外一个控制命令
+4、确保输出的内容在指定文件夹
+
+"""
+import os.path
+import subprocess
+import time
+import settings
+# import win32api, win32gui, win32con
+# import win32process
+# from pynput.keyboard import Key, Controller
+from .capture_basic_mode import CaptureBasic
+import threading
+from .module_watch_dog import FileEventHandler
+
+# from win32gui import EnumWindows, GetWindowText
+
+
+class DigiCam(CaptureBasic):
+
+    def __init__(self):
+        super().__init__()
+        # self.get_capture_one_hwnd()
+        # self.install_path = settings.DigicamInstallPath
+        self.is_first_init = 0
+        # if self.install_path:
+        #     threading.Thread(target=self.check_and_open_software, args=()).start()
+    # 拍照
+    def photograph(self, is_af=True):
+        """
+        Args:
+            data:
+            info_show: 信息输出
+            image_deal_mode:0 表示左脚,1表示右脚
+        Returns:
+        """
+        print("55----------------------DigiCam------photograph")
+        # 执行拍照
+        time.sleep(0.1)
+        # 自动对焦
+        if is_af:
+            self.auto_focus()
+            self.run_capture_action("Capture")
+        else:
+            self.run_capture_action("Capture")
+        time.sleep(float(settings.PhotographSeconds))

+ 199 - 0
python/mcu/capture/module_watch_dog.py

@@ -0,0 +1,199 @@
+from watchdog.events import FileSystemEventHandler
+from watchdog.observers import Observer
+import settings
+import time
+import os
+from utils.utils_func import get_folder, check_path
+import datetime
+from utils.SingletonType import SingletonType
+from databases import CRUD,SqlQuery,PhotoRecord
+from databases import CRUD, SqlQuery, PhotoRecord
+def updateImageRaw(time_str, image_path, goods_art_no, image_index):
+    session = SqlQuery()
+    crud = CRUD(PhotoRecord)
+    res = crud.read(
+        session,
+        conditions={
+            "image_path": None,
+            "goods_art_no": goods_art_no,
+            "image_index": image_index,
+        },
+    )
+    if res:
+        # 格式化为年月日时分
+        update = {
+            "image_path": image_path,
+            "photo_create_time": time_str,
+        }
+        crud.update(
+            session,
+            res.id,
+            **update,
+        )
+
+
+class FileEventHandler(FileSystemEventHandler, metaclass=SingletonType):
+    instance = None
+    init_flag = None
+
+    def __init__(self):
+        # if self.init_flag:
+        #     return
+        # else:
+        #     self.init_flag = True
+        self.goods_art_no = None
+        self.image_index = -1
+        super().__init__()
+        # self.window = window
+        FileSystemEventHandler.__init__(self)
+        self.receive_photo_data = []
+        # 以下为测试用途
+        self._get_image_index = -1
+        self.observer = None
+        self.last_create_time = datetime.datetime.now()
+
+    def start_observer(self,path):
+        if self.observer != None:
+            return
+        print("图片保存目录:", path)
+        if path == None or path == "":
+            return
+        self.observer = Observer()
+
+        watch_path = self.check_and_get_real_dir(path)
+        if watch_path:
+            self.observer.schedule(self, watch_path, recursive=True)  # recursive 遍历目录
+            self.observer.start()
+            print("开启开门狗目录监听")
+        else:
+            print("路径错误不存在")
+
+    def check_and_get_real_dir(self, watch_path):
+        """
+        逻辑:
+        1、检查当前路径如包含Originals 则定位到对应Originals的父级目录
+        2、如没有Originals,则认定为一个普通目录(且子文件夹没有Originals),否则定位到父级目录
+        
+        """
+        if not os.path.exists(watch_path):
+            return None
+
+        if "Originals" in watch_path:
+            root_path = watch_path.split("Originals", 1)[0]
+        else:
+            if "Originals" in [x["folder_name"] for x in get_folder(watch_path)]:
+                root_path = watch_path
+            else:
+                return watch_path
+
+        # 检查并创建日期
+        now = datetime.datetime.now()
+        year = now.year
+        month = now.month
+        day = now.day
+        path = r"{root_path}\Originals\{year}\{month}\{day}".format(root_path=root_path, year=year, month=month, day=day)
+        check_path(path)
+        print("watch_path:",path)
+        return path
+
+    def on_moved(self, event):
+        # if event.is_directory:
+        #     print("directory moved from {0} to {1}".format(event.src_path, event.dest_path))
+        # else:
+        #     print("file moved from {0} to {1}".format(event.src_path, event.dest_path))
+        #     if os.path.split(event.dest_path)[0] == settings.PhotoOutputDir:
+        #         print("1111111")
+        pass
+    def updatePhotoRecord(self):
+        session = SqlQuery()
+        crud = CRUD(PhotoRecord)
+        crud.read()
+    def on_created(self, event):
+        if not event.is_directory:
+            file_path = event.src_path
+            print("file created:{0}".format(file_path))
+            self.receive_photo_data.append(file_path)
+            # self.get_photo_info(file_path)
+            # self.window.data_sign.emit({"_type": "photo_number_music_play"})
+
+            try:
+                take_time = time.time()
+                self.send_log("获取文件file_path:{}".format(file_path))
+                create_time = datetime.datetime.fromtimestamp(os.path.getctime(file_path))
+                # print("获取文件create_time:{}".format(create_time))
+                self.get_photo_info(raw_path=file_path, create_time=create_time, take_time=take_time)
+                self.send_log("获取文件create_time:{}".format(create_time))
+                if self.goods_art_no == None:
+                    print("货号不存在,监听不写入")
+                    return
+                if file_path == None:
+                    print("file_path不存在,监听不写入")
+                    return
+                updateImageRaw(create_time, file_path, self.goods_art_no,self.image_index)
+            except BaseException as e:
+                print("获取文件create_time失败", e)
+                self.send_log("获取文件处理失败{}".format(e))
+
+    def send_log(self, text):
+        print(text)
+
+    def get_photo_info(self, raw_path, create_time, take_time):
+        # 看门狗监听到系统有图片生成后,自动进行图片对应
+        f_path = os.path.split(raw_path)[0]
+
+        print("raw_path:", raw_path)
+        # take_time = time.time()  # 创建图片时间,默认为拍照时间
+        time.sleep(0.2)  # 等待原始图片文件写入
+
+        # 查找真实的图片路径名称
+        f = False
+        if f:
+            if not os.path.exists(raw_path):
+                print("不存在", raw_path)
+                for file in os.listdir(f_path):
+                    file_path = os.path.join(f_path, file)
+                    # 判断是否为文件
+                    if os.path.isfile(file_path):
+                        # 获取文件创建时间
+                        create_time = datetime.datetime.fromtimestamp(os.path.getctime(file_path))
+                        # 更新最早时间
+                        if create_time > self.last_create_time:
+                            self.last_create_time = create_time
+                            raw_path = file_path
+
+        # last_file_size = os.path.getsize(raw_path)
+        # k = 30
+        # # 检查文件是否有写入完成
+        # flag = False
+        # while k:
+        #     k -= 1
+        #     if k == 0:
+        #         break
+        #     _file_size = os.path.getsize(raw_path)
+        #     if last_file_size == _file_size:
+        #         flag = True
+        #         break
+        #     else:
+        #         last_file_size = _file_size
+        #     time.sleep(0.1)
+        # if not flag:
+        #     return
+        # 调用父程序,执行报错图片命令
+        # self.window.show_img_on_photo_todo_list_sign(raw_path=raw_path, take_time=take_time)
+
+    def stop(self):
+        # 结束监听
+        if self.observer is not None:
+            self.observer.stop()
+            print("结束监听")
+            del self.observer
+
+    def __new__(cls, *args, **kwargs):
+        """如果当前没有实例时,调用父类__new__方法,生成示例,有则返回保存的内存地址。"""
+        if not cls.instance:
+            cls.instance = super().__new__(cls)
+        return cls.instance
+
+    def __del__(self):
+        self.stop()
+        print("结束监听,进程关闭")

+ 80 - 0
python/middleware.py

@@ -0,0 +1,80 @@
+from typing import Union, Optional
+from fastapi import FastAPI, Request, Body, Form, Query
+from fastapi.websockets import WebSocket, WebSocketDisconnect
+from fastapi.exceptions import RequestValidationError
+from fastapi.responses import JSONResponse
+from fastapi.staticfiles import StaticFiles
+from fastapi.middleware.cors import CORSMiddleware
+from starlette.exceptions import HTTPException
+from settings import *
+import random
+import os, json
+from pydantic import BaseModel, validator, conint, constr, Field
+from databases import create_all_database, DeviceConfig,SysConfigs, CRUD,batch_insert_sys_configs, SqlQuery,batch_insert_device_configs
+
+# 关闭文档
+app = FastAPI()
+# app.mount("/model", StaticFiles(directory="model"), name="model")
+app.add_middleware(
+    CORSMiddleware,
+    # 允许跨域的源列表,例如 ["http://www.example.org"] 等等,["*"] 表示允许任何源
+    allow_origins=["*"],
+    # 跨域请求是否支持 cookie,默认是 False,如果为 True,allow_origins 必须为具体的源,不可以是 ["*"]
+    allow_credentials=False,
+    # 允许跨域请求的 HTTP 方法列表,默认是 ["GET"]
+    allow_methods=["*"],
+    # 允许跨域请求的 HTTP 请求头列表,默认是 [],可以使用 ["*"] 表示允许所有的请求头
+    # 当然 Accept、Accept-Language、Content-Language 以及 Content-Type 总之被允许的
+    allow_headers=["*"],
+    # 可以被浏览器访问的响应头, 默认是 [],一般很少指定
+    # expose_headers=["*"]
+    # 设定浏览器缓存 CORS 响应的最长时间,单位是秒。默认为 600,一般也很少指定
+    # max_age=1000
+)
+
+
+@app.on_event("startup")
+def on_startup():
+    create_all_database()
+    session = SqlQuery()
+    device_config_crud = CRUD(DeviceConfig)
+    all_devices = device_config_crud.read_all(session)
+    if len(all_devices) == 0:
+        # 如果配置表中一条数据都没有,就将初始化数据全部插入到数据表中
+        actions = json.load(open("action.json", encoding="utf-8"))
+        batch_insert_device_configs(session,actions)
+    sys_config_crud = CRUD(SysConfigs)
+    all_sys_configs = sys_config_crud.read_all(session)
+    if len(all_sys_configs) == 0:
+        # 如果配置表中一条数据都没有,就将初始化数据全部插入到数据表中
+        sys_config_json = json.load(open("sys_configs.json", encoding="utf-8"))
+        batch_insert_sys_configs(session, sys_config_json)
+
+
+class UnicornException(Exception):
+    def __init__(self, msg: str, code: int = -1):
+        self.msg = msg
+        self.code = code
+
+
+@app.exception_handler(UnicornException)
+async def error_throw(request: Request, exc: UnicornException):
+
+    return JSONResponse(
+        status_code=200,
+        content={
+            "code": exc.code,
+            "msg": exc.msg,
+        },
+    )
+
+
+@app.exception_handler(HTTPException)
+async def global_exception_handler(request, exc):
+    if exc.status_code == 500:
+        err_msg = "Server Internal Error"
+    else:
+        err_msg = exc.detail
+    return JSONResponse(
+        {"code": exc.status_code, "err_msg": err_msg, "status": "Failed"}
+    )

+ 3 - 0
python/model/__init__.py

@@ -0,0 +1,3 @@
+from .device_config import DeviceConfig
+from .photo_record import PhotoRecord
+from .sys_configs import SysConfigs

+ 31 - 0
python/model/device_config.py

@@ -0,0 +1,31 @@
+from typing import Optional
+from datetime import datetime
+from sqlmodel import Field, SQLModel
+# 定义DeviceConfig模型类
+
+class DeviceConfig(SQLModel, table=True):
+    __tablename__ = "device_config"
+    id: Optional[int] = Field(default=None, primary_key=True)
+    mode_type: Optional[str] = Field(default="执行左脚程序", index=True, max_length=128)
+    action_name: Optional[str] = Field(
+        default=None, index=True, max_length=128, description="动作名称"
+    )
+    action_status: Optional[bool] = Field(default=True,description="是否启用;true或者false")
+    action_index: Optional[int] = Field(default=999,description="排序,默认999")
+    camera_height: Optional[int] = Field(default=None, description="相机高度;步长1;最小0;最大400")
+    camera_angle: Optional[float] = Field(default=None, description="相机倾角;步长0.1,最小-40;最大40")
+    number_focus: Optional[int] = Field(default=None, description="对焦次数;最小0;最大1")
+    take_picture: Optional[bool] = Field(default=False, description="是否拍照")
+    turntable_position: Optional[float] = Field(default=None, description="转盘位置;步长1,最小0;最大800")
+    turntable_angle: Optional[float] = Field(default=None, description="转盘角度;步长1;最小-720;最大720")
+    shoe_upturn: Optional[bool] = Field(default=None, description="鞋子翻转;")
+    pre_delay: Optional[float] = Field(default=None, description="拍照前延迟;步长1;最小0;最大99")
+    after_delay: Optional[float] = Field(
+        default=None, description="拍照后延迟;步长1;最小0;最大99"
+    )
+    led_switch: Optional[bool] = Field(default=False, description="Led灯光开关;")
+    is_wait: Optional[bool] = Field(default=False, description="没用;")
+    is_need_confirm: Optional[bool] = Field(default=False, description="没用;")
+    update_time: Optional[datetime] = Field(default=None)
+    create_time: Optional[datetime] = Field(default_factory=datetime.utcnow)
+    delete_time: Optional[datetime] = Field(default=None)

+ 15 - 0
python/model/photo_record.py

@@ -0,0 +1,15 @@
+from sqlmodel import SQLModel, Field
+from datetime import datetime
+from typing import Optional
+
+class PhotoRecord(SQLModel, table=True):
+    __tablename__ = "photo_record"
+    id: Optional[int] = Field(default=None, primary_key=True, index=True)
+    goods_art_no: Optional[str] = Field(max_length=128, nullable=False)
+    image_path: Optional[str] = Field(default=None)
+    image_index: Optional[int] = Field(default=None)
+    image_deal_mode: Optional[int] = Field(default=None)
+    photo_create_time: Optional[datetime] = Field(default=None)
+    update_time: Optional[datetime] = Field(default=None)
+    create_time: Optional[datetime] = Field(default_factory=datetime.utcnow)
+    delete_time: Optional[datetime] = Field(default=None)

+ 10 - 0
python/model/sys_configs.py

@@ -0,0 +1,10 @@
+from sqlmodel import SQLModel, Field, Text
+from datetime import datetime
+from typing import Optional
+
+# 系统相关配置表
+class SysConfigs(SQLModel, table=True):
+    __tablename__ = "sys_configs"
+    id: Optional[int] = Field(default=None, primary_key=True, index=True)
+    key: str = Field(max_length=128, nullable=False)
+    value: str = Field(default=None)

+ 57 - 0
python/models.py

@@ -0,0 +1,57 @@
+from middleware import *
+import datetime
+
+class HlmForwardRequest(BaseModel):
+    method: str = Field(default="GET", description="请求方法")
+    headers: dict = Field(default={}, description="请求头")
+    target_url: str = Field(default="", description="目标地址")
+    query_params:str = Field(default="", description="请求参数")
+
+
+class ModelGetDeviceConfig(BaseModel):
+    """获取可执行程序命令列表"""
+    mode_type: Optional[str] =Field(
+        default="执行左脚程序", description="类型,【执行左脚程序】或者【执行右脚程序】"
+    )
+
+
+class ModelGetDeviceConfigDetail(BaseModel):
+    """获取可执行程序命令列表"""
+
+    id: int = Field(
+        default=None, description="可执行程序得id"
+    )
+
+
+class SaveDeviceConfig(BaseModel):
+    """获取可执行程序命令列表"""
+
+    id: Optional[int] = Field(default=None, primary_key=True)
+    mode_type: Optional[str] = Field(default="", index=True, max_length=128)
+    action_name: Optional[str] = Field(default="", index=True, max_length=128)
+    action_index: Optional[int] = Field(default=None)
+    action_status: Optional[bool] = Field(default=None)
+    camera_height: Optional[int] = Field(default=None)
+    camera_angle: Optional[float] = Field(default=None)
+    number_focus: Optional[int] = Field(default=None)
+    take_picture: Optional[bool] = Field(default=None)
+    turntable_position: Optional[float] = Field(default=None)
+    turntable_angle: Optional[float] = Field(default=None)
+    shoe_upturn: Optional[bool] = Field(default=None)
+    pre_delay: Optional[float] = Field(default=None)
+    after_delay: Optional[float] = Field(default=None)
+    led_switch: Optional[bool] = Field(default=None)
+    is_wait: Optional[bool] = Field(default=False)
+    is_need_confirm: Optional[bool] = Field(default=False)
+
+
+class PhotoRecordDelete(BaseModel):
+    """获取可执行程序命令列表"""
+
+    goods_art_nos: list[str] = Field(default=None, description="货号数组")
+
+
+class SysConfigParams(BaseModel):
+    """系统配置"""
+    key: str = Field(default=None, description="类型")
+    value: str = Field(default=None, description="json数据")

BIN
python/requestments.txt


+ 475 - 0
python/service/base.py

@@ -0,0 +1,475 @@
+import os
+import copy
+import configparser
+from module.base_mode.module_aes import Aes
+import exifread
+from natsort import ns, natsorted
+import shutil
+from hashlib import sha256, md5
+import requests
+from datetime import datetime
+
+
+# 获取digicam的路径
+def check_install_path(other):
+    path_list = []
+    path_list.append(other)
+    path_list.append(r"D:\Program Files (x86)\digiCamControl\CameraControl.exe")
+    path_list.append(r"C:\Program Files (x86)\digiCamControl\CameraControl.exe")
+    path_list.append(r"D:\Program Files\digiCamControl\CameraControl.exe")
+    path_list.append(r"C:\Program Files\digiCamControl\CameraControl.exe")
+    for i in path_list:
+        if os.path.exists(i):
+            return i
+    return ""
+
+
+# 输入文件夹,并检查是否是一个正常的图片文件夹。
+def check_goods_folder(folder_path):
+    all_files = os.listdir(folder_path)
+    for file in all_files:
+        file_path = "{}/{}".format(folder_path, file)
+        if not os.path.isdir(file_path):
+            continue
+        if "原始图" in os.listdir(file_path):
+            return folder_path
+    # 上述检查不通过,可能是选择目录错误
+    if "原始图" in all_files:
+        root_path, _ = os.path.split(folder_path)
+        return root_path
+    return None
+
+
+def download_file(url, file_path):
+    try:
+        root_path, file_name = os.path.split(file_path)
+        check_path(root_path)
+        response = requests.get(url)
+        _content = response.content
+        with open(file_path, 'wb') as f:
+            f.write(_content)
+        print("下载成功:{}".format(file_path))
+    except:
+        print("下载失败:{}".format(file_path))
+
+
+def calculate_sha256(file_path):
+    """Calculate the sha256 hash of the given file."""
+    sha256_hash = sha256()
+    try:
+        with open(file_path, "rb") as f:
+            # Read and update hash string value in blocks of 4K
+            for byte_block in iter(lambda: f.read(4096), b""):
+                sha256_hash.update(byte_block)
+            return sha256_hash.hexdigest()
+    except FileNotFoundError:
+        print(f"The file {file_path} does not exist.")
+        return None
+    except Exception as e:
+        print(f"An error occurred: {e}")
+        return None
+
+
+def get_modified_time(file_path):
+    # 获取文件最后修改的时间戳
+    timestamp = os.path.getmtime(file_path)
+    # 将时间戳转换为datetime对象,并格式化为指定格式的字符串
+    modified_time = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
+    return modified_time
+
+
+def download_file_to_io(url):
+    try:
+        # 发送GET请求
+        response = requests.get(url)
+        # 检查请求是否成功
+        response.raise_for_status()  # 如果响应状态码不是200,会抛出HTTPError异常
+        # 返回响应内容,以文本形式(对于HTML、JSON等文本内容)
+        return response.text
+        # 或者返回原始的二进制内容(对于图片、文件等非文本内容)
+        # return response.content
+    except requests.RequestException as e:
+        print(f"An error occurred: {e}")
+        return None
+
+
+def get_md5(file_path):
+    data_md5 = None
+    if os.path.isfile(file_path):
+        f = open(file_path, 'rb')
+        md5_obj = md5()
+        md5_obj.update(f.read())
+        hash_code = md5_obj.hexdigest()
+        f.close()
+        data_md5 = str(hash_code).lower()
+    return data_md5
+
+
+def list_dir(path):
+    listdir = os.listdir(path)
+    return natsorted(listdir, alg=ns.PATH)
+
+
+# 通用串口数据解析器
+def get_data_from_receive_data(receive_data, start, len_data, data_magnification=1):
+    # data_magnification 数据放大倍数,或缩小倍数,默认为1
+    try:
+        if len_data == 1:
+            data = receive_data[start]
+            return data * data_magnification
+        elif len_data == 2:
+            data = receive_data[start] << 8 | receive_data[start + 1]
+            return data * data_magnification
+        elif len_data == 4:
+            data = receive_data[start] << 24 | receive_data[start + 1] << 16 | receive_data[start + 2] << 8 | \
+                   receive_data[start + 3]
+            return data * data_magnification
+        return None
+    except:
+        return None
+
+
+def get_images(path):
+    _Type = [".png", ".PNG", ".jpg", ".JPG", ".gif", ".GIF", ".jpge", ".JPGE", ".CR2"]
+    image_list = []  # 过滤非图片数据
+    for _file in list_dir(path):
+        file_name, e = os.path.splitext(_file)
+        file_path = "{}/{}".format(path, _file)
+        if os.path.isdir(file_path):
+            continue
+        if e in _Type and "mask" not in file_name:
+            image_list.append(
+                {
+                    "file_path": file_path,
+                    "file_name": file_name,
+                    "file": _file,
+                    "root_path": path,
+                    "e": e,
+                }
+            )
+    return image_list
+
+
+def get_image_mask(path):
+    _Type = [".png", ".PNG", ".jpg", ".JPG", ".gif", ".GIF", ".jpge", ".JPGE", ".CR2"]
+    image_list = []  # 过滤非图片数据
+    for _file in list_dir(path):
+        file_name, e = os.path.splitext(_file)
+        file_path = "{}/{}".format(path, _file)
+        if os.path.isdir(file_path):
+            continue
+        if e in _Type and file_name == "mask":
+            image_list.append(
+                {
+                    "file_path": file_path,
+                    "file_name": file_name,
+                    "file": _file,
+                    "root_path": path,
+                    "e": e,
+                }
+            )
+    return image_list
+
+
+# 删除文件夹下的所有文件
+def remove_all_file(directory):
+    try:
+        shutil.rmtree(directory)
+        os.makedirs(directory)
+    except Exception as e:
+        print(f'Failed to clear directory {directory}. Reason: {e}')
+
+
+# 删除文件
+def remove_files(file_path):
+    try:
+        if os.path.exists(file_path):
+            os.remove(file_path)
+            return True
+    except BaseException as e:
+        print("base 188", e)
+    return False
+
+
+def get_folder(path):
+    folder_list = []
+    for _file in list_dir(path):
+        file_path = "{}/{}".format(path, _file)
+        if os.path.isdir(file_path):
+            folder_list.append(
+                {"folder_path": file_path,
+                 "folder_name": _file,
+                 "root_path": path,
+                 "label": "待处理",  # 是否需要继续处理
+                 }
+            )
+    return folder_list
+
+
+# 获取所有货号颜色 文件夹
+def get_all_goods_art_no_folder(path):
+    folder_list = []
+    if not os.path.exists(path):
+        return folder_list
+    if os.path.isfile(path):
+        return folder_list
+
+    temp_folder_list = get_folder(path)
+    for folder_data in temp_folder_list:
+        _p = "{}/原始图".format(folder_data["folder_path"])
+        if os.path.exists(_p):
+            folder_list.append(folder_data)
+    return folder_list
+
+
+def get_date_time_original(file_path):
+    with open(file_path, "rb") as file_data:
+        tags = exifread.process_file(file_data)
+        if "EXIF DateTimeOriginal" in tags:
+            return str(tags["EXIF DateTimeOriginal"])
+        else:
+            return False
+
+
+def get_data_from_hqt(goods_number_list, get_online_data_ins):
+    _goods_number_list = copy.deepcopy(goods_number_list)
+    _list = []
+    # 单次请求数少于20个
+    goods_number_dict = {}
+
+    while _goods_number_list:
+        goods_art_no = _goods_number_list.pop()
+        if "NUM" in goods_art_no:
+            goods_art_no = goods_art_no.replace("NUM", "")
+
+        _list.append(goods_art_no)
+        if len(_list) == 20 or len(_goods_number_list) == 0:
+            online_goods_art_data = get_online_data_ins.get_goods_art_no_info(
+                numbers_list=_list
+            )
+            if online_goods_art_data:
+                for number in online_goods_art_data:
+                    goods_number_dict["NUM" + number] = online_goods_art_data[number]
+            _list = []
+    return goods_number_dict
+
+
+def get_data_from_hqt_with_goods_art_no(goods_art_no_list, get_online_data_ins):
+    _goods_art_no_list = copy.deepcopy(goods_art_no_list)
+    _list = []
+    # 单次请求数少于20个
+    goods_art_no_dict = {}
+
+    while _goods_art_no_list:
+        goods_art_no = _goods_art_no_list.pop()
+
+        _list.append(goods_art_no)
+        if len(_list) == 20 or len(_goods_art_no_list) == 0:
+            online_goods_art_data = get_online_data_ins.get_goods_art_no_info(
+                goods_art_list=_list
+            )
+            if online_goods_art_data:
+                for _goods_art_no in online_goods_art_data:
+                    goods_art_no_dict[_goods_art_no] = online_goods_art_data[
+                        _goods_art_no
+                    ]
+            _list = []
+    return goods_art_no_dict
+
+
+def load_config_form_ini(config_path):
+    config = configparser.ConfigParser()
+    if os.path.exists(config_path):
+        try:
+            config.read(config_path, encoding="utf-8")
+        except:
+            config.read(config_path)
+
+    return config
+
+
+def set_config_to_int(config_ins, config_path, data_dict, section="basicSetup"):
+    for i in data_dict:
+        config_ins.set(section=section, option=i, value=data_dict[i])
+    try:
+        config_ins.write(open(config_path, "w", encoding="utf-8"))
+    except:
+        config_ins.write(open(config_path, "w"))
+    # try:
+    #     config_ins.read(config_path, encoding="utf-8")
+    # except:
+    #     config_ins.read(config_path)
+    return config_ins
+
+
+def get_config(config_ins, key, section="basicSetup"):
+    config_dict1 = config_ins.items(section)
+    __config_dict = {}
+    for i, k in config_dict1:
+        __config_dict[i] = k
+    if key in __config_dict:
+        return __config_dict[key]
+    else:
+        return None
+
+
+def get_config_by_items(config_dict):
+    __config_dict = {}
+    for i, k in config_dict:
+        __config_dict[i] = k
+    return __config_dict
+
+
+def get_dict_value(_dict, key, default=None):
+    if key in _dict:
+        return _dict[key]
+    else:
+        return default
+
+
+def set_key(authorization, SecretKey, SecretIv):
+    # --------------------用户身份--------------
+    if authorization:
+        authorization = Aes().get_key(authorization, SecretKey, SecretIv)
+    with open("key", "w") as f:
+        f.write(authorization)
+    return authorization
+
+
+def print_dic(_dict):
+    for i, v in _dict.items():
+        print("{}:{}".format(i, v))
+    print("\n")
+
+
+def check_path(_path):
+    if not os.path.exists(_path):
+        # 创建多级目录
+        os.makedirs(_path, exist_ok=True)
+        # os.mkdir(_path)
+    return True
+
+
+def move_folders(path_list, target_folder):
+    if not os.path.exists(target_folder):
+        os.makedirs(target_folder)
+    for source_folder in path_list:
+        shutil.move(source_folder, target_folder)
+
+
+# 给定一个图片路径,如果 是原始图下的则返回对应已扣图等信息。否则只返回基础信息
+def get_cutout_image_info(source_image_path):
+    if not os.path.exists(source_image_path):
+        return None
+    if os.path.isdir(source_image_path):
+        return None
+
+    data = {}
+    source_root_path, source_file = os.path.split(source_image_path)
+    source_file_name, source_file_extension = os.path.splitext(source_file)
+    print("---------------source_root_path--------------------")
+    print(source_root_path)
+    print(source_root_path[-3:])
+    # if source_root_path[-3:] != "原始图":
+    #     return None
+
+    data["source_image_path"] = source_image_path
+    data["source_root_path"] = source_root_path
+    data["source_file"] = source_file
+    data["source_file_name"] = source_file_name
+    data["source_file_extension"] = source_file_extension
+
+    cutout_image_path = "{}/原始图_已抠图/{}.png".format(source_root_path[:-3], source_file_name)
+    if not os.path.exists(cutout_image_path):
+        data["cutout_image_path"] = None
+        return data
+
+    cutout_image_root_path, cutout_image_file = os.path.split(cutout_image_path)
+    cutout_image_file_name, cutout_image_file_extension = os.path.splitext(cutout_image_file)
+    data["cutout_image_path"] = cutout_image_path
+    data["cutout_image_root_path"] = cutout_image_root_path
+    data["cutout_image_file"] = cutout_image_file
+    data["cutout_image_file_name"] = cutout_image_file_name
+    data["cutout_image_file_extension"] = cutout_image_file_extension
+    return data
+
+
+# 指定一个列表,指定一个需要移动的数据列表,移动到指定列表中的位置
+def move_elements_inplace(lst, indices, target_index, is_real_same_folder=False):
+    """
+    在原地将列表中指定位置的两个元素移动到目标位置。
+
+    :param lst: 要操作的列表
+    :param indices: 一个包含两个整数的元组或列表,表示要移动的元素的索引
+    :param target_index: 目标插入位置的索引
+    """
+
+    if not isinstance(indices, (list, tuple)) or len(indices) == 0:
+        raise ValueError("Indices must be a non-empty list or tuple of integers.")
+
+    # 添加到末尾
+    if target_index == len(lst):
+        if is_real_same_folder:
+            # 倒序取出所有内容,并添加到末尾
+            indices.sort(reverse=True)
+            temp = []
+            for i in indices:
+                temp.append(lst.pop(i))
+            while temp:
+                lst.append(temp.pop(-1))
+
+        # 重新写入索引
+        for index, image_label in enumerate(lst):
+            image_label.image_index = index
+        return
+
+    # 检查索引是否有效,并排序以确保按照正确的顺序处理
+    valid_indices = sorted(set(indices))  # 去重并排序
+    if not all(0 <= idx < len(lst) for idx in valid_indices):
+        raise IndexError("One or more indices are out of range.")
+
+    if not (0 <= target_index <= len(lst)):
+        raise IndexError("Target index is out of range.")
+
+    elements_to_move = [lst[idx] for idx in valid_indices]
+
+    # 如果目标位置在所有待移动元素的最大索引之后,则不需要调整目标位置
+    max_idx = max(valid_indices)
+    if max_idx < target_index:
+        pass
+    else:
+        # 计算需要减少的位置数(因为删除元素后,后续元素会向前移动)
+        shift_count = sum(1 for idx in valid_indices if idx < target_index)
+        target_index -= shift_count
+
+    # 移除元素(从大索引开始以避免影响小索引处的元素)
+    for idx in reversed(valid_indices):
+        del lst[idx]
+
+    # 插入元素到目标位置,保持它们原来的相对顺序
+    for i, element in enumerate(elements_to_move):
+        lst.insert(target_index + i, element)
+
+    # 重新写入索引
+    for index, image_label in enumerate(lst):
+        image_label.image_index = index
+
+
+# 比较两个相同格式的时间字符串
+def compare_two_times(time_str1, time_str2):
+    # 定义时间格式
+    time_format = "%Y-%m-%d %H:%M:%S"
+    # 将时间字符串解析为datetime对象
+    time1 = datetime.strptime(time_str1, time_format)
+    time2 = datetime.strptime(time_str2, time_format)
+
+    # 比较两个时间
+    if time1 > time2:
+        # print(f"{time_str1} 比 {time_str2} 新。")
+        return "left_new"
+    elif time1 < time2:
+        # print(f"{time_str2} 比 {time_str1} 新。")
+        return "right_new"
+    else:
+        # print("两个时间相等。")
+        return "is_same"

+ 1152 - 0
python/service/base_deal.py

@@ -0,0 +1,1152 @@
+import json
+# from module_generate_goods_art_no_table import GenerateGoodsArtNoTable
+
+from grenerate_main_image_test import GeneratePic
+from threading import Lock
+
+import settings
+from collections import defaultdict
+from remove_bg_ali import RemoveBgALi, Picture
+# from deal_cutout import DealCutout
+
+
+# from data import DataModeAutoDealPics
+import time
+from image_pic_deal import OnePicDeal
+
+from natsort import natsorted,ns
+import os
+import  shutil
+import exifread
+import datetime
+from databases import DeviceConfig,SqlQuery,CRUD
+from model.photo_record import PhotoRecord
+import requests
+import copy
+
+
+"""
+照片自动货号匹配 将图片放置在指定文件夹下,并自动对应不同的货号进行整理
+"""
+
+_Type = ['.png', '.PNG', '.jpg', '.JPG', '.gif', '.GIF', ".jpge", ".JPGE"]
+
+
+class BaseDealImage(object):
+    def __init__(self, image_dir=None):
+        self.goods_images_count_dict = defaultdict(int)
+        # 数据模型
+        self.data_mode_auto_deal_pics = DataModeAutoDealPics()
+        self.image_dir = image_dir
+        pass
+
+    def run_main(self, all_goods_art_no_folder_data, callback_func=None, cutout_mode=None,
+                 resize_image_view=None, windows=None, logo_path=None, image_order_list=None):
+        # 对所有缺失已抠图的进行抠图处理
+        self.run_cutout_image(all_goods_art_no_folder_data=all_goods_art_no_folder_data,
+                              callback_func=callback_func,
+                              cutout_mode=cutout_mode,
+                              windows=windows,
+                              )
+        error_num = 0
+        successful_num = 0
+        for goods_art_no_folder_data in all_goods_art_no_folder_data:
+            if goods_art_no_folder_data["label"] != "待处理":
+                continue
+            if windows:
+                if windows.state != 1:
+                    break
+            folder_name = goods_art_no_folder_data["folder_name"]
+            callback_func("开始处理文件夹==========  {} ".format(folder_name))
+            if settings.IS_TEST:
+                flag = self.shoes_run_one_folder_to_deal(goods_art_no_folder_data=goods_art_no_folder_data,
+                                                         resize_image_view=resize_image_view,
+                                                         logo_path=logo_path,
+                                                         image_order_list=image_order_list,
+                                                         callback_func=callback_func,
+                                                         windows=windows,
+                                                         )
+                if flag is None:
+                    callback_func("货号:{} 数据异常".format(folder_name))
+                else:
+                    if flag:
+                        successful_num += 1
+                        callback_func("货号:{} 图片生成处理成功".format(folder_name))
+                    else:
+                        error_num += 1
+                        callback_func("货号:{} 图片生成处理失败".format(folder_name))
+            else:
+                try:
+                    flag = self.shoes_run_one_folder_to_deal(goods_art_no_folder_data=goods_art_no_folder_data,
+                                                             resize_image_view=resize_image_view,
+                                                             logo_path=logo_path,
+                                                             image_order_list=image_order_list,
+                                                             callback_func=callback_func,
+                                                             windows=windows,
+                                                             )
+                    if flag is None:
+                        callback_func("货号:{} 数据异常".format(folder_name))
+                    else:
+                        if flag:
+                            successful_num += 1
+                            callback_func("货号:{} 图片生成处理成功".format(folder_name))
+                        else:
+                            error_num += 1
+                            callback_func("货号:{} 图片生成处理失败".format(folder_name))
+                except BaseException as e:
+                    error_num += 1
+                    callback_func("货号:{} 图片生成处理异常,原因:{}".format(folder_name, e))
+        callback_func("处理成功:{}个,失败:{}".format(successful_num, error_num))
+
+    def checkImageAmount(self, image_dir: str, amount: int, todo_goods_art_no_folder_name_list=None) -> dict:
+        result = {'code': 0, 'msg': '', 'data': {}}
+        for goods_art_no_folder in self.list_dir(image_dir):
+            # 指定内容检查
+            if todo_goods_art_no_folder_name_list is not None:
+                if goods_art_no_folder not in todo_goods_art_no_folder_name_list:
+                    continue
+
+            if not os.path.isdir("{}/{}".format(image_dir, goods_art_no_folder)):
+                continue
+            if "软件" in goods_art_no_folder:
+                continue
+            if "无法" in goods_art_no_folder:
+                continue
+            if "原始图" not in self.list_dir("{}/{}".format(image_dir, goods_art_no_folder)):
+                result['data'][goods_art_no_folder] = '文件夹下,没有 原始图 文件夹\n'
+                continue
+
+            # 计算单个文件夹原图数量
+            images = [x for x in self.list_dir("{}/{}/原始图".format(image_dir, goods_art_no_folder))]
+            image_num = 0
+            for pic_file_name in images:
+                _, e = os.path.splitext(pic_file_name)
+                if e in _Type:
+                    image_num += 1
+            # if self.number_pictures != 0:
+            if image_num > amount:
+                result['data'][goods_art_no_folder] = '货号图片大于{}张~\n'.format(amount)
+            if image_num < 2:
+                result['data'][goods_art_no_folder] = '货号图片小于于2张~\n'
+        if result['data']:
+            result['code'] = 1
+        return result
+
+    def check_folders_image_amount(self, all_goods_art_no_folder_data, image_order_list):
+        amount = len(image_order_list)
+        message = ""
+        for goods_art_no_folder_data in all_goods_art_no_folder_data:
+            if goods_art_no_folder_data["label"] != "待处理":
+                continue
+            folder_path = goods_art_no_folder_data["folder_path"]
+            folder_name = goods_art_no_folder_data["folder_name"]
+            images = [x for x in self.list_dir("{}/原始图".format(folder_path))]
+            image_num = 0
+            for pic_file_name in images:
+                _, e = os.path.splitext(pic_file_name)
+                if e in _Type:
+                    image_num += 1
+            if image_num > amount:
+                goods_art_no_folder_data["label"] = "错误"
+                message += '货号{}:图片大于{}张~\n'.format(folder_name, amount)
+            if image_num < 2:
+                message += '货号{}:图片小于于2张~\n'.format(folder_name)
+
+        return all_goods_art_no_folder_data, message
+
+    def check_one_folder_image_amount(self, folder_data, amount: int):
+        # 计算单个文件夹原图数量
+        images = [x for x in self.list_dir("{}/原始图".format(folder_data["folder_path"]))]
+        image_num = 0
+        for pic_file_name in images:
+            _, e = os.path.splitext(pic_file_name)
+            if e in _Type:
+                image_num += 1
+        if image_num > amount:
+            return False, '货号{}:图片大于{}张~\n'.format(folder_data["folder_name"], amount)
+        if image_num < 2:
+            return False, '货号{}:图片小于于2张~\n'.format(folder_data["folder_name"])
+        return True, ""
+
+    # 指定的图片顺序
+    def getImageOrder(self, image_order: str, resize_image_view: str):
+        imageOrderList = image_order.replace(",", ",").replace(' ', '').replace('图', '').split(",")
+        if len(set(imageOrderList)) != len(imageOrderList):
+            return {'code': 1, 'msg': '图片位置与顺序重复,请检查您的输入'}
+
+        for val in imageOrderList:
+            if val not in ["俯视", "侧视", "后跟", "鞋底", "内里", "组合", "组合2", "组合3", "组合4", "组合5"]:
+                return {'code': 1, 'msg': '可选项为:俯视,侧视,后跟,鞋底,内里,组合,组合2,组合3,组合4,组合5'}
+
+        if resize_image_view not in imageOrderList:
+            return {'code': 1, 'msg': '缩小的步骤必须是你填写的图片顺序中'}
+
+        return {'code': 0, 'msg': 'sucess', 'imageOrderList': imageOrderList}
+
+    def shoes_run_one_folder_to_deal(self,
+                                     goods_art_no_folder_data,
+                                     image_order_list: list,
+                                     resize_image_view: str,
+                                     logo_path="",
+                                     windows=None,
+                                     callback_func=None):
+        """
+        操作步骤:
+        1、查询每个图片的角度
+        2、
+        """
+
+        is_successful = True
+
+        folder_path = goods_art_no_folder_data["folder_path"]
+        folder_name = goods_art_no_folder_data["folder_name"]
+        all_original_images = self.get_images("{}/原始图".format(folder_path))
+        self.check_path('{}/800x800'.format(folder_path))
+        self.crate_all_folders(folder_path)
+
+        if not all_original_images:
+            return None
+            # _ = ["俯视", "侧视", "后跟", "鞋底", "内里"]
+        for index, image_dict in enumerate(all_original_images):
+            if index < len(image_order_list):
+                image_dict["image_view"] = image_order_list[index]
+            else:
+                image_dict["image_view"] = '其他{}'.format(len(image_order_list) - index + 1)
+
+        # ====================处理所有图片的顺序====================
+        # ["俯视", "侧视", "后跟", "鞋底", "内里"]
+        _config = {"俯视": 1,
+                   "侧视": 2,
+                   "后跟": 3,
+                   "鞋底": 4,
+                   "内里": 5, }
+
+        r = time.time()
+        n = 0
+        _index = 0
+        # 检查是否有重复的角度
+        _d_views = []
+        f = True
+        for image_dict in all_original_images:
+            n += 1
+            _index += 1
+            image_dict["old_file_name"] = image_dict["file_name"]
+            image_dict["random_name"] = "{}-{}".format(r, n)
+            if image_dict["image_view"] in _config:
+                if image_dict["image_view"] not in _d_views:
+                    _d_views.append(image_dict["image_view"])
+                else:
+                    callback_func("货号:{} 处理失败".format(folder_name))
+                    # self.show_progress_detail("货号图{}  存在多个{} 角度~".format(goods_art_no_folder, image_dict["image_view"]))
+                    return None
+
+                image_dict["index"] = _config[image_dict["image_view"]]
+            else:
+                image_dict["index"] = _index
+
+        all_original_images.sort(key=lambda x: x["index"])
+        #  ==========直接进行处理=============
+        i_n = 0
+        image_index = 0  # 图片顺序
+        is_image_deal_mode = 0
+
+        # 删除目录再新建
+        if os.path.exists('{}/阴影图处理'.format(folder_path)):
+            shutil.rmtree('{}/阴影图处理'.format(folder_path))
+
+        self.crate_all_folders(folder_path)
+
+        for image_dict in all_original_images:
+            if windows:
+                if windows.state != 1:
+                    return None
+            i_n += 1
+            image_index += 1
+            original_image_path = "{}/原始图/{}{}".format(folder_path, image_dict["file_name"], image_dict["e"])
+            file_name = image_dict["file_name"]
+            print("正在处理,货号:{}".format(folder_path))
+
+            # self.show_progress_detail("正在处理,货号:{}".format(file_name))
+            # 该文件在800images下没有时,则进行生成新的抠图
+            # 检查是否存在已抠图文件,如没有再去抠图
+
+            original_move_bg_image_path = "{}/原始图_已抠图/{}{}".format(folder_path, image_dict["file_name"], ".png")
+
+            # 此处判断鞋子是否为左右脚
+            if image_index == 1:
+                is_image_deal_mode = 0
+                if OnePicDeal().check_shoe_is_right(image_path=original_move_bg_image_path):
+                    is_image_deal_mode = 1  # 1表示要镜像,0表示不做镜像
+
+            """进行800image 生成"""
+            generate_pic = GeneratePic()
+
+            if settings.OUT_PIC_MODE == ".jpg":
+                out_path = "{}/800x800/{}{}".format(folder_path, file_name, ".jpg")
+            else:
+                out_path = "{}/800x800/{}{}".format(folder_path, file_name, ".png")
+
+            out_process_path_1 = "{}/阴影图处理/{}_{}_阴影{}".format(folder_path, file_name,
+                                                              image_dict["image_view"], ".png")
+
+            out_process_path_2 = "{}/阴影图处理/{}_{}_抠图{}".format(folder_path, file_name,
+                                                              image_dict["image_view"], ".png")
+
+            resize_mode = 1
+            max_box = None
+
+            print("------------1", image_dict["image_view"], resize_image_view)
+            if image_dict["image_view"] == resize_image_view:
+                print(image_dict["image_view"], resize_image_view)
+                resize_mode = 2
+
+            if settings.Mode == "皮具":
+                max_box = (1000, 1200)
+
+            if resize_mode == 2:
+                print(
+                    is_image_deal_mode,
+                    resize_mode,
+                    settings.OUT_PIC_SIZE,
+                    True if i_n == 1 else False,
+                    max_box
+                )
+
+            if not generate_pic.run(image_path=original_image_path,
+                                    cut_image_path=original_move_bg_image_path,
+                                    out_path=out_path,
+                                    image_deal_mode=is_image_deal_mode,
+                                    resize_mode=resize_mode,
+                                    out_pic_size=settings.OUT_PIC_SIZE,
+                                    is_logo=True if i_n == 1 else False,
+                                    out_process_path_1=out_process_path_1,
+                                    out_process_path_2=out_process_path_2,
+                                    max_box=max_box,
+                                    logo_path=logo_path,
+                                    ):
+                is_successful = False
+        if is_successful:
+            return True
+        else:
+            return False
+
+    def to_upload_pic(self, file_path, is_resize=True):
+        file_name = os.path.split(file_path)[1]
+        e = os.path.splitext(file_name)[1][1:]
+
+        im = Picture(file_path)
+        if im.x > 500:
+            im.resize(width=500)
+
+        _ = {"jpg": "JPEG",
+             "JPG": "JPEG",
+             "JPEG": "JPEG",
+             "jpeg": "JPEG",
+             "png": "PNG",
+             "PNG": "PNG", }
+        e = _[e]
+        image_io = im.save_to_io(e)
+        goods_data = {"file_path": os.path.split(file_path)[1],
+                      "image_io": image_io,
+                      "e": e
+                      }
+        url = self.data_mode_image_cut.get_online_data.upload_pic(goods_data=goods_data)
+        return url
+
+    def get_images(self, path):
+        image_list = []  # 过滤非图片数据
+        for _file in self.list_dir(path):
+            file_name, e = os.path.splitext(_file)
+            if e in _Type:
+                image_list.append({"file_path": "{}/{}".format(path, _file),
+                                   "file_name": file_name,
+                                   "e": e})
+        return image_list
+
+    def crate_all_folders(self, root_path):
+        path_list = ["800x800", "原始图_已抠图", "阴影图处理"]
+        for i in path_list:
+            path = "{}/{}".format(root_path, i)
+            self.check_path(path)
+
+    def run_cutout_image(self, all_goods_art_no_folder_data, callback_func=None, cutout_mode=1, windows=None):
+        """
+        处理所有的抠图
+        """
+        callback_func('开始处理抠图~')
+        error_goods_art_no_folder = []
+        for goods_art_no_folder_data in all_goods_art_no_folder_data:
+            if goods_art_no_folder_data["label"] != "待处理":
+                continue
+            folder_path = goods_art_no_folder_data["folder_path"]
+            self.crate_all_folders(folder_path)
+            # 检查是否存在已抠图文件,如没有再去抠图
+            images = [x for x in self.list_dir("{}/原始图".format(folder_path))]
+            cutImageList = []
+            for pic_file_name in images:
+                if windows:
+                    if windows.state != 1:
+                        break
+                # 根据名称判断,没有抠图过的,进行统计
+                file_name, suffix = os.path.splitext(pic_file_name)
+                if suffix in _Type:
+                    original_image_path = "{}/原始图/{}".format(folder_path, pic_file_name)
+                    original_move_bg_image_path = "{}/原始图_已抠图/{}{}".format(folder_path, file_name, ".png")
+                    if not os.path.exists(original_move_bg_image_path):
+                        # 没有抠图文件,进行抠图生成
+                        callback_func("正在抠图 货号:{}".format(file_name))
+                        if cutout_mode == '2':
+                            cutImageList.append({
+                                "file_name": file_name,  # 文件名
+                                "file_e": suffix,  # 后缀,.jpg
+                                "file_path": original_image_path,  # 完整路径
+                                "file": '{}{}'.format(file_name, suffix),  # 图片文件名,带后缀
+                                "need_cutout": True,  # 必须,需要抠图
+                                "out_path": original_move_bg_image_path
+                            })
+                        else:
+                            remove_pic_ins = RemoveBgALi()
+                            if settings.IS_TEST:
+                                im = remove_pic_ins.get_image_cut(file_path=original_image_path,
+                                                                  out_file_path=original_move_bg_image_path)
+                            else:
+                                try:
+                                    im = remove_pic_ins.get_image_cut(file_path=original_image_path,
+                                                                      out_file_path=original_move_bg_image_path)
+                                except FunctionTimedOut as f:
+                                    callback_func("货号图{} 抠图处理超时~".format(file_name))
+                                    im = None
+                                except BaseException as e:
+                                    callback_func("货号图{} 抠图处理失败,原因{}".format(file_name, e))
+                                    im = None
+
+                            if not im:
+                                callback_func("货号图{} 抠图处理失败~".format(file_name))
+                                continue
+                            else:
+                                callback_func("货号图{} 抠图完成~".format(file_name))
+
+            if cutout_mode == '2':
+                dealCutout = DealCutout(windows=None)
+                dealCutout.need_cutout_images = cutImageList
+                dealCutout.run()
+                while True:
+                    time.sleep(1)
+                    if windows:
+                        if windows.state != 1:
+                            break
+                    if dealCutout.state == 3:
+                        if len(dealCutout.resultData) != len(cutImageList):
+                            error_goods_art_no_folder.append(folder_path)
+                        break
+
+        if error_goods_art_no_folder:
+            print("以下货号抠图失败~\n {}".format(error_goods_art_no_folder))
+            callback_func("以下货号抠图失败~\n {}".format(error_goods_art_no_folder))
+        else:
+            callback_func("完成抠图处理")
+
+    def checkCutoutImage(self, image_dir: str, todo_goods_art_no_folder_name_list=None):
+        """
+        进行图片检查,不合规的直接提示
+        """
+        error_goods_art_no_folder = []
+        self.check_path("{}/软件-处理失败".format(image_dir))
+
+        for goods_art_no_folder in self.list_dir(image_dir):
+            # 指定内容检查
+            if todo_goods_art_no_folder_name_list is not None:
+                if goods_art_no_folder not in todo_goods_art_no_folder_name_list:
+                    continue
+
+            if not os.path.isdir("{}/{}".format(image_dir, goods_art_no_folder)):
+                continue
+            if "软件" in goods_art_no_folder:
+                continue
+            if "无法" in goods_art_no_folder:
+                continue
+            if "原始图" not in self.list_dir("{}/{}".format(image_dir, goods_art_no_folder)):
+                error_goods_art_no_folder.append(goods_art_no_folder)
+                continue
+
+            self.check_path("{}/{}/原始图_已抠图".format(image_dir, goods_art_no_folder))
+            self.check_path("{}/{}/800x800".format(image_dir, goods_art_no_folder))
+            self.check_path("{}/{}/阴影图处理".format(image_dir, goods_art_no_folder))
+
+        if error_goods_art_no_folder:
+            self.move_folders(path_list=["{}/{}".format(self.image_dir, x) for x in error_goods_art_no_folder],
+                              target_folder="{}/软件-处理失败".format(self.image_dir))
+            return False
+
+    def move_folders(self, path_list, target_folder):
+        for source_folder in path_list:
+            shutil.move(source_folder, target_folder)
+
+    def rename_folder_for_hqt(self, all_goods_art_no_folder_data):
+        """
+        步骤:
+        规整红蜻蜓的文件名
+        重新按文件名进行命名
+        """
+        goods_art_no_list = []
+        for goods_art_no_folder_data in all_goods_art_no_folder_data:
+            if "@" not in goods_art_no_folder_data["folder_name"]:
+                goods_art_no_folder_data["label"] = "待处理"
+                goods_art_no_list.append(goods_art_no_folder_data["folder_name"])
+            else:
+                goods_art_no_folder_data["label"] = "不处理"
+
+        if goods_art_no_list:
+            # goods_art_no_dict 文件夹与货号的字典
+            goods_art_no_dict = self.get_data_from_hqt_with_goods_art_no(
+                goods_art_no_list=goods_art_no_list)
+            for goods_art_no_folder_data in all_goods_art_no_folder_data:
+                if goods_art_no_folder_data["label"] != "待处理":
+                    continue
+                goods_art_no_folder = goods_art_no_folder_data["folder_name"]
+                if goods_art_no_folder in goods_art_no_dict:
+                    print(goods_art_no_folder)
+                    old_folder_path = goods_art_no_folder_data["folder_path"]
+                    new_folder_name = "{}@NUM{}".format(goods_art_no_folder,
+                                                        goods_art_no_dict[goods_art_no_folder]["编号"])
+                    new_folder_path = "{}/{}".format(goods_art_no_folder_data["root_path"], new_folder_name)
+                    try:
+                        os.rename(old_folder_path, new_folder_path)
+                        goods_art_no_folder_data["folder_path"] = new_folder_path
+                        goods_art_no_folder_data["folder_name"] = new_folder_path
+                        goods_art_no_folder_data["label"] = "待处理"
+                    except BaseException as e:
+                        goods_art_no_folder_data["label"] = "不处理"
+                        print("521 文件夹重名命失败:{}".format(e))
+
+        # 重新规整修改图片名称
+        for goods_art_no_folder_data in all_goods_art_no_folder_data:
+            if goods_art_no_folder_data["label"] != "待处理":
+                continue
+            goods_art_no_folder = goods_art_no_folder_data["folder_name"]
+            _img_all = self.list_dir("{}/原始图".format(goods_art_no_folder_data["folder_path"]))
+            index = 0
+            for _file in _img_all:
+                file_name, suffix = os.path.splitext(_file)
+                if suffix in _Type:
+                    index += 1
+                    folder_path = goods_art_no_folder_data["folder_path"]
+                    new_file_name = "{}({}){}".format(goods_art_no_folder, index, suffix)
+                    new_path = "{}/原始图/{}".format(folder_path, new_file_name)
+                    old_path = "{}/原始图/{}".format(folder_path, _file)
+                    crop_new_path = "{}/原始图_已抠图/{}".format(folder_path, "{}({}).png".format(goods_art_no_folder, index))
+                    crop_old_path = "{}/原始图_已抠图/{}".format(folder_path, "{}.png".format(file_name))
+
+                    if old_path != new_path:
+                        # 存在货号命名错误的,进行修正
+                        try:
+                            if os.path.exists(old_path):
+                                os.rename(old_path, new_path)
+                            if os.path.exists(crop_old_path):
+                                os.rename(crop_old_path, crop_new_path)
+                        except BaseException as e:
+                            goods_art_no_folder_data["label"] = "不处理"
+                            print("550 文件夹重名命失败:{}".format(e))
+
+    def cutImagePiju(self, image_dir: str, image_order='', is_check_number=True, is_filter=True, resize_image_view='后跟',
+                     callback_func=None, event=None, todo_goods_art_no_folder_name_list=None):
+        """
+        1、遍历文件夹,基于生成的结果图看哪些需要进行抠图等处理
+        2、压缩并上传平台获取抠图
+        3、抠图处理成白底图
+        4、做成800*800/200*200
+        :return:
+        """
+        logo_path = ""
+
+        res = self.getImageOrder(image_order=image_order, resize_image_view=resize_image_view)
+        if res['code'] != 0:
+            callback_func(res['msg'])
+            return {'code': 1, 'msg': res['msg']}
+
+        imageOrderList = res['imageOrderList']
+
+        """扫描文档,检查有哪些需要进行抠图等处理"""
+        self.lock = Lock()
+        to_do_images_total = 0
+        error_goods_art_no_folder = []
+        for goods_art_no_folder in self.list_dir(image_dir):
+            # 指定内容检查
+            if todo_goods_art_no_folder_name_list is not None:
+                if goods_art_no_folder not in todo_goods_art_no_folder_name_list:
+                    continue
+
+            if not os.path.isdir("{}/{}".format(image_dir, goods_art_no_folder)):
+                continue
+
+            self.check_path("{}/{}/原始图".format(image_dir, goods_art_no_folder))
+            self.check_path("{}/{}/原始图_已抠图".format(image_dir, goods_art_no_folder))
+            self.check_path("{}/{}/800x800".format(image_dir, goods_art_no_folder))
+            self.check_path("{}/{}/阴影图处理".format(image_dir, goods_art_no_folder))
+
+            # 遍历原始图片文件夹
+            all_original_images = [x for x in
+                                   self.list_dir(
+                                       "{}/{}/原始图".format(image_dir, goods_art_no_folder))]
+            # 检查已抠图文件夹
+            all_moved_images = [os.path.splitext(x)[0] for x in
+                                self.list_dir(
+                                    "{}/{}/原始图_已抠图".format(image_dir, goods_art_no_folder))]
+
+            all_800images = [os.path.splitext(x)[0] for x in
+                             self.list_dir(
+                                 "{}/{}/800x800".format(image_dir, goods_art_no_folder))]
+            if is_check_number and len(imageOrderList) != len(all_original_images):
+                callback_func("{} 文件夹下图片数量与订单数量不一致,请检查!".format(goods_art_no_folder))
+                return {'code': 1, 'msg': '{} 文件夹下图片数量与订单数量不一致,请检查!'.format(goods_art_no_folder)}
+
+            all_800images = []
+            image_num = 0
+            for pic_file_name in all_original_images:
+                if pic_file_name not in all_800images:
+                    # 根据名称判断,没有抠图过的,进行统计
+                    _, e = os.path.splitext(pic_file_name)
+                    print(e)
+                    if e in _Type:
+                        image_num += 1
+                        print("----------》", goods_art_no_folder, pic_file_name)
+                        to_do_images_total += 1
+            # if image_num > 5:
+            #     error_goods_art_no_folder.append(goods_art_no_folder)
+
+        # if error_goods_art_no_folder:
+        #     self.show_progress_detail("以下货号图片张数超过5张~\n {}".format(error_goods_art_no_folder))
+        #     self.set_state(state_value=2)
+        #     return
+
+        if to_do_images_total > 0:
+            # self.progress_sign.emit({"type": "处理图片 抠图、加工等", "progress_bar_value": 0})
+
+            for goods_art_no_folder in self.list_dir(image_dir):
+                # 指定内容检查
+                if todo_goods_art_no_folder_name_list is not None:
+                    if goods_art_no_folder not in todo_goods_art_no_folder_name_list:
+                        continue
+
+                if not os.path.isdir('{}/{}'.format(image_dir, goods_art_no_folder)):
+                    continue
+
+                self.run_one_folder_to_deal(goods_art_no_folder=goods_art_no_folder,
+                                            image_dir=image_dir,
+                                            image_order=image_order,
+                                            resize_image_view=resize_image_view,
+                                            callback_func=callback_func,
+                                            logo_path=logo_path,
+                                            )
+        else:
+            # self.show_progress_detail("没有需要处理的图片~")
+            callback_func('没有需要处理的图片~')
+        # self.set_state(state_value=2)
+        return {'code': 0, 'msg': 'ok'}
+
+    def run_one_folder_to_deal(self, goods_art_no_folder, image_dir, image_order, resize_image_view,
+                               callback_func=None, logo_path=""):
+
+        _img_all = self.list_dir("{}/{}/原始图".format(image_dir, goods_art_no_folder))
+        all_original_images = []  # 过滤非图片数据
+        index = 0
+        for _file in _img_all:
+
+            file_name, e = os.path.splitext(_file)
+            if e in _Type:
+                index += 1
+                new_file_name = "{}({}){}".format(goods_art_no_folder, index, e)
+                new_path = "{}/{}/原始图/{}".format(image_dir, goods_art_no_folder, new_file_name)
+                old_path = "{}/{}/原始图/{}".format(image_dir, goods_art_no_folder, _file)
+                if old_path != new_path:
+                    # 存在货号命名错误的,进行修正
+                    try:
+                        os.rename(old_path, new_path)
+                    except:
+                        pass
+                all_original_images.append(new_file_name)
+
+        if os.path.exists("{}/{}/原始图/镜像.txt".format(image_dir, goods_art_no_folder)):
+            file_mirror_mark = True
+        else:
+            file_mirror_mark = None
+        # if goods_art_no_folder == "AC51016112":
+        #     print(file_mirror_mark)
+        #     raise 111
+
+        all_moved_images = [os.path.splitext(x)[0] for x in
+                            self.list_dir("{}/{}/原始图_已抠图".format(image_dir, goods_art_no_folder))]
+        all_800images = [os.path.splitext(x)[0] for x in
+                         self.list_dir(
+                             "{}/{}/800x800".format(image_dir, goods_art_no_folder))]
+        all_800images = []
+
+        # 检查哪些图片没有做过抠图处理
+        i_n = 0
+        _name_list = ["视角{}".format(x) for x in range(1, len(all_original_images) + 1)]
+        # if goods_art_no_folder == "AC51028001":
+        #     _name_list = ["正视", "45度", "侧视", "后视", "底视", "其他1", "其他2", "其他3"]
+
+        image_index = 0  # 图片顺序
+        is_image_deal_mode = 0
+        max_box = None
+
+        for file in all_original_images:
+            i_n += 1
+            image_index += 1
+            original_image_path = "{}/{}/原始图/{}".format(image_dir, goods_art_no_folder, file)
+            file_name = os.path.splitext(file)[0]
+            """
+            当第三张就是为后跟
+            """
+            if file_name not in all_800images:  # 所有都重新生成
+                # if goods_art_no_folder != "AC51016112":
+                #     continue
+                goods_art_no_folder_path = "{}/{}".format(image_dir, goods_art_no_folder)
+                print("正在处理,货号:{}".format(goods_art_no_folder_path))
+                # self.show_progress_detail("正在处理,货号:{}".format(file_name))
+                callback_func("正在处理,货号:{}".format(file_name))
+                # 该文件在800images下没有时,则进行生成新的抠图
+                # 检查是否存在已抠图文件,如没有再去抠图
+                original_move_bg_image_path = "{}/原始图_已抠图/{}{}".format(goods_art_no_folder_path, file_name,
+                                                                       ".png")
+
+                image_deal_mode = 0  # 默认图片不做镜像处理
+                if not os.path.exists(original_move_bg_image_path):
+                    # 没有抠图文件,进行抠图生成
+                    # self.show_progress_detail("正在抠图 货号:{}".format(file_name))
+                    callback_func("正在抠图 货号:{}".format(file_name))
+                    remove_pic_ins = RemoveBgALi()
+                    im = remove_pic_ins.get_image_cut(file_path=original_image_path,
+                                                      out_file_path=original_move_bg_image_path)
+
+                    if not im:
+                        # self.show_progress_detail("货号图{} 抠图处理失败~".format(file_name))
+                        callback_func("货号图{} 抠图处理失败~".format(file_name))
+                        continue
+
+                if image_index == 1:
+                    is_image_deal_mode = 0
+                    if settings.Mode == "鞋类":
+                        goods_class = "鞋"
+                        # 如果图片已存在,则需要通过加载图片判断是否为左右脚
+                        if OnePicDeal().check_shoe_is_right(image_path=original_move_bg_image_path):
+                            image_deal_mode = 1  # 1表示要镜像,0表示不做镜像
+                            is_image_deal_mode = 1
+                    if settings.Mode == "皮具":
+                        # 图片对应的商品类型
+                        # goods_class = self.get_goods_class(goods_art_no_folder, original_image_path)
+                        max_box = (1000, 1200)
+                        # _ = {"AC51028001": "女包",
+                        #      "AC51028002": "男包",
+                        #      "AC51028003": "皮带",
+                        #      "AC51028004": "女包"}
+                        # if goods_class in _:
+                        #     goods_class = _[goods_class]
+                        # else:
+                        #     goods_class = "女包"
+                        #
+                        # _ = {"女包": (1000, 1200),
+                        #      "男包": (1000, 1200),
+                        #      "皮带": (1000, 1000), }
+                        # max_box = _[goods_class]
+
+                # 获取图片信息非必要程序,用于处理图片模式
+                date_time_original = self.get_date_time_original(original_image_path)  # 获取照片拍照时间
+
+                if date_time_original:
+                    # 基于照片的时间,与数据库匹配goods_art_no
+                    self.lock.acquire()
+                    _data = self.dataModeMatchPhoto.get_goods_art_no(date_time_original)
+                    self.lock.release()
+
+                    if _data:
+                        # 能匹配上数据库
+                        goods_art_no, _image_index, _image_deal_mode = _data
+                        if _image_index < 10:
+                            image_index = _image_index
+
+                        if _image_deal_mode == 1:
+                            image_deal_mode = 1
+                        # print(goods_art_no, image_index, image_deal_mode)
+
+                if file_mirror_mark:
+                    image_deal_mode = 1
+
+                """进行800image 生成"""
+                generate_pic = GeneratePic()
+                if settings.OUT_PIC_MODE == ".jpg":
+                    out_path = "{}/800x800/{}{}".format(goods_art_no_folder_path, file_name, ".jpg")
+                else:
+                    out_path = "{}/800x800/{}{}".format(goods_art_no_folder_path, file_name, ".png")
+
+                out_process_path_1 = "{}/阴影图处理/{}_{}_阴影{}".format(goods_art_no_folder_path, file_name,
+                                                                  _name_list[i_n - 1], ".png")
+                out_process_path_2 = "{}/阴影图处理/{}_{}_抠图{}".format(goods_art_no_folder_path, file_name,
+                                                                  _name_list[i_n - 1], ".png")
+
+                print("image_index", image_index)
+                image_index = 99
+                if generate_pic.run(image_path=original_image_path,
+                                    cut_image_path=original_move_bg_image_path,
+                                    out_path=out_path,
+                                    image_deal_mode=is_image_deal_mode,
+                                    image_index=image_index,
+                                    out_pic_size=settings.OUT_PIC_SIZE,
+                                    is_logo=True if i_n == 1 else False,
+                                    out_process_path_1=out_process_path_1,
+                                    out_process_path_2=out_process_path_2,
+                                    max_box=max_box,
+                                    logo_path=logo_path,
+                                    ):
+                    # self.show_progress_detail("货号图{} _{} 已完成800*800图片制作~".format(image_index, file_name))
+                    callback_func("货号图{} _{} 已完成800*800图片制作~".format(image_index, file_name))
+                else:
+                    # self.show_progress_detail("货号图{} _{}  图片生成处理失败~".format(image_index, file_name))
+                    callback_func("货号图{} _{}  图片生成处理失败~".format(image_index, file_name))
+
+                # 完成处理的图片进度
+                self.lock.acquire()
+                # self.set_progress()
+                self.lock.release()
+
+    def get_goods_art_no(self, date_time_original):
+        time_array = time.strptime(date_time_original, "%Y:%m:%d %H:%M:%S")
+
+        time_array = time.mktime(time_array)
+        datetime_obj = datetime.fromtimestamp(time_array)
+
+        session = SqlQuery()
+        configModel = CRUD(DeviceConfig)
+        result = configModel.read(
+            session, conditions={"photo_create_time": datetime_obj}, order_by="id", ascending=True
+        )
+        if result:
+            return result.goods_art_no, result.image_index, result.image_deal_mode
+        else:
+            return None
+
+
+
+    def get_goods_art_no_info(self, numbers_list=None, goods_art_list=None, headers=None):
+        # 获取商品基础信息,入参为商品的编号
+        url = "{domain}/api/backend/goods_client/goods_query".format(
+            domain=settings.APP_HOST
+        )
+        data = {
+            'goods_art_list': goods_art_list
+        }
+        data = json.dumps(data)
+        _s = requests.session().post(url=url, data=data, headers=headers)
+        response_data = _s.json()
+
+
+        goods_number_data = {}
+        # ["", "", "", "", "", "", "", "", "", "", "", ]
+        if "data" not in response_data:
+            return {}
+
+        for data in response_data["data"]:
+            goods_number_data[data["goods_art_no"]] = {}
+            goods_number_data[data["goods_art_no"]]["商品货号"] = data["goods_art_no"].upper()
+            goods_number_data[data["goods_art_no"]]["款号"] = data["goods_number"].upper()
+            goods_number_data[data["goods_art_no"]]["商品面料"] = data["fabric"]
+            goods_number_data[data["goods_art_no"]]["商品内里"] = data["lining"]
+            goods_number_data[data["goods_art_no"]]["商品鞋底"] = data["sole"]
+            goods_number_data[data["goods_art_no"]]["鞋垫"] = data["insole"]
+            goods_number_data[data["goods_art_no"]]["颜色名称"] = data["color"]
+
+        return goods_number_data
+
+    def get_data_from_hqt_with_goods_art_no(self, goods_art_no_list):
+        _goods_art_no_list = copy.deepcopy(goods_art_no_list)
+        _list = []
+        # 单次请求数少于20个
+        goods_art_no_dict = {}
+
+        while _goods_art_no_list:
+            goods_art_no = _goods_art_no_list.pop()
+
+            _list.append(goods_art_no)
+            if len(_list) == 20 or len(_goods_art_no_list) == 0:
+                online_goods_art_data = self.get_goods_art_no_info(
+                    goods_art_list=_list
+                )
+                if online_goods_art_data:
+                    for _goods_art_no in online_goods_art_data:
+                        goods_art_no_dict[_goods_art_no] = online_goods_art_data[
+                            _goods_art_no
+                        ]
+                _list = []
+        return goods_art_no_dict
+
+
+
+
+    def get_goods_art_no_info(self, numbers_list=None, goods_art_list=None, headers=None):
+        # 获取商品基础信息,入参为商品的编号
+        url = "{domain}/api/backend/goods_client/goods_query".format(
+            domain=settings.APP_HOST
+        )
+        data = {
+            'goods_art_list': goods_art_list
+        }
+
+
+        data = json.dumps(data)
+
+
+        _s = requests.session().post(url=url, data=data, headers=headers)
+        # _s = self.s.get(url=url, params=params, headers=settings.Headers)
+        response_data = _s.json()
+
+
+        goods_number_data = {}
+        # ["", "", "", "", "", "", "", "", "", "", "", ]
+        if "data" not in response_data:
+            return {}
+
+        for data in response_data["data"]:
+            goods_number_data[data["goods_art_no"]] = {}
+            goods_number_data[data["goods_art_no"]]["商品货号"] = data["goods_art_no"].upper()
+            goods_number_data[data["goods_art_no"]]["款号"] = data["goods_number"].upper()
+            goods_number_data[data["goods_art_no"]]["商品面料"] = data["fabric"]
+            goods_number_data[data["goods_art_no"]]["商品内里"] = data["lining"]
+            goods_number_data[data["goods_art_no"]]["商品鞋底"] = data["sole"]
+            goods_number_data[data["goods_art_no"]]["鞋垫"] = data["insole"]
+            goods_number_data[data["goods_art_no"]]["颜色名称"] = data["color"]
+
+        return goods_number_data
+
+
+
+
+    def get_data_from_hqt(self, goods_number_list):
+        _goods_number_list = copy.deepcopy(goods_number_list)
+        _list = []
+        # 单次请求数少于20个
+        goods_number_dict = {}
+
+        while _goods_number_list:
+            goods_art_no = _goods_number_list.pop()
+            if "NUM" in goods_art_no:
+                goods_art_no = goods_art_no.replace("NUM", "")
+
+            _list.append(goods_art_no)
+            if len(_list) == 20 or len(_goods_number_list) == 0:
+                online_goods_art_data = self.get_goods_art_no_info(
+                    numbers_list=_list
+                )
+                if online_goods_art_data:
+                    for number in online_goods_art_data:
+                        goods_number_dict["NUM" + number] = online_goods_art_data[
+                            number
+                        ]
+                _list = []
+        return goods_number_dict
+
+
+
+
+
+    def dealMoveImage(self, image_dir: str, callback_func=None) -> dict:
+        if not self.check_path(image_dir=image_dir + "/历史"):
+            return {'code': 1, 'msg': '文件夹创建失败', 'data': {}}
+
+        # 遍历目标文件夹,获取有拍摄信息的图片,并按拍摄时间排序
+        files = self.list_dir(image_dir)
+        original_photo_list = []  # 原始图片列表
+        for file in files:
+            # -----图片清洗
+            file_path = image_dir + "/" + file
+            if os.path.isdir(file_path):  # 忽略文件夹
+                continue
+            file_name, suffix = os.path.splitext(file)
+            if suffix not in _Type:  # 非图片进行移除
+                shutil.move(file_path, image_dir + "/历史/" + file)
+                continue
+
+            date_time_original = self.get_date_time_original(file_path)  # 获取照片拍照时间
+            if date_time_original:
+                # 基于照片的时间,与数据库匹配goods_art_no
+                _data = self.get_goods_art_no(date_time_original)
+                if _data:
+                    # 能匹配上数据库
+                    goods_art_no, image_index, image_deal_mode = _data
+                    print("832 与数据库匹配goods_art_no", file_name, date_time_original, goods_art_no)
+                    original_photo_list.append({"file_path": file_path,
+                                                "file": file,
+                                                "date_time_original": date_time_original,
+                                                "goods_art_no": goods_art_no,
+                                                "image_index": image_index,
+                                                "real_goods_art_no": "",
+                                                "real_goods_number": "",
+                                                })
+                else:
+                    # 匹配不上报错
+                    # self.show_progress_detail("图片:{} 无法对应货号,不做处理".format(file))
+                    if callback_func:
+                        callback_func("图片:{} 无对应货号".format(file))
+                    # shutil.move(photo_dict["file_path"], self.image_dir + "/历史/" + photo_dict["file"])
+                    continue
+            else:
+                shutil.move(file_path, image_dir + "/历史/" + file)
+
+        if not original_photo_list:
+            return {"code": 1, "msg": "没有任何匹配的图片", 'data': {}}
+
+        if settings.PROJECT == "红蜻蜓":
+            # 批量请求货号图信息
+            goods_art_no_list = [x["goods_art_no"] for x in original_photo_list]
+            goods_art_no_list = list(set(goods_art_no_list))
+            goods_art_no_list = [x for x in goods_art_no_list if "NUM" not in x]
+
+            if goods_art_no_list:
+                goods_art_no_dict = self.get_data_from_hqt_with_goods_art_no(
+                    goods_art_no_list=goods_art_no_list)
+
+                for i in original_photo_list:
+                    if i["goods_art_no"] in goods_art_no_dict:
+                        i["real_goods_art_no"] = i["goods_art_no"]
+                        i["real_goods_number"] = "NUM{}".format(goods_art_no_dict[i["goods_art_no"]]["编号"])
+
+            # 批量请求编号对应信息
+            goods_number_list = [x["goods_art_no"] for x in original_photo_list]
+            goods_number_list = list(set(goods_number_list))
+            goods_number_list = [x for x in goods_number_list if "NUM" in x]
+
+            if goods_number_list:
+                goods_number_dict = self.get_data_from_hqt(goods_number_list=goods_number_list)
+                for i in original_photo_list:
+                    if i["goods_art_no"] in goods_number_dict:
+                        i["real_goods_number"] = i["goods_art_no"]
+                        i["real_goods_art_no"] = goods_number_dict[i["goods_art_no"]]["商品货号"]
+
+        # 排序需要基于拍照的文件序号进行处理
+        original_photo_list.sort(
+            key=lambda x: "{}-{}-{}".format(x["goods_art_no"], x["image_index"], x["file"]))
+
+        # print(original_photo_list)
+        # 对有拍摄信息的图片进行数据库比对,如有比对上,则移动至货号文件夹,否则移入历史文件夹
+        total_num = len(original_photo_list)
+        # 当天日期作为文件夹
+        seconds = time.time()
+        output_path = "output/{f_name}".format(f_name=time.strftime("%Y-%m-%d", time.localtime(seconds)))
+
+        # 遍历每个匹配好的数据进行处理
+        n = 0
+        for photo_dict in original_photo_list:
+            n += 1
+            # 进度条
+            goods_art_no = photo_dict["goods_art_no"]
+            original_image_path = photo_dict["file_path"]
+            # 输出货号文件夹
+            if photo_dict["real_goods_art_no"]:
+                goods_art_no = "{}@{}".format(photo_dict["real_goods_art_no"], photo_dict["real_goods_number"])
+
+            goods_art_no_path = "{output_path}/{goods_art_no}".format(output_path=output_path,
+                                                                      goods_art_no=goods_art_no)
+
+            # 创建货号下的一系列文件夹
+            self.create_folder(goods_art_no_path)
+
+            # 重命名并进行移动
+            print("开始移动:{}  {} 命名为:{}".format(goods_art_no, original_image_path, goods_art_no_path))
+            self.move_images(goods_art_no, goods_art_no_path, original_image_path)  # 货号、货号文件路径、原始图路径
+            time.sleep(0.2)
+
+
+            # self.progress_sign.emit({"type": "移动原始图片", "progress_bar_value": int(n / total_num * 100)})
+            # self.show_progress_detail("货号{} 相关文件夹创建完成,已移动原图~".format(goods_art_no))
+            if callback_func:
+                callback_func("货号{} 相关文件夹创建完成,已移动原图~".format(goods_art_no))
+
+            print("已完成移动处理")
+
+        if n != 0:
+            # if settings.MattingPics:
+            #     # 检查所有未处理的货号文件夹,查看是否有完成图片加工处理
+            #     self.deal_images()
+
+            # 自动生成一个货号表
+            print("output_path", output_path)
+            GenerateGoodsArtNoTable.deal(output_path)
+
+        # 完成处理
+        # self.set_state(state_value=2)
+        return {'code': 0, 'msg': '处理完成', 'target_path': output_path, 'data': {}}
+
+    def check_path(self, image_dir: str):
+        if not os.path.exists(image_dir):
+            os.mkdir(image_dir)
+        return True
+
+    def get_date_time_original(self, file_path):
+        with open(file_path, 'rb') as file_data:
+            tags = exifread.process_file(file_data)
+            if "EXIF DateTimeOriginal" in tags:
+                return str(tags["EXIF DateTimeOriginal"])
+            else:
+                return False
+
+    def create_folder(self, path):
+        def check_folder(__path):
+            if not os.path.exists(__path):
+                os.makedirs(__path)
+                return False
+            return True
+
+        # 文件夹不存在,创建货号子集文件夹
+        if not check_folder(path):
+            for name in ["原始图", "原始图_已抠图", "800x800", "200images"]:
+                other_path = path + "/" + name
+                check_folder(other_path)
+
+    def move_images(self, goods_art_no, goods_art_no_path, old_image_path):
+        """
+        步骤:
+        1、移动到原始图
+        Args:
+            goods_art_no:
+            goods_art_no_path:
+            old_image_path:
+
+        Returns:
+
+        """
+        # 移动到原始图
+        file = os.path.split(old_image_path)[1]
+        # 扩展名
+        e = os.path.splitext(file)[1]
+        # 获取图片序列
+        self.goods_images_count_dict[goods_art_no] += 1
+        # A9999(1).jpg
+        new_file_name = "{}({})".format(goods_art_no, self.goods_images_count_dict[goods_art_no])
+        original_image_path = "{}/原始图/{}{}".format(goods_art_no_path, new_file_name, e)
+        # 移动图片
+        shutil.move(old_image_path, original_image_path)
+
+    def pixianRemoveImageBg(self, file_path: str, out_file_path: str, callbackek_func=None):
+        url = self.dataModeMatchPhoto.get_online_data.uploadImage(local_path=file_path)
+
+        remonveUrl = settings.AIGC_DOMAIN + '/api/ai_image/main/remove_background'
+        param = {'base_image': url}
+        post_headers = {"Authorization": settings.Authorization,
+                        "Content-Length": "",
+                        "Content-Type": "application/json",
+                        "Accept": "application/json"}
+
+        result = requests.post(remonveUrl, data=json.dumps(param), headers=post_headers).json()
+        print(result)
+        if "code" in result and result['code'] == 0:
+            response = requests.get(result['data']['image'][0])
+            with open(out_file_path, 'wb') as file:
+                file.write(response.content)
+                return result['data']['image'][0]
+        else:
+            callbackek_func("精细化抠图处理失败 {}".format(result['message']))
+            return ''
+
+    def list_dir(self, path):
+        listdir = os.listdir(path)
+        return natsorted(listdir, alg=ns.PATH)

+ 28 - 0
python/service/data.py

@@ -0,0 +1,28 @@
+import settings
+from module.data_mode.data_metaclass import DataBaseModel
+from PIL import Image
+from io import BytesIO
+
+
+class DataModeAutoDealPics(DataBaseModel):
+    def __init__(self):
+        super().__init__()
+
+    def check_is_right_foot_by_api(self, image):
+        image = image.convert('RGB')
+
+        re_x = int(640)
+        re_y = int(image.height * re_x / image.width)
+        image = image.resize((re_x, re_y))
+        e = "JPEG"
+
+        img = BytesIO()
+        image.save(img, format=e)  # format: PNG or JPEG
+        img.seek(0)  # rewind to the start
+
+        image_url = self.get_online_data.upload_image_by_io(image_io=img)
+        if settings.IS_TEST:
+            print("识别左右脚,{}".format(image_url))
+        # 识别左右脚
+        r_data = self.get_online_data.yolo_shoes_category(image_url=image_url)
+        return r_data

+ 133 - 0
python/service/deal_cutout.py

@@ -0,0 +1,133 @@
+import os.path
+import time
+from concurrent.futures import ThreadPoolExecutor, wait
+import threading
+
+from deal_one_image import DealOneImage, DealOneImageBeforehand
+from module.log.log import MyLogger
+from module.online_request.module_online_data import GetOnlineDataHLM
+
+
+class DealCutout(QThread):
+    signal_data = Signal(dict)
+
+    def __init__(self, windows):
+        super().__init__()
+        self.windows = windows
+        self.lock = threading.Lock()
+        self.need_cutout_images = {}
+        self.state = 2  # 1进行中 2停止  状态 用于中途取消, 3已结束
+        # 当前阿里预处理后的图片数量(未消费)
+        self.is_upload_pic_num = 0
+        self.is_deal_num = 0
+        # 图片列表
+        self.upload_pic_dict = {}
+        self.logger = MyLogger().logger
+
+        self.remaining_times = 0  # 剩余总次数
+        self.get_online_data = GetOnlineDataHLM()
+
+        self.resultData = [] # 最终的结果
+
+    def refresh_times(self, is_online=True, remaining_times=None):
+        # 刷新剩余次数
+        if remaining_times is not None:
+            self.remaining_times = remaining_times
+
+        if is_online:
+            _ = self.get_online_data.get_cutout_image_times()
+            if _ is False:
+                self.remaining_times = 0
+            else:
+                if "balance" in _:
+                    self.remaining_times = _["balance"]
+
+        if self.remaining_times <= 0:
+            return False
+        return True
+
+    def send_sign(self, data):
+        # show_info    complete
+        self.signal_data.emit(data)
+
+    def check_before(self):
+        self.refresh_times()
+        if self.remaining_times <= 0:
+            self.send_sign({"type": "show_info",
+                            "message": "精细化抠图余量不足",
+                            })
+            return False
+
+        return True
+
+    def run(self):
+        """
+        need_cutout_images 结构:
+        [
+        "file_name": file_name, # 文件名
+        "file_e": file_e, # 后缀,.jpg
+        "file_path": image_path, # 完整路径
+        "file": file, # 图片文件名,带后缀
+        "need_cutout": True,# 必须,需要抠图
+        "out_path":图片输出路径
+        ]
+        """
+        if not self.check_before():
+            self.signal_data.emit({"_type": "complete",
+                                   "data": []})
+            self.state = 3
+            return
+
+        executor = ThreadPoolExecutor(max_workers=4)
+        executor_pic_upload = ThreadPoolExecutor(max_workers=2)
+
+        tasks_1 = []
+        tasks_2 = []
+        self.state = 1
+        self.resultData = []
+        self.is_upload_pic_num = 0
+        self.is_deal_num = 0
+        num = 0
+        for image_data in self.need_cutout_images:
+            if not image_data["need_cutout"]:
+                continue
+            num += 1
+            task_1 = executor.submit(DealOneImage(image_data=image_data, lock=self.lock, windows=self, num=num).run)
+            tasks_1.append(task_1)
+
+            task_2 = executor_pic_upload.submit(
+                DealOneImageBeforehand(image_data=image_data, lock=self.lock, windows=self, num=num).run)
+            tasks_2.append(task_2)
+        self.check_thread(tasks_1, tasks_2)
+
+    def __del__(self):
+        self.state = 2
+
+    def check_thread(self, *tasks_list):
+        time.sleep(2)
+        success_image_path = []
+        while 1:
+            f = True
+            for tasks in tasks_list:
+                done, not_done = wait(tasks)
+                if not_done:
+                    time.sleep(2)
+                    f = False
+                    continue
+
+                for task in done:
+                    try:
+                        image_path = task.result()
+                        if image_path:
+                            if isinstance(image_path, str):
+                                if os.path.exists(image_path):
+                                    success_image_path.append(image_path)
+                    except BaseException as e:
+                        self.logger.info("有线程出错:{}".format(e))
+            if f:
+                break
+
+        self.signal_data.emit({"_type": "complete",
+                               "data": success_image_path})
+        self.resultData = success_image_path
+        self.state = 3

+ 437 - 0
python/service/deal_image.py

@@ -0,0 +1,437 @@
+
+import os
+
+from  natsort import natsorted,ns
+import shutil
+import exifread
+import time
+import datetime
+from databases import DeviceConfig,SqlQuery,CRUD
+import settings
+import copy
+import json
+import requests
+from service.pic_deal import Picture
+import xlsxwriter
+from PIL import Image
+
+_Type = ['.png', '.PNG', '.jpg', '.JPG', '.gif', '.GIF', ".jpge", ".JPGE"]
+
+class DealImage():
+
+
+
+
+    def __init__(self, image_dir=None):
+        self.image_dir = image_dir
+        self.header = None
+    def check_path(self, image_dir: str):
+        if not os.path.exists(image_dir):
+            os.mkdir(image_dir)
+        return True
+    def list_dir(self, path):
+        listdir = os.listdir(path)
+        return natsorted(listdir, alg=ns.PATH)
+
+    def get_date_time_original(self, file_path):
+        with open(file_path, 'rb') as file_data:
+            tags = exifread.process_file(file_data)
+            if "EXIF DateTimeOriginal" in tags:
+                return str(tags["EXIF DateTimeOriginal"])
+            else:
+                return False
+
+    def get_goods_art_no(self, date_time_original):
+        time_array = time.strptime(date_time_original, "%Y:%m:%d %H:%M:%S")
+
+        time_array = time.mktime(time_array)
+        datetime_obj = datetime.fromtimestamp(time_array)
+
+        session = SqlQuery()
+        configModel = CRUD(DeviceConfig)
+        result = configModel.read(
+            session, conditions={"photo_create_time": datetime_obj}, order_by="id", ascending=True
+        )
+        if result:
+            return result.goods_art_no, result.image_index, result.image_deal_mode
+        else:
+            return None
+
+
+
+    def get_data_from_hqt_with_goods_art_no(self, goods_art_no_list):
+        _goods_art_no_list = copy.deepcopy(goods_art_no_list)
+        _list = []
+        # 单次请求数少于20个
+        goods_art_no_dict = {}
+
+        while _goods_art_no_list:
+            goods_art_no = _goods_art_no_list.pop()
+
+            _list.append(goods_art_no)
+            if len(_list) == 20 or len(_goods_art_no_list) == 0:
+                online_goods_art_data = self.get_goods_art_no_info(
+                    goods_art_list=_list
+                )
+                if online_goods_art_data:
+                    for _goods_art_no in online_goods_art_data:
+                        goods_art_no_dict[_goods_art_no] = online_goods_art_data[
+                            _goods_art_no
+                        ]
+                _list = []
+        return goods_art_no_dict
+
+    def get_goods_art_no_info(self, numbers_list=None, goods_art_list=None):
+        # 获取商品基础信息,入参为商品的编号
+        url = "{domain}/api/backend/goods_client/goods_query".format(
+            domain=settings.APP_HOST
+        )
+        data = {
+            'goods_art_list': goods_art_list
+        }
+        data = json.dumps(data)
+        _s = requests.session().post(url=url, data=data, headers=self.header)
+        response_data = _s.json()
+
+
+        goods_number_data = {}
+        # ["", "", "", "", "", "", "", "", "", "", "", ]
+        if "data" not in response_data:
+            return {}
+
+        for data in response_data["data"]:
+            goods_number_data[data["goods_art_no"]] = {}
+            goods_number_data[data["goods_art_no"]]["商品货号"] = data["goods_art_no"].upper()
+            goods_number_data[data["goods_art_no"]]["款号"] = data["goods_number"].upper()
+            goods_number_data[data["goods_art_no"]]["商品面料"] = data["fabric"]
+            goods_number_data[data["goods_art_no"]]["商品内里"] = data["lining"]
+            goods_number_data[data["goods_art_no"]]["商品鞋底"] = data["sole"]
+            goods_number_data[data["goods_art_no"]]["鞋垫"] = data["insole"]
+            goods_number_data[data["goods_art_no"]]["颜色名称"] = data["color"]
+
+        return goods_number_data
+
+
+    def get_data_from_hqt(self, goods_number_list):
+        _goods_number_list = copy.deepcopy(goods_number_list)
+        _list = []
+        # 单次请求数少于20个
+        goods_number_dict = {}
+
+        while _goods_number_list:
+            goods_art_no = _goods_number_list.pop()
+            if "NUM" in goods_art_no:
+                goods_art_no = goods_art_no.replace("NUM", "")
+
+            _list.append(goods_art_no)
+            if len(_list) == 20 or len(_goods_number_list) == 0:
+                online_goods_art_data = self.get_goods_art_no_info(
+                    numbers_list=_list
+                )
+                if online_goods_art_data:
+                    for number in online_goods_art_data:
+                        goods_number_dict["NUM" + number] = online_goods_art_data[
+                            number
+                        ]
+                _list = []
+        return goods_number_dict
+
+    def create_folder(self, path):
+        def check_folder(__path):
+            if not os.path.exists(__path):
+                os.makedirs(__path)
+                return False
+            return True
+
+        # 文件夹不存在,创建货号子集文件夹
+        if not check_folder(path):
+            for name in ["原始图", "原始图_已抠图", "800x800", "200images"]:
+                other_path = path + "/" + name
+                check_folder(other_path)
+
+
+
+    def move_images(self, goods_art_no, goods_art_no_path, old_image_path):
+        """
+        步骤:
+        1、移动到原始图
+        Args:
+            goods_art_no:
+            goods_art_no_path:
+            old_image_path:
+
+        Returns:
+
+        """
+        # 移动到原始图
+        file = os.path.split(old_image_path)[1]
+        # 扩展名
+        e = os.path.splitext(file)[1]
+        # 获取图片序列
+        self.goods_images_count_dict[goods_art_no] += 1
+        # A9999(1).jpg
+        new_file_name = "{}({})".format(goods_art_no, self.goods_images_count_dict[goods_art_no])
+        original_image_path = "{}/原始图/{}{}".format(goods_art_no_path, new_file_name, e)
+        # 移动图片
+        shutil.move(old_image_path, original_image_path)
+
+    def dealMoveImage(self, image_dir: str, callback_func=None) -> dict:
+        if not self.check_path(image_dir=image_dir + "/历史"):
+            return {'code': 1, 'msg': '文件夹创建失败', 'data': {}}
+
+        # 遍历目标文件夹,获取有拍摄信息的图片,并按拍摄时间排序
+        files = self.list_dir(image_dir)
+        original_photo_list = []  # 原始图片列表
+        for file in files:
+            # -----图片清洗
+            file_path = image_dir + "/" + file
+            if os.path.isdir(file_path):  # 忽略文件夹
+                continue
+            file_name, suffix = os.path.splitext(file)
+            if suffix not in _Type:  # 非图片进行移除
+                shutil.move(file_path, image_dir + "/历史/" + file)
+                continue
+
+            date_time_original = self.get_date_time_original(file_path)  # 获取照片拍照时间
+            if date_time_original:
+                # 基于照片的时间,与数据库匹配goods_art_no
+                _data = self.get_goods_art_no(date_time_original)
+                if _data:
+                    # 能匹配上数据库
+                    goods_art_no, image_index, image_deal_mode = _data
+                    print("832 与数据库匹配goods_art_no", file_name, date_time_original, goods_art_no)
+                    original_photo_list.append({"file_path": file_path,
+                                                "file": file,
+                                                "date_time_original": date_time_original,
+                                                "goods_art_no": goods_art_no,
+                                                "image_index": image_index,
+                                                "real_goods_art_no": "",
+                                                "real_goods_number": "",
+                                                })
+                else:
+                    # 匹配不上报错
+                    # self.show_progress_detail("图片:{} 无法对应货号,不做处理".format(file))
+                    if callback_func:
+                        callback_func("图片:{} 无对应货号".format(file))
+                    # shutil.move(photo_dict["file_path"], self.image_dir + "/历史/" + photo_dict["file"])
+                    continue
+            else:
+                shutil.move(file_path, image_dir + "/历史/" + file)
+
+        if not original_photo_list:
+            return {"code": 1, "msg": "没有任何匹配的图片", 'data': {}}
+
+        if settings.PROJECT == "红蜻蜓":
+            # 批量请求货号图信息
+            goods_art_no_list = [x["goods_art_no"] for x in original_photo_list]
+            goods_art_no_list = list(set(goods_art_no_list))
+            goods_art_no_list = [x for x in goods_art_no_list if "NUM" not in x]
+
+            if goods_art_no_list:
+                goods_art_no_dict = self.get_data_from_hqt_with_goods_art_no(
+                    goods_art_no_list=goods_art_no_list)
+
+                for i in original_photo_list:
+                    if i["goods_art_no"] in goods_art_no_dict:
+                        i["real_goods_art_no"] = i["goods_art_no"]
+                        i["real_goods_number"] = "NUM{}".format(goods_art_no_dict[i["goods_art_no"]]["编号"])
+
+            # 批量请求编号对应信息
+            goods_number_list = [x["goods_art_no"] for x in original_photo_list]
+            goods_number_list = list(set(goods_number_list))
+            goods_number_list = [x for x in goods_number_list if "NUM" in x]
+
+            if goods_number_list:
+                goods_number_dict = self.get_data_from_hqt(goods_number_list=goods_number_list)
+                for i in original_photo_list:
+                    if i["goods_art_no"] in goods_number_dict:
+                        i["real_goods_number"] = i["goods_art_no"]
+                        i["real_goods_art_no"] = goods_number_dict[i["goods_art_no"]]["商品货号"]
+
+        # 排序需要基于拍照的文件序号进行处理
+        original_photo_list.sort(
+            key=lambda x: "{}-{}-{}".format(x["goods_art_no"], x["image_index"], x["file"]))
+
+        # print(original_photo_list)
+        # 对有拍摄信息的图片进行数据库比对,如有比对上,则移动至货号文件夹,否则移入历史文件夹
+        total_num = len(original_photo_list)
+        # 当天日期作为文件夹
+        seconds = time.time()
+        output_path = "output/{f_name}".format(f_name=time.strftime("%Y-%m-%d", time.localtime(seconds)))
+
+        # 遍历每个匹配好的数据进行处理
+        n = 0
+        for photo_dict in original_photo_list:
+            n += 1
+            # 进度条
+            goods_art_no = photo_dict["goods_art_no"]
+            original_image_path = photo_dict["file_path"]
+            # 输出货号文件夹
+            if photo_dict["real_goods_art_no"]:
+                goods_art_no = "{}@{}".format(photo_dict["real_goods_art_no"], photo_dict["real_goods_number"])
+
+            goods_art_no_path = "{output_path}/{goods_art_no}".format(output_path=output_path,
+                                                                      goods_art_no=goods_art_no)
+
+            # 创建货号下的一系列文件夹
+            self.create_folder(goods_art_no_path)
+
+            # 重命名并进行移动
+            print("开始移动:{}  {} 命名为:{}".format(goods_art_no, original_image_path, goods_art_no_path))
+            self.move_images(goods_art_no, goods_art_no_path, original_image_path)  # 货号、货号文件路径、原始图路径
+            time.sleep(0.2)
+
+
+            # self.progress_sign.emit({"type": "移动原始图片", "progress_bar_value": int(n / total_num * 100)})
+            # self.show_progress_detail("货号{} 相关文件夹创建完成,已移动原图~".format(goods_art_no))
+            if callback_func:
+                callback_func("货号{} 相关文件夹创建完成,已移动原图~".format(goods_art_no))
+
+            print("已完成移动处理")
+
+        if n != 0:
+            # if settings.MattingPics:
+            #     # 检查所有未处理的货号文件夹,查看是否有完成图片加工处理
+            #     self.deal_images()
+
+            # 自动生成一个货号表
+            print("output_path", output_path)
+            self.deal(output_path)
+
+        # 完成处理
+        # self.set_state(state_value=2)
+        return {'code': 0, 'msg': '处理完成', 'target_path': output_path, 'data': {}}
+
+
+
+    def deal(cls, dir_path):
+
+        # print("dir_path", dir_path)
+        out_excel_data = []
+        for goods_art_no_folder in os.listdir(dir_path):  # 遍历货号文件夹集合
+            if not os.path.isdir(
+                    "{}/{}".format(dir_path, goods_art_no_folder)
+            ):  # 非文件夹进行过滤
+                continue
+            if "软件" in goods_art_no_folder:
+                continue
+
+            # print("goods_art_no_folder", goods_art_no_folder)
+            # 如果存在800的主图,则优先进行使用
+            big_image_folder_path = "{}/{}/800x800".format(
+                dir_path, goods_art_no_folder
+            )
+            if not os.path.exists(big_image_folder_path):
+                os.mkdir(big_image_folder_path)
+            all_big_images = os.listdir(big_image_folder_path)
+            goods_pic_total = len(all_big_images)
+            _Type = [".png", ".PNG", ".jpg", ".JPG", ".gif", ".GIF", ".jpge", ".JPGE"]
+            thumb_image_file_path = None
+            # print("all_big_images",all_big_images)
+            if all_big_images:
+                for _file in all_big_images:
+                    # print(_file)
+                    file_name, e = os.path.splitext(_file)
+                    # print(file_name, e)
+                    if e in _Type:
+                        thumb_image_file_path = "{}/{}/800x800/{}".format(
+                            dir_path, goods_art_no_folder, _file
+                        )
+                        break
+
+            # 如果不存在主图则进行使用原始图
+            if thumb_image_file_path is None:
+                _path = "{}/{}/原始图".format(dir_path, goods_art_no_folder)
+                if not os.path.exists(_path):
+                    continue
+
+                all_original_images = os.listdir(_path)  # 遍历货号原始图文件夹
+                goods_pic_total = len(all_original_images)
+                if not all_original_images:
+                    continue
+                image_file = all_original_images[0]  # 取第一个货号图
+                image_file_path = "{}/{}/原始图/{}".format(
+                    dir_path, goods_art_no_folder, image_file
+                )
+
+                if not os.path.exists(
+                        "{}/{}/200images".format(dir_path, goods_art_no_folder)
+                ):
+                    os.mkdir("{}/{}/200images".format(dir_path, goods_art_no_folder))
+
+                thumb_image_file_path = "{}/{}/200images/{}".format(
+                    dir_path, goods_art_no_folder, image_file
+                )
+                if not os.path.exists(thumb_image_file_path):
+                    # 开始触发进行压缩生成文件
+                    shutil.copy(image_file_path, thumb_image_file_path)  # 复制文件
+                    pic = Picture(thumb_image_file_path)
+                    pic.resize(width=600)
+                    pic.save_img(thumb_image_file_path)
+            # print("thumb_image_file_path", thumb_image_file_path)
+
+            goods_number = ""
+            if "@" in goods_art_no_folder:
+                _ = goods_art_no_folder.split("@")
+                goods_art_no_folder = _[0]
+                goods_number = _[1].replace("NUM", "")
+
+            out_excel_data.append(
+                [
+                    goods_number,
+                    goods_art_no_folder,
+                    thumb_image_file_path,
+                    goods_pic_total,
+                ]
+            )
+
+        if out_excel_data:
+            out_excel_path = "{}/货号表-{}.xlsx".format(dir_path, time.time())
+            options = {
+                "default_format_properties": {
+                    "align": "left",
+                    "valign": "vcenter",
+                    "text_wrap": True,
+                }
+            }
+            book = xlsxwriter.Workbook(filename=out_excel_path, options=options)
+            sheet = book.add_worksheet("sheet1")
+            # sheet.freeze_panes(1, 2)
+            sheet.set_column("B:B", 17)
+            sheet.set_column("D:D", 20)
+
+            sheet.write_row("A1", ["编号", "原货号", "新货号", "缩略图", "原始图张数"])
+            for index, data in enumerate(out_excel_data):
+                # print(data)
+                goods_number, goods_art_no, image_file, goods_pic_total = data
+                try:
+                    im = Image.open(image_file)
+                    im_x, im_y = im.size
+                    image_width = 100
+                    image_height = int(im_y * image_width / im_x)
+                    sheet.set_row(index + 1, 95)
+                    x_scale = round(
+                        image_width / im_x, 2
+                    )  # 固定宽度/要插入的原始图片宽
+                    y_scale = round(
+                        image_height / im_y, 2
+                    )  # 固定高度/要插入的原始图片高
+                    sheet.insert_image(
+                        index + 1,
+                        3,
+                        image_file,
+                        {
+                            "x_scale": x_scale,
+                            "y_scale": y_scale,
+                            "x_offset": 5,
+                            "y_offset": 5,
+                            "object_position":1,
+                        },
+                        )
+
+
+                except:
+                    pass
+                sheet.write_row("A{}".format(index + 2), [goods_number])
+                sheet.write_row("B{}".format(index + 2), [goods_art_no])
+                sheet.write_row("E{}".format(index + 2), [goods_pic_total])

+ 423 - 0
python/service/deal_one_image.py

@@ -0,0 +1,423 @@
+import copy
+import os.path
+# from module.other.module_online_data import GetOnlineData
+from module.online_request.module_online_data import GetOnlineDataHLM
+
+import time
+from module.log.log import MyLogger
+
+import os
+from PIL import Image
+# from module.other.remove_bg_ali import RemoveBgALi
+from module.base_mode.remove_bg_pixian import RemoveBgPiXian
+from module.base_mode.remove_bg_ali import RemoveBgALi
+from module.base_mode.remove_bg_ali import Picture
+import cv2
+import numpy as np
+import settings
+import math
+
+class Base(object):
+    def __init__(self, image_data, lock, windows, num):
+        self.lock = lock
+        self.windows = windows
+        self.image_data = image_data
+        self.num = num
+        self.get_online_data = GetOnlineDataHLM()
+        self.file_path = image_data["file_path"]
+        self.file_name = image_data["file_name"]
+        self.file = os.path.split(self.file_path)[1]
+        self.is_once_data = {}
+        self.logger = MyLogger().logger
+
+    def add_log(self, text, _type="info"):
+        self.logger.info("第{}个,图片名称:{},内容:{}".format(self.num, self.file, text))
+
+    def send_info(self, text="", is_success=None, _type="show_info", need_point_return=False):
+        with self.lock:
+            if is_success is not None:
+                if is_success:
+                    self.windows.remaining_times = self.windows.remaining_times - 1
+                else:
+                    # 分数返回
+                    if need_point_return:
+                        # 分数返回
+                        if self.is_once("add_point"):
+                            print("第{}个,图片名称:{},内容:{}".format(self.num, self.file, "扣分返回"))
+                            self.dispose_point(_type="add")
+                            self.windows.remaining_times = self.windows.remaining_times + 1
+
+            if text:
+                data = {"_type": _type,
+                        "data": text,
+                        }
+                self.windows.send_sign(data)
+
+    def refresh_times(self, cumulative_frequency_times_change):
+        if cumulative_frequency_times_change > 0:
+            self.windows.remaining_times = self.windows.remaining_times - 1
+
+    def check_path(self, _path):
+        if not os.path.exists(_path):
+            os.mkdir(_path)
+        return True
+
+    def is_once(self, key):
+        if key not in self.is_once_data:
+            self.is_once_data[key] = 0
+            return True
+        return False
+
+    def dispose_point(self, _type):
+        n = 3
+        while n:
+            n -= 1
+            try:
+                _r = self.get_online_data.dispose_point(_type)
+                balance = _r["data"]["balance"]
+                return True
+            except:
+                time.sleep(0.5)
+                continue
+
+        return False
+
+
+class DealOneImage(Base):
+    def __init__(self, image_data, lock, windows, num):
+        super().__init__(image_data, lock, windows, num)
+        self.image_data = image_data
+        self.lock = lock
+        self.windows = windows
+        self.num = num
+        self.file_path = image_data["file_path"]
+        self.file = os.path.split(self.file_path)[1]
+        self.r_pixian = RemoveBgPiXian()
+        self.file_name = image_data["file_name"]
+        self.out_path = image_data["out_path"]
+
+    def run(self):
+        # 直接调用抠图
+        # 1、增加获取key,2、key需要加密、3、429报错 重试再来拿一个KEY
+        self.add_log("开始处理")
+        self.send_info(text="{} 处理中".format(self.file_name))
+
+        if self.windows.remaining_times <= 0:
+            self.send_info(text="次数不足,处理失败", is_success=False)
+            return
+
+        # 检查图片上传是否有结束
+        n = 60
+        while 1:
+            if self.windows.state != 1:
+                return
+            n -= 1
+            if self.file_path in self.windows.upload_pic_dict:
+                break
+
+            else:
+                time.sleep(1)
+                if n <= 0:
+                    self.send_info(text="{} 处理超时", is_success=False)
+                    return
+                continue
+
+        s = time.time()
+        # print(self.upload_pic_dict[file_path])
+        _flag = self.windows.upload_pic_dict[self.file_path]["flag"]
+        if not _flag:
+            self.add_log("未查到上传的图片地址")
+            self.send_info(text="{} 上传错误", is_success=False)
+            return
+
+        image_deal_info = self.windows.upload_pic_dict[self.file_path]["image_deal_info"]
+        original_im = self.windows.upload_pic_dict[self.file_path]["_im"]
+
+        self.add_log("抠图中")
+
+        with self.lock:
+            self.windows.is_upload_pic_num -= 1
+
+        try:
+            balance = self.get_online_data.get_cutout_image_times()["balance"]
+            self.add_log("查询balance:{}成功".format(balance))
+            if balance <= 0:
+                self.add_log("次数不足,处理失败")
+                self.send_info(text="次数不足,处理失败", is_success=False)
+                return
+        except:
+            self.add_log("查询balance失败")
+            self.send_info(text="查询balance失败", is_success=False)
+            return
+
+        n = 0
+        while 1:
+            # 获取key
+            if self.windows.state != 1:
+                return
+            n += 1
+            data = self.get_online_data.get_key_secret()
+            key = (data["api_info"]["api_key"], data["api_info"]["api_serect"])
+            self.add_log("查询key成功")
+
+            if not key:
+                _data = {"text": "出错/超时",
+                         "info": "多次获取key失败",
+                         }
+                self.add_log(text="多次获取key失败")
+                self.send_info(text="{} 处理失败,请联系管理员".format(self.file_name), is_success=False)
+                return
+
+            if self.is_once("sub_point"):
+                # 调用扣分
+                with self.lock:
+
+                    self.refresh_times(cumulative_frequency_times_change=1)
+                    f = self.dispose_point(_type="sub")
+                if not f:
+                    self.add_log(text="多次获取调用余额扣减失败")
+                    self.send_info(text="多次获取调用余额扣减失败", is_success=False)
+                    return
+
+            pixian_cutout_data = self.r_pixian.run_by_image_im(original_im, key)
+
+            if pixian_cutout_data["status_code"] == 200:
+                second_cut_image = pixian_cutout_data["im"]
+
+                self.add_log(text="调用抠图完成")
+                break
+
+            elif pixian_cutout_data["status_code"] == 402:
+                if n >= 2:
+                    self.add_log(text="多次抠图失败:{}".format(pixian_cutout_data["status_code"]))
+                    self.send_info(text="处理失败,请联系管理员", is_success=False, need_point_return=True)
+                    if self.is_once("余额不足报错"):
+                        # 余额不足报错,钉钉消息通知
+                        self.get_online_data.send_message("Pixian:{} 余额不足".format(key))
+                        pass
+                    return
+                self.add_log(text="抠图失败:{},延迟6秒".format(pixian_cutout_data["status_code"]))
+                time.sleep(6)
+                continue
+
+            elif pixian_cutout_data["status_code"] == 429:
+                if n >= 2:
+                    self.add_log(text="多次抠图失败:{}".format(pixian_cutout_data["status_code"]))
+                    self.send_info(text="处理失败,请联系管理员", is_success=False, need_point_return=True)
+                    return
+
+                self.add_log(text="{}抠图失败:{},延迟10秒".format(self.file_name, pixian_cutout_data["status_code"]))
+                time.sleep(10)
+                continue
+            else:
+                self.send_info(text="{} 处理失败,请联系管理员".format(self.file_name), is_success=False, need_point_return=True)
+                if "message" in pixian_cutout_data:
+                    text = "抠图异常,code:{},message:{}".format(pixian_cutout_data["status_code"],
+                                                            pixian_cutout_data["message"])
+                else:
+                    text = "抠图异常,code:{}".format(pixian_cutout_data["status_code"])
+                self.add_log(text)
+                return
+
+        # 拼接处理
+        # print("耗时1:", time.time() - s)
+
+        try:
+            if image_deal_info["二次抠图是否缩放"]:
+                # print("图片尺寸还原")
+                self.add_log(text="图片尺寸进行还原")
+                original_im = image_deal_info["抠图扩边后PIL对象"]
+                second_cut_image = self.picture_resize_to_original(second_cut_image, original_im)
+            # 创建空白图片并粘贴回去
+            _img_im = Image.new(mode="RGBA", size=image_deal_info["原始图片大小"], color=(0, 0, 0, 0))
+            _img_im.paste(second_cut_image, box=(image_deal_info["抠图扩边后位置"][0], image_deal_info["抠图扩边后位置"][1]))
+            _img_im.save(self.out_path)
+
+            self.send_info(text="{} 抠图已完成".format(self.file_name), is_success=True)
+            return self.file_path
+
+        except BaseException as e:
+            # print(e)
+            text = "{} 图片处理错误,代码44".format(e)
+            self.add_log(text)
+            self.send_info(text=text, is_success=False, need_point_return=True)
+            return
+
+    def picture_resize_to_original(self, _img, original_im):
+        """
+
+        Parameters
+        ----------
+        _img 需要还原的PIL对象
+        original_im 原图对象
+
+        Returns
+        -------
+
+        """
+
+        # 将抠图结果转成mask
+        # 将抠图结果放大到原始图大小
+        _img = _img.resize(original_im.size,resample=1)
+        new_big_mask = Image.new('RGB', _img.size, (0, 0, 0))
+        white = Image.new('RGB', _img.size, (255, 255, 255))
+        new_big_mask.paste(white, mask=_img.split()[3])
+
+        # ---------制作选区缩小的mask
+        mask = cv2.cvtColor(np.asarray(new_big_mask), cv2.COLOR_BGR2GRAY)  # 将PIL 格式转换为 CV对象
+        mask[mask != 255] = 0
+        # 黑白反转
+        # mask = 255 - mask
+        # 选区缩小10
+        kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (10, 10))
+        erode_im = cv2.morphologyEx(mask, cv2.MORPH_ERODE, kernel)
+
+        # -------再进行抠图处理
+        mask = Image.fromarray(cv2.cvtColor(erode_im, cv2.COLOR_GRAY2RGBA))  # CV 对象转 PIL
+        transparent_im = Image.new('RGBA', original_im.size, (0, 0, 0, 0))
+        transparent_im.paste(original_im, (0, 0), mask.convert('L'))
+        # 上述抠图结果进行拼接
+        _img.paste(transparent_im, (0, 0), transparent_im)
+
+        return _img
+
+
+class DealOneImageBeforehand(Base):
+    def __init__(self, image_data, lock, windows, num):
+        super().__init__(image_data, lock, windows, num)
+        self.image_data = image_data
+        self.lock = lock
+        self.windows = windows
+        self.get_online_data = GetOnlineDataHLM()
+        self.num = num
+        self.file_path = image_data["file_path"]
+        self.file = os.path.split(self.file_path)[1]
+        self.r_ali = RemoveBgALi()
+        self.file_name = image_data["file_name"]
+
+    def run(self):
+        while 1:
+            if self.windows.state != 1:
+                return
+
+            with self.lock:
+                if self.windows.is_upload_pic_num >= 4:
+                    f = False
+                else:
+                    self.windows.is_upload_pic_num += 1
+                    f = True
+            if f:
+                break
+            else:
+                time.sleep(1)
+                continue
+        image_deal_info = {}
+        try:
+            cut_image, image_deal_info = self.get_image_cut()
+
+            f = True
+            url = ""
+        except BaseException as e:
+            f = False
+            url = ""
+            cut_image = ""
+
+        with self.lock:
+            self.windows.upload_pic_dict[self.file_path] = {"url": url,
+                                                            "image_deal_info": image_deal_info,
+                                                            "flag": f,
+                                                            "_im": cut_image, }
+
+    def get_image_cut(self):
+        original_pic = Picture(self.file_path)
+        original_pic.im = self.get_image_orientation(original_pic.im)
+        original_pic.x, original_pic.y = original_pic.im.size
+
+        original_pic.im = original_pic.im.convert("RGB")
+        image_deal_info = {}
+        image_deal_info["原始图片大小"] = (original_pic.x, original_pic.y)
+
+        # 原始图过小,则不需要使用阿里进行预处理
+        if original_pic.x * original_pic.y < 1000000:
+            cut_image = original_pic.im
+            image_deal_info["抠图扩边后图片大小"] = cut_image.size
+            image_deal_info["二次抠图是否缩放"] = False
+            image_deal_info["抠图扩边后位置"] = (0, 0, original_pic.x, original_pic.y)
+        else:
+            self.add_log("开始预抠图处理")
+            cut_image = self.r_ali.get_image_cut(file_path=None, out_file_path=None, original_im=original_pic.im)
+
+            self.add_log("预抠图处理结束")
+
+            x1, y1, x2, y2 = cut_image.getbbox()
+            image_deal_info["鞋子原始位置"] = (x1, y1, x2, y2)
+            o_w, o_h = cut_image.size
+            image_deal_info["鞋子原始抠图后大小"] = (o_w, o_h)
+            # 扩边处理
+            _w, _h = x2 - x1, y2 - y1
+            out_px = 0.025
+            _w, _h = int(out_px * _w), int(out_px * _h)
+            n_x1, n_y1, n_x2, n_y2 = x1 - _w, y1 - _h, x2 + _w, y2 + _h
+            if n_x1 < 0:
+                n_x1 = 0
+            if n_y1 < 0:
+                n_y1 = 0
+            if n_x2 > o_w:
+                n_x2 = o_w
+            if n_y2 > o_h:
+                n_y2 = o_h
+            image_deal_info["抠图扩边后位置"] = (n_x1, n_y1, n_x2, n_y2)
+            cut_image = original_pic.im.crop(image_deal_info["抠图扩边后位置"])
+
+            image_deal_info["抠图扩边后图片大小"] = cut_image.size
+            x, y = image_deal_info["抠图扩边后图片大小"]
+
+            # 12000000
+            max_size = settings.MAX_PIXIAN_SIZE
+            if x * y > max_size:
+                r = math.sqrt(max_size) / math.sqrt(x * y)
+                r = r*0.9
+                size = (int(x * r), int(y * r))
+                # print("图片:{} pixian触发二次缩放,原尺寸{}*{},新尺寸:{}".format(self.file_name, x, y, size))
+                self.add_log(text="图片进行压缩,压缩前:{},压缩后:{}".format(image_deal_info["抠图扩边后图片大小"], size))
+                image_deal_info["抠图扩边后PIL对象"] = copy.deepcopy(cut_image)
+                cut_image = cut_image.resize(size=size,resample=1)
+                # print(cut_image.size)
+                # print(image_deal_info["抠图扩边后PIL对象"].size)
+                image_deal_info["二次抠图是否缩放"] = True
+            else:
+                image_deal_info["二次抠图是否缩放"] = False
+        return cut_image, image_deal_info
+
+    def get_image_orientation(self, img):
+        # 获取EXIF数据
+        exif = img._getexif()
+        if exif is not None:
+            # EXIF标签274对应的是Orientation
+            orientation = exif.get(0x0112)
+            if orientation == 2:
+                # 水平翻转
+                img = img.transpose(Image.FLIP_LEFT_RIGHT)
+            elif orientation == 3:
+                # 旋转180度
+                img = img.rotate(180, expand=True)
+            elif orientation == 4:
+                # 垂直翻转
+                img = img.transpose(Image.FLIP_TOP_BOTTOM)
+            elif orientation == 5:
+                # 水平翻转后顺时针旋转90度
+                img = img.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.ROTATE_270)
+            elif orientation == 6:
+                # 顺时针旋转90度
+                img = img.transpose(Image.ROTATE_270)
+            elif orientation == 7:
+                # 水平翻转后逆时针旋转90度
+                img = img.transpose(Image.FLIP_LEFT_RIGHT).transpose(Image.ROTATE_90)
+            elif orientation == 8:
+                # 逆时针旋转90度
+                img = img.transpose(Image.ROTATE_90)
+        else:
+            print("没有EXIF数据或没有方向信息")
+            orientation = 1
+
+        return img

+ 138 - 0
python/service/excel_base_func.py

@@ -0,0 +1,138 @@
+from PySide6.QtWidgets import QApplication
+from PIL import Image
+import settings
+import time
+import os
+
+def get_clip_image(image_path):
+    try:
+        cb = QApplication.clipboard()
+        if cb.mimeData().hasImage():
+            qt_img = cb.image()
+            pil_img = Image.fromqimage(qt_img)  # 转换为PIL图像
+            if pil_img.width > 10:
+                pil_img = pil_img.convert("RGB")
+                pil_img.save(image_path)
+                print("图片剪切板保存成功:{}".format(image_path))
+
+            return True
+        return False
+    except BaseException as e:
+        print(e)
+        settings.logger.info("获取剪切板图片异常:{}".format(e))
+        return False
+
+
+def get_one_cell_shape_image(shape, image_out_path):
+    try:
+        flag = True
+        _address = shape.TopLeftCell.Address
+        print("_address", _address)  # $E$2
+        shape.LockAspectRatio = True
+        old_w = shape.Width
+        shape.Width = 500
+        print("shape.Copy")
+        shape.Copy()
+
+        time.sleep(0.1)
+
+        if not get_clip_image(image_out_path):
+            flag = False
+        shape.Width = old_w
+    except BaseException as e:
+        print("get_one_cell_shape_image", e)
+        return False
+
+    return flag
+
+
+def add_pic_with_wps(sheet, row, column, image_path, pic_w=35):
+    cell = sheet.Cells(row, column)
+    cell_value = cell.Value
+    if cell_value:
+        if "DISPIMG" in cell_value:
+            print("已有图片")
+            return
+    cell.ColumnWidth = 10
+    cell.RowHeight = 42
+    im = Image.open(image_path)
+    w = pic_w
+    h = int((im.height * w) / im.width)
+
+    # pic = sheet.Shapes.AddPicture(FileName=image_path, LinkToFile=False, SaveWithDocument=True,
+    #                               Left=cell.Left + 2, Top=cell.Top + 2, Width=-1, Height=-1)
+    # https://learn.microsoft.com/zh-tw/office/vba/api/excel.shapes.addpicture
+    pic = sheet.Shapes.AddPicture(image_path, 0, 1, cell.Left + 2, cell.Top + 2, w, h)
+    pic.LockAspectRatio = True
+    pic.Placement = 1  # 随单元格大小变化
+    # os.remove(image_path)
+    # r = pic.ShapeRange
+    # r.LockAspectRatio = True
+
+
+def add_pic_with_office(sheet, row, column, image_path, pic_w=35):
+    # self.add_pic_with_wps(sheet, row, column, image_path, pic_w=35)
+    # return
+    cell = sheet.Cells(row, column)
+    cell_value = cell.Value
+    if cell_value:
+        if "DISPIMG" in cell_value:
+            print("已有图片")
+            return
+
+    cell.ColumnWidth = 10
+    cell.RowHeight = 42
+
+    im = Image.open(image_path)
+    w = pic_w
+    h = int((im.height * w) / im.width)
+
+    pic_shape = sheet.Shapes.AddShape(1, cell.Left + 2, cell.Top + 2, w, h)
+    pic_shape.Fill.UserPicture(image_path)
+    pic_shape.Line.Weight = 0
+    pic_shape.Placement = 1  # 随单元格大小变化
+
+
+def get_all_row_sheet_images(sheet):
+    """
+    返回每行中是否存在形状,第二行为index=0
+    """
+    row_address_data_dict = {}
+
+    for i, shape in enumerate(sheet.Shapes):
+        try:
+            _address = shape.TopLeftCell.Address
+            _row = 0
+            if isinstance(_address, str):
+                _row = _address.split("$")[2]
+                _row = int(_row) - 2
+            if _row == -1:
+                break
+            row_address_data_dict[_row] = shape
+        except BaseException as e:
+            print(e)
+            break
+
+    return row_address_data_dict
+
+
+def save_log_text():
+    # 移除30条以外的文件
+    f_list = []
+    for new_file_name in os.listdir(r"log\upload_log"):
+        _file_path = "{}\{}".format(r"log\upload_log", new_file_name)
+        f_list.append({"file_name": new_file_name,
+                       "file_path": _file_path,
+                       "create_time": os.path.getctime(_file_path)
+                       })
+
+    if f_list:
+        f_list.sort(key=lambda x: x["create_time"], reverse=True)
+        while 1:
+            if len(f_list) > 30:
+                del_file_dict = f_list.pop()
+                del_file_path = "{}\{}".format(os.getcwd(), del_file_dict["file_path"])
+                os.remove(del_file_path)
+            else:
+                break
+    pass

+ 446 - 0
python/service/grenerate_main_image_test.py

@@ -0,0 +1,446 @@
+import os
+import copy
+import time
+from image_deal_base_func import *
+from PIL import Image, ImageDraw
+from blend_modes import multiply
+import os
+import settings
+from functools import wraps
+
+
+def time_it(func):
+    @wraps(func)  # 使用wraps来保留原始函数的元数据信息
+    def wrapper(*args, **kwargs):
+        start_time = time.time()  # 记录开始时间
+        result = func(*args, **kwargs)  # 调用原始函数
+        end_time = time.time()  # 记录结束时间
+        print(f"Executing {func.__name__} took {end_time - start_time:.4f} seconds.")  # 打印耗时
+        return result
+
+    return wrapper
+
+
+class GeneratePic(object):
+    def __init__(self, is_test=False):
+        # self.logger = MyLogger()
+        self.is_test = is_test
+        pass
+
+    @time_it
+    def get_mask_and_config(self, im_jpg: Image, im_png: Image):
+        """
+        步骤:
+        1、尺寸进行对应缩小
+        2、查找并设定鞋底阴影蒙版
+        3、自动色阶检查亮度
+        4、输出自动色阶参数、以及放大的尺寸蒙版
+        """
+        # ===================尺寸进行对应缩小(提升处理速度)
+        im_jpg = to_resize(im_jpg, width=800)
+        im_png = to_resize(im_png, width=800)
+        x1, y1, x2, y2 = im_png.getbbox()
+
+        cv2_png = pil_to_cv2(im_png)
+        # =====================设定鞋底阴影图的蒙版
+        # 查找每列的最低非透明点
+        min_y_values = find_lowest_non_transparent_points(cv2_png)
+        # 在鞋底最低处增加一条直线蒙版,蒙版宽度为有效区域大小
+        image_high = im_jpg.height
+        print("图片高度:", image_high)
+        # TODO 待移除
+        settings.app.processEvents()
+        cv2_jpg = pil_to_cv2(im_jpg)
+        # 返回线条图片,以及最低位置
+        print("返回线条图片,以及最低位置")
+        img_with_shifted_line, lowest_y = draw_shifted_line(image=cv2_jpg,
+                                                            min_y_values=min_y_values,
+                                                            shift_amount=15,
+                                                            one_line_pos=(x1, x2),
+                                                            line_color=(0, 0, 0),
+                                                            line_thickness=20,
+                                                            app=settings.app)
+        # TODO 待移除
+        settings.app.processEvents()
+        print("66  制作蒙版")
+        # 制作蒙版
+        mask_line = cv2_to_pil(img_with_shifted_line)
+        mask = mask_line.convert('L')  # 转换为灰度图
+        mask = ImageOps.invert(mask)
+        # 蒙版扩边
+        print("72  蒙版扩边")
+        mask = expand_or_shrink_mask(pil_image=mask, expansion_radius=65, blur_radius=35)
+
+        # mask1 = expand_mask(mask, expansion_radius=30, blur_radius=10)
+        # mask1.save("mask1.png")
+        # mask2 = expand_or_shrink_mask(pil_image=mask, expansion_radius=60, blur_radius=30)
+        # mask2.save("mask2.png")
+        # raise 11
+
+        # TODO 待移除
+        settings.app.processEvents()
+        # ====================生成新的图片
+        print("84  生成新的图片")
+        bg = Image.new(mode="RGBA", size=im_png.size, color=(255, 255, 255, 255))
+        bg.paste(im_png, mask=im_png)
+        bg.paste(im_jpg, mask=mask)  # 粘贴有阴影的地方
+        # TODO 待移除
+        settings.app.processEvents()
+        if self.is_test:
+            _bg = bg.copy()
+            draw = ImageDraw.Draw(_bg)
+            # 定义直线的起点和终点坐标
+            start_point = (0, lowest_y)  # 直线的起始点
+            end_point = (_bg.width, lowest_y)  # 直线的结束点
+            # 定义直线的颜色(R, G, B)
+            line_color = (255, 0, 0)  # 红色
+            # 绘制直线
+            draw.line([start_point, end_point], fill=line_color, width=1)
+            # mask.show()
+            # bg = pil_to_cv2(bg)
+            # cv2.line(bg, (x1, lowest_y + 5), (x2, lowest_y + 5), color=(0, 0, 0),thickness=2)
+            # bg = cv2_to_pil(bg)
+            _r = Image.new(mode="RGBA", size=im_png.size, color=(246, 147, 100, 255))
+            mask_line = mask_line.convert('L')  # 转换为灰度图
+            mask_line = ImageOps.invert(mask_line)
+            _bg.paste(_r, mask=mask)
+            _bg.show()
+
+        # bg.save(r"C:\Users\gymmc\Desktop\data\bg.png")
+        # bg.show()
+        # ==================自动色阶处理======================
+        # 对上述拼接后的图片进行自动色阶处理
+        bg = bg.convert("RGB")
+        _im = cv2.cvtColor(np.asarray(bg), cv2.COLOR_RGB2BGR)
+        # 背景阴影
+        im_shadow = cv2.cvtColor(_im, cv2.COLOR_BGR2GRAY)
+        print("image_high lowest_y", image_high, lowest_y)
+        if lowest_y < 0 or lowest_y >= image_high:
+            lowest_y = image_high - 1
+        print("image_high lowest_y", image_high, lowest_y)
+        rows = [lowest_y]  # 需要检查的像素行
+
+        print("copy.copy(im_shadow)")
+        _im_shadow = copy.copy(im_shadow)
+        Midtones = 0.7
+        Highlight = 235
+        k = 8
+        print("循环识别")
+        while k:
+            # TODO 待移除
+            print("循环识别:{}".format(k))
+            settings.app.processEvents()
+            k -= 1
+            Midtones += 0.1
+            if Midtones > 1:
+                Midtones = 1
+            Highlight -= 3
+            _im_shadow = levels_adjust(img=im_shadow, Shadow=0, Midtones=Midtones, Highlight=Highlight,
+                                       OutShadow=0,
+                                       OutHighlight=255, Dim=3)
+            brightness_list = calculate_average_brightness_opencv(img_gray=_im_shadow, rows_to_check=rows)
+            print(brightness_list)
+
+            if brightness_list[0] >= settings.GRENERATE_MAIN_PIC_BRIGHTNESS:
+                break
+        print("Midtones,Highlight:", Midtones, Highlight)
+
+        im_shadow = cv2_to_pil(_im_shadow)
+
+        # ========================================================
+        # 计算阴影的亮度,用于确保阴影不要太黑
+
+        # 1、图片预处理,只保留阴影
+        only_shadow_img = im_shadow.copy()
+        only_shadow_img.paste(Image.new(mode="RGBA", size=only_shadow_img.size, color=(255, 255, 255, 255)),
+                              mask=im_png)
+        average_brightness = calculated_shadow_brightness(only_shadow_img)
+        print("average_brightness:", average_brightness)
+
+        config = {
+            "Midtones": Midtones,
+            "Highlight": Highlight,
+            "average_brightness": average_brightness,
+        }
+
+        return mask, config
+
+    def get_mask_and_config_beifen(self, im_jpg: Image, im_png: Image, out_image_path=None):
+        """
+        步骤:
+        1、尺寸进行对应缩小
+        2、查找并设定鞋底阴影蒙版
+        3、自动色阶检查亮度
+        4、输出自动色阶参数、以及放大的尺寸蒙版
+        """
+        # ===================尺寸进行对应缩小
+        orign_x, orign_y = im_jpg.size
+
+        im_jpg = to_resize(im_jpg, width=800)
+        im_png = to_resize(im_png, width=800)
+        x1, y1, x2, y2 = im_png.getbbox()
+
+        cv2_png = pil_to_cv2(im_png)
+        # =====================设定鞋底阴影图的蒙版
+        # 查找每列的最低非透明点
+        min_y_values = find_lowest_non_transparent_points(cv2_png)
+        # 在鞋底最低处增加一条直线蒙版,蒙版宽度为有效区域大小
+        cv2_jpg = pil_to_cv2(im_jpg)
+        # 返回线条图片,以及最低位置
+        img_with_shifted_line, lowest_y = draw_shifted_line(image=cv2_jpg,
+                                                            min_y_values=min_y_values,
+                                                            shift_amount=15,
+                                                            one_line_pos=(x1, x2),
+                                                            line_color=(0, 0, 0),
+                                                            line_thickness=20)
+
+        # 制作蒙版
+        mask = cv2_to_pil(img_with_shifted_line)
+        mask = mask.convert('L')  # 转换为灰度图
+        mask = ImageOps.invert(mask)
+        # 蒙版扩边
+        mask = expand_mask(mask, expansion_radius=30, blur_radius=10)
+
+        # ====================生成新的图片
+        bg = Image.new(mode="RGBA", size=im_png.size, color=(255, 255, 255, 255))
+        bg.paste(im_png, mask=im_png)
+        bg.paste(im_jpg, mask=mask)  # 粘贴有阴影的地方
+
+        # bg = pil_to_cv2(bg)
+        # cv2.line(bg, (x1, lowest_y + 5), (x2, lowest_y + 5), color=(0, 0, 0),thickness=2)
+        # bg = cv2_to_pil(bg)
+        # bg.show()
+
+        # bg.save(r"C:\Users\gymmc\Desktop\data\bg.png")
+        # bg.show()
+        # ==================自动色阶处理======================
+        # 对上述拼接后的图片进行自动色阶处理
+        bg = bg.convert("RGB")
+        _im = cv2.cvtColor(np.asarray(bg), cv2.COLOR_RGB2BGR)
+        # 背景阴影
+        im_shadow = cv2.cvtColor(_im, cv2.COLOR_BGR2GRAY)
+
+        rows = [lowest_y]  # 需要检查的像素行
+        _im_shadow = copy.copy(im_shadow)
+        Midtones = 0.62
+        Highlight = 235
+        k = 10
+        while k:
+            k -= 1
+            Midtones += 0.1
+            if Midtones > 1:
+                Midtones = 1
+            Highlight -= 3
+            _im_shadow = levels_adjust(img=im_shadow, Shadow=0, Midtones=Midtones, Highlight=Highlight,
+                                       OutShadow=0,
+                                       OutHighlight=255, Dim=3)
+            brightness_list = calculate_average_brightness_opencv(img_gray=_im_shadow, rows_to_check=rows)
+            print(brightness_list)
+            if brightness_list[0] >= 254:
+                break
+        print("Midtones,Highlight:", Midtones, Highlight)
+        config = (Midtones, Highlight)
+        im_shadow = cv2_to_pil(_im_shadow)
+        im_shadow.paste(im_png, (0, 0), im_png)  # 把原图粘贴回去,避免色差
+        if out_image_path:
+            im_shadow.save(out_image_path)
+
+        return mask, config
+
+    def my_test(self, **kwargs):
+        if "output_queue" in kwargs:
+            output_queue = kwargs["output_queue"]
+        else:
+            output_queue = None
+        time.sleep(3)
+        if output_queue is not None:
+            output_queue.put(True)
+
+    @time_it
+    def run(self, image_path, cut_image_path, out_path, image_deal_mode=0, image_index=99,
+            out_pic_size=1024, is_logo=True, out_process_path_1=None, out_process_path_2=None,
+            resize_mode=None, max_box=None, logo_path="", **kwargs):  # im 为cv对象
+        """
+        image_path:原始图
+        cut_image_path:抠图结果 与原始图尺寸相同
+        out_path:输出主图路径
+        image_deal_mode:图片处理模式,1表示需要镜像处理
+        image_index:图片顺序索引
+        out_pic_size:输出图片宽度大小
+        is_logo=True 是否要添加logo水印
+        out_process_path_1=None, 有阴影的图片,白底非透明
+        out_process_path_2=None, 已抠图的图片
+        resize_mode=0,1,2 主体缩小尺寸
+        """
+        if "output_queue" in kwargs:
+            output_queue = kwargs["output_queue"]
+        else:
+            output_queue = None
+
+        # ==========先进行剪切原图
+        _s = time.time()
+        orign_im = Image.open(image_path)  # 原始图
+        print("242  need_time_1:{}".format(time.time() - _s))
+
+        orign_x, orign_y = orign_im.size
+        cut_image = Image.open(cut_image_path)  # 原始图的已扣图
+        cut_image, new_box = get_mini_crop_img(img=cut_image)
+        im_shadow = orign_im.crop(new_box)  # 切图
+        new_x, new_y = im_shadow.size
+
+        # ================自动色阶处理
+        _s = time.time()
+        shadow_mask, config = self.get_mask_and_config(im_jpg=im_shadow, im_png=cut_image)
+        print("242  need_time_2:{}".format(time.time() - _s))
+
+        shadow_mask = shadow_mask.resize(im_shadow.size)
+
+        # =====抠图,形成新的阴影背景图=====
+        # TODO 待移除
+        settings.app.processEvents()
+
+        _new_im_shadow = Image.new(mode="RGBA", size=im_shadow.size, color=(255, 255, 255, 255))
+        _new_im_shadow.paste(im_shadow, mask=shadow_mask)  # 粘贴有阴影的地方
+        # _new_im_shadow.show()
+        _new_im_shadow = pil_to_cv2(_new_im_shadow)
+        _new_im_shadow = cv2.cvtColor(_new_im_shadow, cv2.COLOR_BGR2GRAY)
+        _new_im_shadow = levels_adjust(img=_new_im_shadow,
+                                       Shadow=0,
+                                       Midtones=config["Midtones"],
+                                       Highlight=config["Highlight"],
+                                       OutShadow=0,
+                                       OutHighlight=255, Dim=3)
+
+        im_shadow = cv2_to_pil(_new_im_shadow)
+
+        # ================处理阴影的亮度==================
+        average_brightness = config["average_brightness"]
+        if config["average_brightness"] < 180:
+            # 调整阴影亮度
+            backdrop_prepped = np.asfarray(Image.new(mode="RGBA", size=im_shadow.size, color=(255, 255, 255, 255)))
+            im_shadow = im_shadow.convert("RGBA")
+            source_prepped = np.asfarray(im_shadow)
+            # im_shadow.show()
+
+            opacity = (average_brightness - 30) / 160
+            opacity = max(0.5, min(opacity, 1))
+
+            print("阴影透明度:{}%".format(int(opacity * 100)))
+            blended_np = multiply(backdrop_prepped, source_prepped, opacity=int(opacity * 100) / 100)
+            im_shadow = Image.fromarray(np.uint8(blended_np)).convert('RGB')
+            # im_shadow.show()
+
+        # 把原图粘贴回去,避免色差
+        im_shadow.paste(cut_image, (0, 0), mask=cut_image)
+        # _new_im_shadow.show()
+
+        # ===========处理其他====================
+
+        # 保存带有阴影的底图,没有logo
+        if out_process_path_1:
+            out_image_1 = im_shadow.copy()
+            if image_deal_mode == 1:
+                out_image_1 = out_image_1.transpose(Image.FLIP_LEFT_RIGHT)
+            out_image_1.save(out_process_path_1)
+
+        # 保存抠图结果,没有底图,没有logo
+        if out_process_path_2:
+            out_image_2 = cut_image.copy()
+            if image_deal_mode == 1:
+                out_image_2 = out_image_2.transpose(Image.FLIP_LEFT_RIGHT)
+            out_image_2.save(out_process_path_2)
+
+        # 不生成主图时直接退出
+        if not out_path:
+            return True
+
+        # im_shadow.show()
+        # =====================主图物体的缩放依据大小
+        if max_box:
+            im_shadow = to_resize(_im=im_shadow, width=max_box[0], high=max_box[1])
+            cut_image = to_resize(_im=cut_image, width=max_box[0], high=max_box[1])
+        else:
+            if resize_mode is None:
+                im_shadow = to_resize(_im=im_shadow, width=1400, high=1400)
+                cut_image = to_resize(_im=cut_image, width=1400, high=1400)
+
+            elif resize_mode == 1:
+                im_shadow = to_resize(_im=im_shadow, width=1400, high=1400)
+                cut_image = to_resize(_im=cut_image, width=1400, high=1400)
+
+            elif resize_mode == 2:
+                # todo 兼容长筒靴等,将图片大小限制在一个指定的box内
+                im_shadow = to_resize(_im=im_shadow, width=650)
+                cut_image = to_resize(_im=cut_image, width=650)
+                # 再次检查需要约束缩小到一定高度,适应长筒靴
+                _im_x, _im_y = cut_image.size
+                if _im_y > 1400:
+                    im_shadow = to_resize(_im=im_shadow, high=1400)
+                    cut_image = to_resize(_im=cut_image, high=1400)
+
+                # if im_shadow.height <= im_shadow.width * 1.2:
+                #     im_shadow = to_resize(_im=im_shadow, width=650)
+                #     cut_image = to_resize(_im=cut_image, width=650)
+                # else:
+                #     im_shadow = to_resize(_im=im_shadow, high=1400)
+                #     cut_image = to_resize(_im=cut_image, high=1400)
+
+        if image_deal_mode == 1:
+            # 翻转
+            im_shadow = im_shadow.transpose(Image.FLIP_LEFT_RIGHT)
+            cut_image = cut_image.transpose(Image.FLIP_LEFT_RIGHT)
+
+        # 创建底层背景
+        image_bg = Image.new("RGB", (1600, 1600), (255, 255, 255))
+
+        image_bg_x, image_bg_y = image_bg.size
+        image_x, image_y = im_shadow.size
+
+        _x = int((image_bg_x - image_x) / 2)
+        _y = int((image_bg_y - image_y) / 2)
+
+        image_bg.paste(im_shadow, (_x, _y))
+        image_bg.paste(cut_image, (_x, _y), cut_image)  # 再叠加原图避免色差
+
+        if "小苏" in settings.Company:
+            # 所有主图加logo
+            is_logo = True
+
+        if is_logo:
+            # logo_path = ""
+            # if settings.PROJECT == "红蜻蜓":
+            #     logo_path = r"resources\LOGO\HQT\logo.png"
+            # elif settings.PROJECT == "惠利玛":
+            #     if "小苏" in settings.Company:
+            #         logo_path = r"resources\LOGO\xiaosushuoxie\logo.png"
+            #     elif "惠利玛" in settings.Company:
+            #         logo_path = r"resources\LOGO\HLM\logo.png"
+            #     else:
+            #         pass
+            if not logo_path:
+                logo_im = Image.new("RGBA", (1600, 1600), (0, 0, 0, 0))
+            else:
+                if os.path.exists(logo_path):
+                    logo_im = Image.open(logo_path)
+                else:
+                    logo_im = Image.new("RGBA", (1600, 1600), (0, 0, 0, 0))
+
+            image_bg.paste(logo_im, (0, 0), logo_im)
+
+        # image_bg = image_bg.resize((out_pic_size, out_pic_size), Image.BICUBIC)
+        if settings.OUT_PIC_FACTOR > 1.0:
+            print("图片锐化处理")
+            image_bg = sharpen_image(image_bg, factor=settings.OUT_PIC_FACTOR)
+
+        if out_pic_size < 1600:
+            image_bg = image_bg.resize((out_pic_size, out_pic_size), resample=settings.RESIZE_IMAGE_MODE)
+
+        if settings.OUT_PIC_MODE == ".jpg":
+            image_bg.save(out_path, quality=100, dpi=(300, 300), format="JPEG")
+        else:
+            # quality=quality
+            image_bg.save(out_path, quality=100)
+
+        if output_queue is not None:
+            output_queue.put(True)
+        return True

+ 46 - 0
python/service/handle_detail.py

@@ -0,0 +1,46 @@
+from functools import partial
+import os
+from upload_pic import UploadPic  # 假设 UploadPic 类在 upload_pic 模块中
+from run_main import RunMain
+
+class HandleDetail():
+
+    def __int__(self):
+        self.run_main = RunMain(windows=self)
+
+    def deal_run_end_sign(self, config_data: dict):
+        if config_data["sign_text"] == "开始抠图":
+            # 先做整体校验
+            self.run_main.check_for_cutout_image_first_call_back(return_data=self.run_main.check_before_cutout,
+                                                                 config_data=config_data)
+
+        print(config_data)
+        if config_data["sign_text"] == "已结束抠图处理":
+            if config_data["detail_is_enable"]:
+                # 先做整体校验
+                # temp_name=self.last_temp, temp_name_list=self.temp_list
+                func = partial(self.run_main.check_before_detail, config_data=config_data)
+                self.do_thread_run(func=func,
+                                   call_back=self.run_main.check_for_detail_first_call_back,
+                                   time_out=30,
+                                   is_show_mask=False)
+            else:
+                self.set_state(state_value=2)
+
+        if config_data["sign_text"] == "已结束详情处理":
+            if config_data["upload_is_enable"]:
+                to_deal_dir = "{}/软件-详情图生成".format(config_data["image_dir"])
+                print("to_deal_dir", to_deal_dir)
+                if os.path.exists(to_deal_dir):
+                    self.upload_pic = UploadPic(windows=self, to_deal_dir=to_deal_dir, config_data=config_data)
+                    self.upload_pic.run_end_sign.connect(self.deal_run_end_sign)
+                    self.upload_pic.show_progress_detail_sign.connect(self.show_progress_detail)
+                    self.upload_pic.run()
+                    # threading.Thread(target=self.upload_pic.run_by_thread, args=()).start()
+                else:
+                    self.set_state(state_value=2)
+            else:
+                self.set_state(state_value=2)
+
+        if config_data["sign_text"] == "结束":
+            self.set_state(state_value=2)

+ 264 - 0
python/service/image_deal_base_func.py

@@ -0,0 +1,264 @@
+import cv2
+import numpy as np
+from PIL import Image, ImageEnhance, ImageFilter, ImageOps
+import settings
+
+# 锐化图片
+def sharpen_image(img, factor=1.0):
+    # 创建一个ImageEnhance对象
+    enhancer = ImageEnhance.Sharpness(img)
+    # 应用增强,值为0.0给出模糊图像,1.0给出原始图像,大于1.0给出锐化效果
+    # 调整这个值来增加或减少锐化的程度
+    sharp_img = enhancer.enhance(factor)
+    return sharp_img
+
+
+def to_resize(_im, width=None, high=None) -> Image:
+    _im_x, _im_y = _im.size
+    if width and high:
+        if _im_x >= _im_y:
+            high = None
+        else:
+            width = None
+    if width:
+        re_x = int(width)
+        re_y = int(_im_y * re_x / _im_x)
+    else:
+        re_y = int(high)
+        re_x = int(_im_x * re_y / _im_y)
+    _im = _im.resize((re_x, re_y),resample=settings.RESIZE_IMAGE_MODE)
+    return _im
+
+
+def pil_to_cv2(pil_image):
+    # 将 PIL 图像转换为 RGB 或 RGBA 格式
+    if pil_image.mode != 'RGBA':
+        pil_image = pil_image.convert('RGBA')
+    # 将 PIL 图像转换为 numpy 数组
+    cv2_image = np.array(pil_image)
+    # 由于 PIL 的颜色顺序是 RGB,而 OpenCV 的颜色顺序是 BGR,因此需要交换颜色通道
+    cv2_image = cv2.cvtColor(cv2_image, cv2.COLOR_RGBA2BGRA)
+    return cv2_image
+
+
+def cv2_to_pil(cv_img):
+    return Image.fromarray(cv2.cvtColor(cv_img, cv2.COLOR_BGR2RGB))
+
+
+def get_mini_crop_img(img):
+    old_x, old_y = img.size
+    x1, y1, x2, y2 = img.getbbox()
+
+    goods_w, goods_h = x2 - x1, y2 - y1
+    _w, _h = int(goods_w / 10), int(goods_h / 10)  # 上下左右扩展位置
+    new_x1, new_y1, new_x2, new_y2 = x1 - _w, y1 - _h, x2 + _w, y2 + _h  # 防止超限
+    new_x1 = 0 if new_x1 < 0 else new_x1
+    new_y1 = 0 if new_y1 < 0 else new_y1
+    new_x2 = old_x if new_x2 > old_x else new_x2
+    new_y2 = old_y if new_y2 > old_y else new_y2
+    img = img.crop((new_x1, new_y1, new_x2, new_y2))  # 切图
+    box = (new_x1, new_y1, new_x2, new_y2)
+    return img, box
+
+def expand_or_shrink_mask(pil_image, expansion_radius=5, iterations=1, blur_radius=0):
+    """
+    对输入的PIL黑白图像(掩膜)进行膨胀或腐蚀操作,以扩大或缩小前景区域。
+
+    :param pil_image: 输入的PIL黑白图像对象
+    :param expansion_radius: 结构元素大小,默认是一个3x3的小正方形;负值表示收缩
+    :param iterations: 操作迭代次数,默认为1次
+    :param blur_radius: 高斯模糊的半径,默认不应用模糊
+    :return: 修改后的PIL黑白图像对象
+    """
+
+    # 将PIL图像转换为numpy数组,并确保其为8位无符号整数类型
+    img_np = np.array(pil_image).astype(np.uint8)
+
+    # 如果不是二值图像,则应用阈值处理
+    if len(np.unique(img_np)) > 2:  # 检查是否为二值图像
+        _, img_np = cv2.threshold(img_np, 127, 255, cv2.THRESH_BINARY)
+
+    # 定义结构元素(例如正方形)
+    abs_expansion_radius = abs(expansion_radius)
+    kernel = np.ones((abs_expansion_radius, abs_expansion_radius), np.uint8)
+
+    # 根据expansion_radius的符号选择膨胀或腐蚀操作
+    if expansion_radius >= 0:
+        modified_img_np = cv2.dilate(img_np, kernel, iterations=iterations)
+    else:
+        modified_img_np = cv2.erode(img_np, kernel, iterations=iterations)
+
+    # 如果提供了blur_radius,则应用高斯模糊
+    if blur_radius > 0:
+        modified_img_np = cv2.GaussianBlur(modified_img_np, (blur_radius * 2 + 1, blur_radius * 2 + 1), 0)
+
+    # 将numpy数组转换回PIL图像
+    modified_pil_image = Image.fromarray(modified_img_np)
+
+    return modified_pil_image
+
+def expand_mask(mask, expansion_radius=5, blur_radius=0):
+    # 对蒙版进行膨胀处理
+    mask = mask.filter(ImageFilter.MaxFilter(expansion_radius * 2 + 1))
+    # 应用高斯模糊滤镜
+    if blur_radius > 0:
+        mask = mask.filter(ImageFilter.GaussianBlur(blur_radius))
+    return mask
+
+
+def find_lowest_non_transparent_points(cv2_png):
+    # cv2_png 为cv2格式的带有alpha通道的图片
+
+    alpha_channel = cv2_png[:, :, 3]
+    """使用Numpy快速查找每列的最低非透明点"""
+    h, w = alpha_channel.shape
+    # 创建一个掩码,其中非透明像素为True
+    mask = alpha_channel > 0
+    # 使用np.argmax找到每列的第一个非透明像素的位置
+    # 因为是从底部向上找,所以需要先翻转图像
+    flipped_mask = np.flip(mask, axis=0)
+    min_y_values = h - np.argmax(flipped_mask, axis=0) - 1
+    # 将全透明列的值设置为-1
+    min_y_values[~mask.any(axis=0)] = -1
+    return min_y_values
+
+
+def draw_shifted_line(image, min_y_values, shift_amount=15,
+                      one_line_pos=(0, 100),
+                      line_color=(0, 0, 0),
+                      line_thickness=20,
+                      app=None):
+    """
+    image:jpg cv2格式的原始图
+    min_y_values 透明图中,不透明区域的最低那条线
+    shift_amount:向下偏移值
+    line_color:线颜色
+    line_thickness:线宽
+    """
+    # 将最低Y值向下迁移20个像素,但确保不超过图片的高度
+    # 创建空白图片
+    if app:
+        # TODO 待移除
+        app.processEvents()
+    image = np.ones((image.shape[0], image.shape[1], 3), dtype=np.uint8) * 255
+
+    # 对线条取转成图片
+    shifted_min_y_values = np.clip(min_y_values + shift_amount, 0, image.shape[0] - 1)
+    if app:
+        # TODO 待移除
+        app.processEvents()
+    # 使用Numpy索引批量绘制直线
+    min_y_threshold = 50  # Y轴像素小于50的不处理
+    valid_x = (shifted_min_y_values >= min_y_threshold) & (shifted_min_y_values != -1)
+    if app:
+        # TODO 待移除
+        app.processEvents()
+    # 对曲线取平均值
+    # # 对曲线取平均值
+    # min_y = np.max(min_y_values)
+    # min_y_values_2 = min_y_values + min_y
+    # min_y_values_2 = min_y_values_2 / 2
+    # min_y_values_2 = min_y_values_2.astype(int)
+    # shifted_min_y_values = np.clip(min_y_values_2 + shift_amount, 0, image.shape[0] - 1)
+
+    x_coords = np.arange(image.shape[1])[valid_x]
+    y_start = shifted_min_y_values[valid_x]
+    y_end = y_start + line_thickness
+
+    # 使用Numpy广播机制创建线条区域的索引
+    for x, start, end in zip(x_coords, y_start, y_end):
+        image[start:end, x, :3] = line_color  # 只修改RGB通道
+    if app:
+        # TODO 待移除
+        app.processEvents()
+    # 计算整个图像的最低非透明点
+    lowest_y = np.max(min_y_values[min_y_values != -1]) if np.any(min_y_values != -1) else -1
+    # 绘制原最低非透明点处的线
+    cv2.line(image, (one_line_pos[0], lowest_y + 5), (one_line_pos[1], lowest_y + 5), line_color,
+             thickness=line_thickness)
+    if app:
+        # TODO 待移除
+        app.processEvents()
+
+    _y = lowest_y + 18
+    if _y > image.shape[0]:  # 超过图片尺寸
+        _y = image.shape[0] - 5
+    return image, _y
+
+
+def clean_colors(img):
+    # 转成灰度图
+    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
+    return img
+
+
+def calculated_shadow_brightness(img: Image):
+    # 打开图片并转换为灰度模式
+    image = img.convert('L')
+    # 将图片数据转为numpy数组
+    image_data = np.array(image)
+    # 创建布尔掩码以识别非白色区域
+    non_white_mask = image_data < 252
+
+    # 使用掩码提取非白色像素的亮度值
+    non_white_values = image_data[non_white_mask]
+
+    # print(len(non_white_values),len(image_data))
+    # 如果存在非白色像素,则计算平均亮度;否则返回0
+    if len(non_white_values) > 0:
+        average_brightness = np.mean(non_white_values)
+    else:
+        average_brightness = 0  # 没有非白色像素时的情况
+
+    return average_brightness
+
+
+def levels_adjust(img, Shadow, Midtones, Highlight, OutShadow, OutHighlight, Dim):
+    # 色阶处理
+    # img 为cv2格式
+
+    # dim = 3的时候调节RGB三个分量, 0调节B,1调节G,2调节R
+    if Dim == 3:
+        mask_shadow = img < Shadow
+        img[mask_shadow] = Shadow
+        mask_Highlight = img > Highlight
+        img[mask_Highlight] = Highlight
+    else:
+        mask_shadow = img[..., Dim] < Shadow
+        img[mask_shadow] = Shadow
+        mask_Highlight = img[..., Dim] > Highlight
+        img[mask_Highlight] = Highlight
+
+    if Dim == 3:
+        Diff = Highlight - Shadow
+        rgbDiff = img - Shadow
+        clRgb = np.power(rgbDiff / Diff, 1 / Midtones)
+        outClRgb = clRgb * (OutHighlight - OutShadow) / 255 + OutShadow
+        data = np.array(outClRgb * 255, dtype='uint8')
+        img = data
+    else:
+        Diff = Highlight - Shadow
+        rgbDiff = img[..., Dim] - Shadow
+        clRgb = np.power(rgbDiff / Diff, 1 / Midtones)
+        outClRgb = clRgb * (OutHighlight - OutShadow) / 255 + OutShadow
+        data = np.array(outClRgb * 255, dtype='uint8')
+        img[..., Dim] = data
+    return img
+
+
+def calculate_average_brightness_opencv(img_gray, rows_to_check):
+    # 二值化的图片 CV对象
+    # 计算图片亮度
+    height, width = img_gray.shape
+
+    brightness_list = []
+    for row in rows_to_check:
+        if 0 <= row < height:
+            # 直接计算该行的平均亮度
+            row_data = img_gray[row, :]
+            average_brightness = np.mean(row_data)
+            brightness_list.append(average_brightness)
+        else:
+            print(f"警告:行号{row}超出图片范围,已跳过。")
+
+    return brightness_list

+ 639 - 0
python/service/image_pic_deal.py

@@ -0,0 +1,639 @@
+"""
+"""
+
+import cv2
+import numpy as np
+from PIL import Image, ImageDraw, ImageFont
+from module.view_control.auto_deal_pics.data import DataModeAutoDealPics
+
+
+class OnePicDeal(object):
+    def __init__(self):
+        # 数据模型
+        self.data_mode_auto_deal_pics = DataModeAutoDealPics()
+
+    def check_shoe_is_right(self, im=None, image_path=None):
+        # 先进行API识别左右脚
+        if im is None:
+            im = Image.open(image_path)
+        try:
+            r_data = self.data_mode_auto_deal_pics.check_is_right_foot_by_api(image=im)
+        except BaseException as e:
+            r_data = None
+            print("20", e)
+
+        flag = self.check_shoe_is_right_by_pixel(im=im)
+        if r_data:
+            if "拖鞋" in r_data:
+                flag = flag is not True
+        if flag:
+            print("自动识别----->这是左脚")
+        else:
+            print("自动识别----->这是右脚")
+        return flag
+
+    def check_shoe_is_right_by_pixel(self, im=None, image_path=None):
+        if im is None:
+            im = Image.open(image_path)
+        # 注意,只支持透明图
+        # 打开图像文件
+        im = im.crop(im.getbbox())
+        # image.show()
+        # 获取图像第一行的像素数据
+        pixel_data = im.load()
+        pix_list = []
+        h = int(im.height / 20)
+        for i in range(im.width):
+            _r, _g, _b, _a = pixel_data[i, h]
+            if _a > 10:
+                pix_list.append(i)
+
+        left_f_num = 0
+        middle_w = int(im.width / 2)
+        for i in pix_list:
+            if i < middle_w:
+                left_f_num += 1
+            else:
+                left_f_num -= 1
+        if left_f_num > 0:
+            return True
+        else:
+            return False
+
+    def how_to_use(self):
+        # 单图缩放处理
+        a = {"command": "resize",
+             "plugins_mode": "relative",  # pixel 相对(宽度、高度、其他参考图),或绝对像素
+             "base_im": {"im": "im"},  # 参考基于其他图 PIL格式
+             "base": "width",  # base:pixel,width,height,by_im 基于长边、基于短边  (基于短边时,则缩放到指定尺寸)by_im确保能塞进参考图内
+             "value": 649,  # 固定值,如果为百分比,则为0
+             "percentage": 0, }  # 百分比
+
+        # 单图圆角处理
+        a = {"command": "radius",  # radius
+             "plugins_mode": "relative",  # pixel 相对(短边),或绝对像素
+             "circular_pos": (0, 1, 0, 1),  # 从左上角顺时针,记录圆角数量
+             "value": 649,  # 固定值,如果为百分比,则为0
+             "percentage": 0, }  # 百分比
+
+        # 单图处理成圆形
+        a = {"command": "circular",  # circular
+             }
+
+        # 单图旋转处理
+        a = {"command": "rotate",
+             "plugins_mode": "",
+             "value": 649,  # 固定值 顺时针
+             }
+
+        # 图片粘贴处理
+        a = {
+            "command": "paste_img",
+            "img": {"im": "im"},
+            "pos": {"plugins_mode": "relative",  # pixel
+                    "base": "center",  # nw,nc,ne,ec ... 各个方向参考点
+                    "value": (100, 100),
+                    "percentage": (0.5, 0.5),
+                    },
+            "margins": (0, 0, 0, 0),  # 上下左右边距
+        }
+
+        # 图片剪裁处理
+        a = {
+            "command": "crop_img",
+            "img": {"im": "im"},
+            "pos": {"plugins_mode": "relative",  # pixel
+                    "base": "center",  # nw,nc,ne,ec ... 各个方向参考点
+                    "value": (100, 100, 10, 10),
+                    },
+            "color_fill": (255, 255, 255)
+        }
+
+        # 图片添加文字
+        a = {
+            "command": "add_text",
+            "pos": {"plugins_mode": "relative",  # pixel
+                    "base": "center",  # nw,nc,ne,ec ... 各个方向参考点
+                    "value": (100, 100),
+                    "percentage": 0, },
+            "font": "",
+            "text": "",
+            "anchor": "",  # mm 为居中 ma 为左右居中,上面居顶
+            "align": "对齐方式",
+            "direction": "文本的方向",
+            "max_len_one_line": "单行长度",
+            "spacing": 10,
+            "fill": "文字颜色",
+        }
+
+    def add_text(self, img: Image, command):
+        draw_1 = ImageDraw.Draw(img)
+        # 定义字体,你需要有一个.ttf字体文件
+        font = command["font"]
+        text = command["text"]
+        spacing = 4 if not command["spacing"] else command["spacing"]
+        fill = command["fill"]
+        anchor = None if not command["anchor"] else command["anchor"]
+        align = "left" if not command["align"] else command["align"]  # left, center 或 right
+        _, _, text_width, text_height = draw_1.textbbox((0, 0), text, font=font)
+
+        xy = (0, 0)
+        if command["pos"]["plugins_mode"] == "pixel":
+            value = command["pos"]["value"]
+            xy = value
+
+        draw_1.multiline_text(xy, text,
+                              fill=fill,
+                              font=font,
+                              anchor=anchor,
+                              spacing=spacing,
+                              align=align,
+                              direction=None,
+                              features=None,
+                              language=None,
+                              stroke_width=0,
+                              stroke_fill=None,
+                              embedded_color=False)
+        return img
+
+    def resize(self, img: Image, command):
+        if command["plugins_mode"] == "pixel":
+            if command["base"] == "width":
+                img = self.to_resize(img, width=command["value"])
+            if command["base"] == "high":
+                img = self.to_resize(img, height=command["value"])
+        # 相对值
+        if command["plugins_mode"] == "relative":
+            base_im = command["base_im"]["im"]
+            if command["base"] == "width":
+                img = self.to_resize(img, width=img.width * command["percentage"] if not base_im else int(
+                    base_im.width * command["percentage"]))
+            if command["base"] == "height":
+                img = self.to_resize(img, width=img.height * command["percentage"] if not base_im else int(
+                    base_im.height * command["percentage"]))
+            # by_im确保能塞进参考图内
+            if command["base"] == "by_im":
+                percentage = 1 if not command["percentage"] else command["percentage"]
+                box_width, box_height = int(base_im.width * percentage), int(base_im.height * percentage)
+                width, height = img.width, img.height
+                if box_width / box_height < width / height:
+                    scale = box_width / width
+                else:
+                    scale = box_height / height
+                img = img.resize((int(width * scale), int(height * scale)))
+        # img.show()
+        return img
+
+    def paste_img(self, img: Image, command):
+        # 粘贴绝对像素
+        base = "nw" if not command["pos"]["base"] else command["pos"]["base"]
+        value = command["pos"]["value"]
+        percentage = (0, 0) if not command["pos"]["percentage"] else command["pos"]["percentage"]
+
+        if command["margins"]:
+            top, down, left, right = command["margins"]
+        else:
+            top, down, left, right = 0, 0, 0, 0
+
+        if percentage != (0, 0):  # percentage 不按占比模式
+            if base in ("nw", "wn", "wc", "cw", "nc", "cn", "center"):
+                value = (int(img.width), int(img.height))
+            if base in ("sw", "ws", "sc", "cs", "center"):
+                value = (int(img.width), -1 * int(img.height))
+            if base in ("ec", "ce"):
+                value = (int(img.width), int(img.height))
+
+        img_1 = command["img"]["im"]
+        if command["pos"]["plugins_mode"] == "pixel":
+            if base == "ec" or "ce":
+                p_x = int(img.width - img_1.width) + value[0]
+                p_y = value[1]
+            if base == "nw" or "wn":
+                deviation_x, deviation_y = 0, 0
+                p_x, p_y = value
+
+            if base == "cs" or "sc":
+                p_x, p_y = value
+            if base == "center":
+                deviation_x, deviation_y = int((img.width - img_1.width) / 2), int((img.height - img_1.height) / 2)
+                p_x = deviation_x + value[0] + left
+                p_y = deviation_y + value[1] + top
+            if base == "sw" or base == "ws":
+                # deviation_x, deviation_y = 0, int((img.height - img_1.height))
+                p_x = value[0] + left
+                p_y = img.height - (img_1.height + value[1] + down)
+            if base == "wc" or base == "cw":
+                p_x = value[0] + left
+                p_y = int((img.height - img_1.height) / 2) + value[1] + top
+
+            try:
+                img.paste(img_1, (p_x, p_y), img_1)
+            except:
+                img.paste(img_1, (p_x, p_y), img_1.convert("RGBA"))
+
+        return img
+
+    def crop_img(self, img: Image, command):
+        base = "nw" if not command["pos"]["base"] else command["pos"]["base"]
+        # print(base)
+        value = command["pos"]["value"]
+        percentage = command["pos"]["percentage"]
+
+        if command["margins"]:
+            top, down, left, right = command["margins"]
+        else:
+            top, down, left, right = 0, 0, 0, 0
+
+        out_img_size = (value[2], value[3])
+        # 默认填充色
+        color_fill = command["color_fill"]
+        if not color_fill:
+            color_fill = (0, 0, 0)
+
+        if command["pos"]["plugins_mode"] == "pixel":
+            if base == "nw" or "wn":
+                box = value
+            if base == "sw" or base == "ws":
+                # deviation_x, deviation_y = 0, int((img.height - img_1.height))
+                box = (value[0], img.height - (value[1] + value[3]), value[2], value[3])
+                print(box)
+
+            if base == "se" or base == "es":
+                box = (img.width - (value[0] + value[2]), img.height - (value[1] + value[3]), value[2], value[3])
+                print(box)
+            box = [box[0], box[1], box[0] + box[2], box[1] + box[3]]
+
+        print("box", box)
+        print("img.width", img.width)
+        print("img.height", img.height)
+
+        out_img = img.crop(box=box)
+        print(out_img.size)
+        # out_img.show()
+        if box[0] < 0:
+            out_img.paste(Image.new("RGB", (-1 * box[0], out_img.height), color_fill), (0, 0))
+        # print(img.width, box)
+
+        if box[2] > img.width:
+            # print(box[2] - img.width, img.height)
+            i = Image.new("RGB", (box[2] - img.width, out_img.height), color_fill)
+            out_img.paste(i, (img.width - box[0], 0))
+        if box[1] < 0:
+            out_img.paste(Image.new("RGB", (img.width, -1 * box[1]), color_fill), (0, 0))
+
+        if box[3] > img.height:
+            out_img.paste(Image.new("RGB", (out_img.width, box[3] - img.height), color_fill),
+                          (0, img.height - box[1]))
+
+        # bg_img = Image.new("RGB", out_img_size, color_fill)
+        # if box[0] < 0:
+        #     x = -1 * box[0]
+        # elif box[2] > img.width:
+        #     x = img.width - box[2]
+        # else:
+        #     x = 0
+        #
+        # if box[1] < 0:
+        #     y = -1 * box[1]
+        # elif box[2] > img.height:
+        #     y = img.height - box[1]
+        # else:
+        #     y = 0
+        # bg_img.paste(img, box=(x, y), )
+
+        # print(box)
+        # img = img.crop(box=box)
+        # out_img.show()
+        return out_img
+
+    def radius(self, img: Image, command):
+        # 单图圆角处理
+
+        radii = command["value"]
+        if radii > img.width / 2:
+            radii = int(img.width / 2)
+        if radii > img.height / 2:
+            radii = int(img.height / 2)
+
+        # 画圆(用于分离4个角)
+        circle = Image.new('L', (radii * 2, radii * 2), 0)  # 创建一个黑色背景的画布
+        draw = ImageDraw.Draw(circle)
+        draw.ellipse((0, 0, radii * 2, radii * 2), fill=255)  # 画白色圆形
+
+        # 原图
+        img = img.convert("RGBA")
+        w, h = img.size
+
+        # 画4个角(将整圆分离为4个部分)
+        alpha = Image.new('L', img.size, 255)
+        _pos = command["circular_pos"]
+        if not _pos:
+            _pos = (1, 1, 1, 1)
+        for index, i in enumerate(_pos):
+            if index == 0 and i == 1:
+                alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0))  # 左上角
+            if index == 1 and i == 1:
+                alpha.paste(circle.crop((radii, 0, radii * 2, radii)), (w - radii, 0))  # 右上角
+            if index == 2 and i == 1:
+                alpha.paste(circle.crop((radii, radii, radii * 2, radii * 2)), (w - radii, h - radii))  # 右下角
+            if index == 3 and i == 1:
+                alpha.paste(circle.crop((0, radii, radii, radii * 2)), (0, h - radii))  # 左下角
+        # alpha.show()
+        img.putalpha(alpha)  # 白色区域透明可见,黑色区域不可见
+        return img
+
+    def to_resize(self, _im, width=None, height=None):
+        _im_x, _im_y = _im.size
+        if width and height:
+            if _im_x >= _im_y:
+                height = None
+            else:
+                width = None
+        if width:
+            re_x = int(width)
+            re_y = int(_im_y * re_x / _im_x)
+        else:
+            re_y = int(height)
+            re_x = int(_im_x * re_y / _im_y)
+        _im = _im.resize((re_x, re_y))
+        return _im
+
+    def add_pic(self, detailed_images):
+        if not detailed_images:
+            return
+        page_len = 0
+        for index, im in enumerate(detailed_images):
+            page_len += im.height
+        bg_im = Image.new("RGB", (im.width, page_len), (255, 255, 255))
+
+        n = 0
+        for index, im in enumerate(detailed_images):
+            bg_im.paste(im, (0, n))
+            n += im.height
+        # bg_im.show()
+        return bg_im
+
+    def get_goods_pos(self, im, cut_image):
+        # 保留多余内容
+        old_x, old_y = im.size
+        x1, y1, x2, y2 = cut_image.getbbox()
+        goods_w, goods_h = x2 - x1, y2 - y1
+        _w, _h = int(goods_w / 10), int(goods_h / 10)  # 上下左右扩展位置
+        new_x1, new_y1, new_x2, new_y2 = x1 - _w, y1 - _h, x2 + _w, y2 + _h  # 防止超限
+        new_x1 = 0 if new_x1 < 0 else new_x1
+        new_y1 = 0 if new_y1 < 0 else new_y1
+        new_x2 = old_x if new_x2 > old_x else new_x2
+        new_y2 = old_y if new_y2 > old_y else new_y2
+        # 剪切掉多余的内容,保留阴影
+        im = im.crop((new_x1, new_y1, new_x2, new_y2))  # 切图
+        return im
+
+    def deal_one_pic(self, data=None, is_show=False):
+        """
+        通用图片处理器
+        1、图片位置处理
+        输出拼接后的图片,以及拼接后商品所属位置
+        """
+        for command in data:
+            if command["command"] == "paste_img":
+                img1 = command["img1"]["im"]
+                img2 = command["img2"]["im"]
+                if "resize" in command["img2"]:
+                    if "width" in command["img2"]["resize"]:
+                        img2 = self.to_resize(img2, width=command["img2"]["resize"]["width"])
+                        if is_show:
+                            img2.show()
+                img1.paste(img2, command["img2"]["pos"])
+            if command["command"] == "image_crop":
+                img1 = img1.crop(command["image_crop"])
+        img = img1
+        return img
+
+    def deal_one_pic_2(self, orign_im, data=None):
+        """
+        通用图片处理器
+        1、基于某原始图,进行加工,包括粘贴、单图处理等逻辑
+        """
+        # 单图缩放处理
+        a = {"command": "resize",
+             "plugins_mode": "relative",  # pixel 相对(宽度、高度、其他参考图),或绝对像素
+             "base_im": "im",  # 参考基于其他图 PIL格式
+             "base": "width",  # base:pixel,width,height,by_long_side,by_short_side基于长边、基于短边  (基于短边时,则缩放到指定尺寸)
+             "value": 649,  # 固定值,如果为百分比,则为0
+             "percentage": 0, }  # 百分比
+
+        # 单图圆角处理
+        a = {"command": "radius",  # radius
+             "plugins_mode": "relative",  # pixel 相对(短边),或绝对像素
+             "circular_pos": (0, 1, 0, 1),  # 从左上角顺时针,记录圆角数量
+             "value": 649,  # 固定值,如果为百分比,则为0
+             "percentage": 0, }  # 百分比
+
+        # 单图处理成圆形
+        a = {"command": "circular",  # circular
+             }
+
+        # 单图旋转处理
+        a = {"command": "rotate",
+             "plugins_mode": "",
+             "value": 649,  # 固定值 顺时针
+             }
+
+        # 图片粘贴处理
+        a = {
+            "command": "paste_img",
+            "img": {"im": "im"},
+            "pos": {"plugins_mode": "relative",  # pixel
+                    "base": "center",  # nw,nc,ne,ec ... 各个方向参考点
+                    "value": (100, 100),
+                    "percentage": 0, },
+            "margins": (0, 0, 0, 0),  # 上下左右边距
+        }
+
+        # 图片剪裁处理
+        a = {
+            "command": "crop_img",
+            "img": {"im": "im"},
+            "plugins_mode": "relative",  # pixel
+            "base": "center",  # nw,nc,ne,ec ... 各个方向参考点
+            "box": (0, 0, 0, 0),
+        }
+
+        # 图片添加文字
+        a = {
+            "command": "add_text",
+            "pos": {"plugins_mode": "relative",  # pixel
+                    "base": "center",  # nw,nc,ne,ec ... 各个方向参考点
+                    "value": (100, 100),
+                    "percentage": 0, },
+            "font": "",
+            "text": "",
+            "align": "对齐方式",
+            "direction": "文本的方向",
+            "fill": "文字颜色",
+        }
+
+        # 图片渐变处理
+        # https://blog.csdn.net/skying159/article/details/119532479
+
+        def get_value(data_dict, key):
+            return 1
+
+        r_dict = {"nw": 0,
+                  }
+
+        _r = 0
+        for command in data:
+            if command["command"] == "paste_img":
+                img1 = command["img1"]["im"]
+                img2 = command["img2"]["im"]
+                if "resize" in command["img2"]:
+                    if "width" in command["img2"]["resize"]:
+                        img2 = self.to_resize(img2, width=command["img2"]["resize"]["width"])
+                if "pos" in command["img2"]:
+                    base = command["img2"]["base"]
+                    value = command["img2"]["value"]
+                    if command["img2"]["plugins_mode"] == "relative":
+                        # 相对位置处理,居中比较特殊
+                        # 其他的先进行旋转与镜像,处理后,进行反向操作
+                        x, y = 0, 0
+                        if base == "center":
+                            x, y = int((img1.width - img2.width) / 2), int((img1.height - img2.height) / 2)
+
+                        if base == "nw" or base == "nw":
+                            pass
+
+                        w, h = img1.width * value[0], img1.height * value[0]
+
+                img1.paste(img2, command["img2"]["pos"])
+                if _r != 0:
+                    img1 = img1.rotate(_r * -1)
+                continue
+            if command["command"] == "image_crop":
+                img1 = img1.crop(command["image_crop"])
+                continue
+        img = img1
+        return img
+
+    def get_overlay_pic(self, pil_img_1, pil_img_2, color):
+        im_w, im_h = pil_img_1.size
+        cv_im = cv2.cvtColor(np.asarray(pil_img_1), cv2.COLOR_RGB2BGR)
+
+        # 创建一张纯色底图
+        image_white = Image.new("RGB", (im_w, im_h), color)
+
+        cv_image_white = cv2.cvtColor(np.asarray(image_white), cv2.COLOR_RGB2BGR)
+        new_im = self.to_color_2(cv_image_white, cv_im)
+        # new_im = cv2.addWeighted(new_im, 0.7, cv_im_2, 0.3, 0)
+        # new_im = cv2.add(new_im, cv_im_2)
+
+        new_im = Image.fromarray(cv2.cvtColor(new_im, cv2.COLOR_BGR2RGB))
+        new_im.paste(pil_img_2, (0, 0), pil_img_2)
+        return new_im
+
+    def to_color_2(self, target, blend):  # 正片叠底
+        return np.array(np.multiply(target / 256, blend / 256) * 256, dtype=np.uint8)
+
+    def to_color_1(self, img1, img2):  # PS颜色模式
+        img1 = cv2.cvtColor(img1, cv2.COLOR_BGR2HSV)
+        img2 = cv2.cvtColor(img2, cv2.COLOR_BGR2HSV)
+        img2[:, :, 0] = img1[:, :, 0]
+        img2[:, :, 1] = img1[:, :, 1]
+        res = cv2.cvtColor(img2, cv2.COLOR_HSV2BGR)
+        return res
+
+
+if __name__ == '__main__':
+    from collections import defaultdict
+
+
+    def set_dict(one_dict):
+        _ = defaultdict(str)
+        for i, v in one_dict.items():
+            if isinstance(v, dict):
+                v = set_dict(v)
+            _[i] = v
+        return _
+
+
+    def deal_one_pic(img: Image, data=None):
+        """
+        通用图片处理器
+        1、基于某原始图,进行加工,包括粘贴、单图处理等逻辑
+        """
+        data = [set_dict(x) for x in data]
+
+        for command in data:
+            if command["command"] == "resize":
+                img = OnePicDeal().resize(img, command)
+                continue
+            if command["command"] == "paste_img":
+                img = OnePicDeal().paste_img(img, command)
+                continue
+            if command["command"] == "crop_img":
+                img = OnePicDeal().crop_img(img, command)
+                continue
+            if command["command"] == "radius":
+                img = OnePicDeal().radius(img, command)
+                continue
+            if command["command"] == "add_text":
+                img = OnePicDeal().add_text(img, command)
+                continue
+
+        return img
+
+
+    image_path = r"D:\MyDocuments\PythonCode\MyPython\red_dragonfly\deal_pics\auto_capture_V2\IPC\output\-秋季订货会夏季补充给葛明明\NUM24106316\阴影图处理\NUM24106316(3)_后跟_阴影.png"
+    to_paste_img = Image.open(image_path)
+    data = []
+    bg_color = (246, 246, 246)
+    # 单图缩放处理
+    view = "后跟"
+    data.append({"command": "resize",
+                 "plugins_mode": "pixel",  # pixel 相对(宽度、高度、其他参考图),或绝对像素
+                 "base_im": {"im": ""},  # 参考基于其他图 PIL格式
+                 "base": "width",  # base:pixel,width,height,by_im 基于长边、基于短边  (基于短边时,则缩放到指定尺寸)by_im确保能塞进参考图内
+                 "value": 1300 if view == "后跟" else 2200,  # 固定值,如果为百分比,则为0
+                 "percentage": 0, })  # 百分比
+
+    view_dict = {"俯视": (0, 0, 1100, 1200),
+                 "内里": (-1, -100, 1100, 1200),
+                 "后跟": (0, -100, 1100, 1200),
+                 "鞋底": (0, -100, 1100, 1200),
+                 }
+
+    data.append({
+        "command": "crop_img",
+        "img": "",
+        "pos": {"plugins_mode": "pixel",  # pixel
+                "base": "sw",  # nw,nc,ne,ec ... 各个方向参考点
+                "value": view_dict[view],
+                },
+        "color_fill": bg_color,
+    })
+
+    # 处理圆角
+    data.append({"command": "radius",  # radius
+                 "plugins_mode": "relative",  # pixel 相对(短边),或绝对像素
+                 "circular_pos": (1, 0, 0, 1),  # 从左上角顺时针,记录圆角数量
+                 "value": 100,  # 固定值,如果为百分比,则为0
+                 "percentage": 0, })  # 百分比
+
+    to_paste_img = deal_one_pic(to_paste_img, data)
+    # print(to_paste_img.size)
+
+    # 粘贴到白底图上
+    img = Image.new("RGB", (1200, 1316), (255, 255, 255))
+    data = []
+    data.append({
+        "command": "paste_img",
+        "img": {"im": to_paste_img},
+        "pos": {"plugins_mode": "pixel",  # pixel  relative
+                "base": "wc",  # nw,nc,ne,ec,... 各个方向参考点
+                "value": (100, 0),
+                "percentage": "",
+                },
+        "margins": (0, 0, 0, 0),  # 上下左右边距
+    })
+    img = deal_one_pic(img, data)
+    img.show()

+ 95 - 0
python/service/init_load_source.py

@@ -0,0 +1,95 @@
+import asyncio
+import json
+import time
+from settings import HLM_HOST
+import aiohttp
+import os
+from utils.utils_func import get_md5, get_modified_time, compare_two_times, check_path
+from utils.common import message_queue
+
+class init_load_source:
+
+    async def load_source(self):
+        await self.down_resouce()
+
+    async def down_resouce(self):
+        response_data = await self.get_update_file()
+        if response_data:
+            for relative_file_path, value in response_data['data'].items():
+                file_path = f"{os.getcwd()}/{relative_file_path}"
+                if os.path.exists(file_path):
+                    file_md5 = get_md5(file_path)
+                    if file_md5 != value["file_md5"]:
+                        file_modified_time = get_modified_time(file_path)
+                        if compare_two_times(file_modified_time, response_data["update_time"]) == "left_new":
+                            continue
+                        else:
+                            print(f"md5 不同 开始下载:{file_path}")
+                            await self.async_download_file(value["url"], file_path)
+                    else:
+                        pass
+                else:
+                    print(f"文件不存在 开始下载:{file_path}")
+                    await self.send_message(f"开始下载:{file_path}")
+                    await self.async_download_file(value["url"], file_path)
+        else:
+            print("获取更新文件内容失败")
+
+    async def send_message(self, msg):
+
+        message = {
+            'code': 0,
+            'msg_code': 'down_source',
+            'message': msg
+        }
+        await message_queue.put(json.dumps(message))
+
+
+    async def get_update_file(self, type="client_camera", plugins_name="plugins_A"):
+        """异步获取指定类型的插件文件更新信息
+
+        Args:
+            type (str, optional): 设备类型标识,默认为"client_camera"
+            plugins_name (str, optional): 需要查询的插件名称,默认为"plugins_A"
+
+        Returns:
+            dict or None: 成功时返回插件文件的JSON数据,失败或未找到时返回None
+        """
+        url = HLM_HOST + "/api/openai/query_client_addons"
+        params = {"type": type}
+        try:
+            async with aiohttp.ClientSession() as session:
+                # 发送初始请求获取插件列表数据
+                async with session.get(url, params=params, timeout=10) as response:
+                    response.raise_for_status()
+                    raw_data = await response.json()
+
+                    if not raw_data or not raw_data.get("data"):
+                        return None
+
+                # 从插件列表中查找匹配的插件项
+                item = next(
+                    (item for item in raw_data["data"]["list"] if item["name"] == plugins_name),
+                    None
+                )
+                if item:
+                    # 根据插件项中的URL获取具体文件内容
+                    url = item["url"]
+                    async with session.get(url, timeout=10) as item_response:
+                        item_response.raise_for_status()
+                        return await item_response.json()
+                return None
+        except Exception as e:
+            print(f"An error occurred: {e}")
+            return None
+
+
+    async def async_download_file(self, url: str, file_path: str):
+        """异步下载文件"""
+        async with aiohttp.ClientSession() as session:
+            async with session.get(url) as response:
+                root_path, file_name = os.path.split(file_path)
+                check_path(root_path)
+                with open(file_path, 'wb') as f:
+                    async for chunk in response.content.iter_chunked(1024):
+                        f.write(chunk)

+ 279 - 0
python/service/module_generate_goods_art_no_table.py

@@ -0,0 +1,279 @@
+
+import threading
+import xlsxwriter
+import shutil
+from pic_deal import Picture
+
+
+from excel_base_func import *
+
+
+class GenerateGoodsArtNoTable():
+
+
+    def __init__(self):
+        pass
+
+
+
+    def show_progress(self, data):
+        progress_bar_value = data["progress_bar_value"]
+        self.label_5.setText(data["type"])
+        self.progressBar.setValue(progress_bar_value)
+        pass
+
+    def check(self):
+        _path = self.image_dir + "/历史"
+        if not os.path.exists(_path):
+            os.mkdir(_path)
+        return True
+
+    def set_state(self, state_value: int):
+        # 0禁用  1进行中  2已结束
+        if state_value not in [0, 1, 2]:
+            return
+        self.state = state_value
+        if self.state == 0:
+            self.pushButton.setText("执行中")
+            self.pushButton.setEnabled(False)
+        if self.state == 1:
+            self.progressBar.show()
+            self.textBrowser_2.show()
+            self.pushButton.setText("执行中")
+            self.pushButton.setEnabled(False)
+            self.textBrowser_2.clear()
+        if self.state == 2:
+            self.pushButton.setText("执行完毕")
+            self.pushButton.setEnabled(True)
+            self.progressBar.hide()
+
+    def run(self):
+        self.set_state(state_value=1)
+        self.t = threading.Thread(target=self.run_by_thread, args=())
+        self.t.start()
+
+    def show_progress_detail(self, text):
+        self.textBrowser_2.append(text)
+
+    def run_by_thread(self, dir_path=None):
+        if not dir_path:
+            dir_path = self.label_4.text()
+        if not dir_path:
+            self.show_progress_detail("请选择文件夹")
+            self.set_state(state_value=2)
+            return
+
+        if not os.path.exists(dir_path):
+            self.show_progress_detail("该文件夹不存在")
+            self.set_state(state_value=2)
+            return
+        GenerateGoodsArtNoTable.deal(dir_path)
+        # 完成处理
+        self.set_state(state_value=2)
+
+    def save_as_excel(self, out_excel_data, out_excel_path=None):
+        self.show_progress_detail("开始尝试导出Excel文件~~~~")
+
+        def close_book(_book):
+            try:
+                _book.close()
+            except BaseException as e:
+                print(e)
+                self.show_progress_detail("请先关闭文件:{}".format(out_excel_path))
+                return False
+            return True
+
+        options = {
+            "default_format_properties": {
+                "align": "left",
+                "valign": "vcenter",
+                "text_wrap": True,
+            }
+        }
+        book = xlsxwriter.Workbook(filename=out_excel_path, options=options)
+        sheet = book.add_worksheet("sheet1")
+        # sheet.freeze_panes(1, 2)
+        sheet.set_column("B:B", 17)
+
+        sheet.write_row("A1", ["货号", "缩略图"])
+        for index, data in enumerate(out_excel_data):
+            # print(data)
+            goods_no, image_file = data
+            try:
+                im = Image.open(image_file)
+                im_x, im_y = im.size
+                image_width = 100
+                image_height = int(im_y * image_width / im_x)
+                sheet.set_row(index + 1, 95)
+                x_scale = round(image_width / im_x, 2)  # 固定宽度/要插入的原始图片宽
+                y_scale = round(image_height / im_y, 2)  # 固定高度/要插入的原始图片高
+
+                sheet.insert_image(
+                    index + 1,
+                    1,
+                    image_file,
+                    {
+                        "x_scale": x_scale,
+                        "y_scale": y_scale,
+                        "x_offset": 5,
+                        "y_offset": 5,
+                    },
+                )
+            except:
+                self.show_progress_detail("图片异常,{}".format(image_file))
+                pass
+            sheet.write_row("A{}".format(index + 2), [goods_no])
+
+        close_book(book)
+        self.show_progress_detail("已保存文件至:{}".format(out_excel_path))
+        # while 1:
+        #     if self.is_del:
+        #         break
+        #
+        #     if not close_book(book):
+        #         time.sleep(1)
+        #         self.show_progress_detail("请先关闭文件:{}".format(out_excel_path))
+        #     else:
+        #         self.show_progress_detail("已保存文件至:{}".format(out_excel_path))
+        #         break
+
+    @classmethod
+    def deal(cls, dir_path):
+        def close_book(_book):
+            try:
+                _book.close()
+            except BaseException as e:
+                print(e)
+                return False
+            return True
+
+        # print("dir_path", dir_path)
+        out_excel_data = []
+        for goods_art_no_folder in os.listdir(dir_path):  # 遍历货号文件夹集合
+            if not os.path.isdir(
+                "{}/{}".format(dir_path, goods_art_no_folder)
+            ):  # 非文件夹进行过滤
+                continue
+            if "软件" in goods_art_no_folder:
+                continue
+
+            # print("goods_art_no_folder", goods_art_no_folder)
+            # 如果存在800的主图,则优先进行使用
+            big_image_folder_path = "{}/{}/800x800".format(
+                dir_path, goods_art_no_folder
+            )
+            if not os.path.exists(big_image_folder_path):
+                os.mkdir(big_image_folder_path)
+            all_big_images = os.listdir(big_image_folder_path)
+            goods_pic_total = len(all_big_images)
+            _Type = [".png", ".PNG", ".jpg", ".JPG", ".gif", ".GIF", ".jpge", ".JPGE"]
+            thumb_image_file_path = None
+            # print("all_big_images",all_big_images)
+            if all_big_images:
+                for _file in all_big_images:
+                    # print(_file)
+                    file_name, e = os.path.splitext(_file)
+                    # print(file_name, e)
+                    if e in _Type:
+                        thumb_image_file_path = "{}/{}/800x800/{}".format(
+                            dir_path, goods_art_no_folder, _file
+                        )
+                        break
+
+            # 如果不存在主图则进行使用原始图
+            if thumb_image_file_path is None:
+                _path = "{}/{}/原始图".format(dir_path, goods_art_no_folder)
+                if not os.path.exists(_path):
+                    continue
+
+                all_original_images = os.listdir(_path)  # 遍历货号原始图文件夹
+                goods_pic_total = len(all_original_images)
+                if not all_original_images:
+                    continue
+                image_file = all_original_images[0]  # 取第一个货号图
+                image_file_path = "{}/{}/原始图/{}".format(
+                    dir_path, goods_art_no_folder, image_file
+                )
+
+                if not os.path.exists(
+                    "{}/{}/200images".format(dir_path, goods_art_no_folder)
+                ):
+                    os.mkdir("{}/{}/200images".format(dir_path, goods_art_no_folder))
+
+                thumb_image_file_path = "{}/{}/200images/{}".format(
+                    dir_path, goods_art_no_folder, image_file
+                )
+                if not os.path.exists(thumb_image_file_path):
+                    # 开始触发进行压缩生成文件
+                    shutil.copy(image_file_path, thumb_image_file_path)  # 复制文件
+                    pic = Picture(thumb_image_file_path)
+                    pic.resize(width=600)
+                    pic.save_img(thumb_image_file_path)
+            # print("thumb_image_file_path", thumb_image_file_path)
+
+            goods_number = ""
+            if "@" in goods_art_no_folder:
+                _ = goods_art_no_folder.split("@")
+                goods_art_no_folder = _[0]
+                goods_number = _[1].replace("NUM", "")
+
+            out_excel_data.append(
+                [
+                    goods_number,
+                    goods_art_no_folder,
+                    thumb_image_file_path,
+                    goods_pic_total,
+                ]
+            )
+
+        if out_excel_data:
+            out_excel_path = "{}/货号表-{}.xlsx".format(dir_path, time.time())
+            options = {
+                "default_format_properties": {
+                    "align": "left",
+                    "valign": "vcenter",
+                    "text_wrap": True,
+                }
+            }
+            book = xlsxwriter.Workbook(filename=out_excel_path, options=options)
+            sheet = book.add_worksheet("sheet1")
+            # sheet.freeze_panes(1, 2)
+            sheet.set_column("B:B", 17)
+            sheet.set_column("D:D", 20)
+
+            sheet.write_row("A1", ["编号", "原货号", "新货号", "缩略图", "原始图张数"])
+            for index, data in enumerate(out_excel_data):
+                # print(data)
+                goods_number, goods_art_no, image_file, goods_pic_total = data
+                try:
+                    im = Image.open(image_file)
+                    im_x, im_y = im.size
+                    image_width = 100
+                    image_height = int(im_y * image_width / im_x)
+                    sheet.set_row(index + 1, 95)
+                    x_scale = round(
+                        image_width / im_x, 2
+                    )  # 固定宽度/要插入的原始图片宽
+                    y_scale = round(
+                        image_height / im_y, 2
+                    )  # 固定高度/要插入的原始图片高
+                    sheet.insert_image(
+                        index + 1,
+                        3,
+                        image_file,
+                        {
+                            "x_scale": x_scale,
+                            "y_scale": y_scale,
+                            "x_offset": 5,
+                            "y_offset": 5,
+                            "object_position":1,
+                        },
+                    )
+
+
+                except:
+                    pass
+                sheet.write_row("A{}".format(index + 2), [goods_number])
+                sheet.write_row("B{}".format(index + 2), [goods_art_no])
+                sheet.write_row("E{}".format(index + 2), [goods_pic_total])
+            close_book(book)

+ 102 - 0
python/service/pic_deal.py

@@ -0,0 +1,102 @@
+from PIL import Image
+from io import BytesIO
+
+from service.remove_bg_ali import RemoveBgALi as RemoveBg
+
+
+
+
+class Picture:
+    def __init__(self, in_path):
+        self.im = Image.open(in_path)
+        self.x, self.y = self.im.size
+        # print(self.x, self.y)
+
+    def resize_regular(self, width, high):
+        self.im = self.im.resize((width, high), Image.Resampling.LANCZOS)
+
+    def save_img(self, out_path, quality=90):
+        # self.im = self.im.convert("RGB")
+        self.im.save(out_path, quality=quality)
+
+    def crop_with_png(self):
+        self.im = self.im.crop(self.im.getbbox())
+        self.x, self.y = self.im.size
+
+    def resize(self, width=None, high=None):
+        if width:
+            re_x = int(width)
+            re_y = int(self.y * re_x / self.x)
+        else:
+            re_y = int(high)
+            re_x = int(self.x * re_y / self.y)
+        self.im = self.im.resize((re_x, re_y))
+        self.x, self.y = self.im.size
+
+    def save_to_io(self, format):
+        img = BytesIO()
+        self.im.save(img, format=format)  # format: PNG or JPEG
+        img.seek(0)  # rewind to the start
+        return img
+
+    def corp_square(self):
+        if self.y < self.x:
+            return
+        self.im = self.im.crop((0, int((self.y - self.x) / 2), self.x, self.y - int((self.y - self.x) / 2)))
+
+
+class PicDeal(object):
+    def __init__(self):
+        self.mode = "ali"
+
+    def remove_bg(self, in_path, out_path):
+        """
+        1、三方软件进行抠图处理
+        2、图片进行清晰度转化,转化成原始图片尺寸
+        """
+
+        if Remove_Bg_Mode == "zuo_tang" or Remove_Bg_Mode == "ali":
+            remove_bg_ins = RemoveBg()
+            if remove_bg_ins.run(in_path, out_path):
+                return True
+        else:
+            return False
+
+    def create_800image(self, original_image, cut_image_path, out_path, image_mode=None):
+        """
+        image_mode: 处理模式,主要用于图片缩放大小确定
+        original_image:原始图,用于获取商品阴影
+        cut_image_path:抠图结果
+        """
+
+        image = Picture(original_image)
+        image.crop_with_png()
+        # image.im.show()
+        if image.x >= image.y:
+            image.resize(width=575)
+        else:
+            image.resize(high=575)
+
+            image_bg = Image.open("resources/goods_bg.jpg")
+            image_bg_x, image_bg_y = image_bg.size
+            image_x, image_y = image.x, image.y
+            _x = int((image_bg_x - image_x) / 2)
+            _y = int((image_bg_y - image_y) / 2)
+            image_bg.paste(image.im, (_x, _y), image.im)
+            # image_bg.show()
+            image_bg.save(out_path)
+
+    def resize_and_save(self, image_path, out_path, width):
+        im = Image.open(image_path)
+        x, y = im.size
+        re_x = int(width)
+        re_y = int(y * re_x / x)
+        im = im.resize((re_x, re_y), Image.ANTIALIAS)
+        im.save(out_path)
+
+    def save(self, out_path):
+        pass
+
+
+if __name__ == '__main__':
+    pass

+ 268 - 0
python/service/remove_bg_ali.py

@@ -0,0 +1,268 @@
+import copy
+import json
+import os
+from PIL import Image
+from alibabacloud_imageseg20191230.client import Client as imageseg20191230Client
+from alibabacloud_imageseg20191230.models import SegmentCommodityAdvanceRequest
+from alibabacloud_imageseg20191230 import models as imageseg_20191230_models
+from alibabacloud_tea_util.models import RuntimeOptions
+from alibabacloud_tea_openapi import models as open_api_models
+from alibabacloud_tea_openapi.models import Config
+from alibabacloud_tea_util import models as util_models
+import requests
+from io import BytesIO
+import cv2
+import numpy as np
+from func_timeout import func_set_timeout
+from func_timeout import FunctionTimedOut
+
+# 自己的
+AccessKeyId = 'LTAI5t7GVSbV5GuqUo935v4f'
+AccessKeySecret = 'IAe8CMw5PTp61zrV2rEYeLtKRVdL3A'
+
+# 惠利玛公司的KEY
+# AccessKeyId = 'LTAI5tCk4p881X8hymj2FYFk'
+# AccessKeySecret = 'rQMgHwciTN4Gusbpt8CM8tflgsxh1V'
+
+
+# https://help.aliyun.com/zh/viapi/developer-reference/python?spm=a2c4g.11186623.0.i0#task-2252575
+# pip install alibabacloud_goodstech20191230
+# pip install alibabacloud_tea_openapi
+# pip install alibabacloud_tea_util
+
+class Segment(object):
+    def __init__(self):
+        self.client = self.create_client()
+
+    def get_no_bg_common(self, file_path):
+        # 初始化RuntimeObject
+        runtime_option = RuntimeOptions()
+        try:
+            # 场景一:文件在本地
+            img = open(file_path, 'rb')
+            # 使用完成之后记得调用img.close()关闭流
+            # 场景二,使用任意可访问的url
+            # url = 'https://viapi-test-bj.oss-cn-beijing.aliyuncs.com/viapi-3.0domepic/ocr/RecognizeBankCard/yhk1.jpg'
+            # img = io.BytesIO(urlopen(url).read())
+            # 4、初始化Request,这里只是以RecognizeBankCard为例,其他能力请使用相应能力对应的类
+            request = SegmentCommodityAdvanceRequest()
+            request.image_urlobject = img
+
+            # 5、调用api,注意,recognize_bank_card_advance需要更换为相应能力对应的方法名。方法名是根据能力名称按照一定规范形成的,如能力名称为SegmentCommonImage,对应方法名应该为segment_common_image_advance。
+            response = self.client.segment_common_image_advance(request, runtime_option)
+            # 获取整体结果
+            # print(response.body)
+            img.close()
+            return response.body
+            # 获取单个字段,这里只是一个例子,具体能力下的字段需要看具体能力的文档
+            # print(response.body.data.card_number)
+            # tips: 可通过response.body.__dict__查看属性名称
+        except Exception as error:
+            # 获取整体报错信息
+            print("error", error)
+            return None
+            # 获取单个字段
+            # print(error.code)
+            # tips: 可通过error.__dict__查看属性名称
+
+    def get_no_bg_goods(self, file_path=None, _im=None):
+        # file_path = r"D:\MyDocuments\PythonCode\MyPython\red_dragonfly\deal_pics\change_color_2\test\_MG_9061.jpg"
+        # file_path_1 = r"D:\MyDocuments\PythonCode\MyPython\red_dragonfly\deal_pics\change_color_2\test\_MG_9061_resize.png"
+        # if file_path:
+        #     img = open(file_path, 'rb')
+        # if _im:
+        # https://blog.csdn.net/weixin_43411585/article/details/107780941
+        im = _im
+        # im.save(file_path)
+        img = BytesIO()
+        im.save(img, format='JPEG')  # format: PNG or JPEG
+        img.seek(0)  # rewind to the start
+
+        # img = img_byte.getvalue()  # im对象转为二进制流
+        # with open(file_path, "wb") as binary_file:
+        #     binary_file.write(im.tobytes())
+
+        # file_path = r"D:\MyDocuments\PythonCode\MyPython\red_dragonfly\deal_pics\change_color_2\test\1.png"
+        # img = open(file_path, 'rb')
+
+        request = imageseg_20191230_models.SegmentCommodityAdvanceRequest()
+        request.image_urlobject = img
+        client = self.create_client()
+        # 5、调用api,注意,recognize_bank_card_advance需要更换为相应能力对应的方法名。方法名是根据能力名称按照一定规范形成的,如能力名称为SegmentCommonImage,对应方法名应该为segment_common_image_advance。
+        runtime = util_models.RuntimeOptions()
+        response = client.segment_commodity_advance(request, runtime)
+        # img.close()
+        # print("1111111111111", response.body)
+        return response.body
+
+    def create_client(self):
+        """
+        使用AK&SK初始化账号Client
+        @param access_key_id:
+        @param access_key_secret:
+        @return: Client
+        @throws Exception
+        """
+        config = open_api_models.Config(
+            # 必填,您的 AccessKey ID,
+            access_key_id=AccessKeyId,
+            # 必填,您的 AccessKey Secret,
+            access_key_secret=AccessKeySecret
+        )
+        # 访问的域名
+        config.endpoint = f'imageseg.cn-shanghai.aliyuncs.com'
+        return imageseg20191230Client(config)
+
+
+class Picture:
+    def __init__(self, in_path, im=None):
+        if im:
+            self.im = im
+        else:
+            self.im = Image.open(in_path)
+        self.x, self.y = self.im.size
+        # print(self.x, self.y)
+
+    def save_img(self, outpath, quality=90):
+        # self.im = self.im.convert("RGB")
+        self.im.save(outpath, quality=quality)
+
+    def resize(self, width):
+        re_x = int(width)
+        re_y = int(self.y * re_x / self.x)
+        self.im = self.im.resize((re_x, re_y), Image.BICUBIC)
+        self.x, self.y = self.im.size
+
+    def resize_by_heigh(self, heigh):
+        re_y = int(heigh)
+        re_x = int(self.x * re_y / self.y)
+        self.im = self.im.resize((re_x, re_y), Image.BICUBIC)
+        self.x, self.y = self.im.size
+
+
+class RemoveBgALi(object):
+    def __init__(self):
+        self.segment = Segment()
+
+    @func_set_timeout(30)
+    def get_image_cut(self, file_path, out_file_path=None, original_im=None):
+        if original_im:
+            original_pic = Picture(in_path=None, im=original_im)
+        else:
+            original_pic = Picture(file_path)
+
+        if original_pic.im.mode != "RGB":
+            print("抠图图片不能是PNG")
+            return None
+
+        new_pic = copy.copy(original_pic)
+        after_need_resize = False
+        if new_pic.x > new_pic.y:
+            if new_pic.x > 2000:
+                after_need_resize = True
+                new_pic.resize(2000)
+        else:
+            if new_pic.y > 2000:
+                after_need_resize = True
+                new_pic.resize_by_heigh(heigh=2000)
+
+        # new_pic.im.show()
+        body = self.segment.get_no_bg_goods(file_path=None, _im=new_pic.im)
+        body = eval(str(body))
+        try:
+            image_url = body["Data"]["ImageURL"]
+        except BaseException as e:
+            print("阿里抠图错误:", e)
+            # 处理失败,需要删除过程图片
+            return None
+        # 字节流转PIL对象
+        response = requests.get(image_url)
+        pic = response.content
+        _img_im = Image.open(BytesIO(pic))  # 阿里返回的抠图结果 已转PIL对象
+        # 原图更大,则需要执行CV处理
+        if after_need_resize:
+            # 将抠图结果转成mask
+            # _img_im = Image.open(_path)
+            # 将抠图结果放大到原始图大小
+            _img_im = _img_im.resize(original_pic.im.size)
+            new_big_mask = Image.new('RGB', _img_im.size, (0, 0, 0))
+            white = Image.new('RGB', _img_im.size, (255, 255, 255))
+            new_big_mask.paste(white, mask=_img_im.split()[3])
+
+            # ---------制作选区缩小的mask
+            # mask = cv2.imread(mask_path)
+            # mask = cv2.cvtColor(mask, cv2.COLOR_BGR2GRAY)
+            mask = cv2.cvtColor(np.asarray(new_big_mask), cv2.COLOR_BGR2GRAY)  # 将PIL 格式转换为 CV对象
+            mask[mask != 255] = 0
+            # 黑白反转
+            # mask = 255 - mask
+            # 选区缩小10
+            kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (10, 10))
+            erode_im = cv2.morphologyEx(mask, cv2.MORPH_ERODE, kernel)
+
+            # -------再进行抠图处理
+            mask = Image.fromarray(cv2.cvtColor(erode_im, cv2.COLOR_GRAY2RGBA))  # CV 对象转 PIL
+            transparent_im = Image.new('RGBA', original_pic.im.size, (0, 0, 0, 0))
+            # original_pic.im.show()
+            # mask.show()
+            transparent_im.paste(original_pic.im, (0, 0), mask.convert('L'))
+            # transparent_im.show()
+            # 上述抠图结果进行拼接
+            _img_im.paste(transparent_im, (0, 0), transparent_im)
+            # _img_im.show("11111111111111111111111")
+        if out_file_path:
+            _img_im.save(out_file_path)
+        return _img_im
+
+    def get_image_cut1(self, file_path, out_file_path=None):
+        original_pic = Picture(file_path)
+        new_pic = copy.copy(original_pic)
+        if new_pic.x > 2000:
+            new_pic.resize(2000)
+        # new_pic.im.show()
+        body = self.segment.get_no_bg_goods(file_path=out_file_path, _im=new_pic.im)
+        body = eval(str(body))
+        try:
+            image_url = body["Data"]["ImageURL"]
+        except BaseException as e:
+            print("阿里抠图错误:", e)
+            # 处理失败,需要删除过程图片
+            return None
+        # 字节流转PIL对象
+        response = requests.get(image_url)
+        pic = response.content
+        _img_im = Image.open(BytesIO(pic))  # 阿里返回的抠图结果 已转PIL对象
+
+        if original_pic.x > 2000:
+            # 原图更大,则需要执行CV处理
+            # _img_im.show()
+            # 对mask进行放大,然后进行抠图处理
+            print("对mask进行放大,然后进行抠图处理")
+            transparent_im = Image.new('RGBA', original_pic.im.size, (0, 0, 0, 0))
+            # original_pic.im.show()
+            # mask.show()
+            _img_im = _img_im.resize((original_pic.x, original_pic.y))
+            # _img_im.show()
+            transparent_im.paste(original_pic.im, (0, 0), mask=_img_im)
+            # transparent_im.show()
+            # transparent_im.show()
+            _img_im = transparent_im
+            # 上述抠图结果进行拼接
+            # _img_im.paste(transparent_im, (0, 0), transparent_im)
+            pass
+
+        _img_im.save(out_file_path)
+        return _img_im
+
+    def download_picture(self, url, out_path):
+        response = requests.get(url)
+        pic = response.content
+        with open(out_path, 'wb') as f:
+            f.write(pic)
+
+
+if __name__ == '__main__':
+    r = RemoveBgALi()
+    path = r"D:\MyDocuments\PythonCode\MyPython\red_dragonfly\deal_pics\auto_capture_V2\IPC\test\171112057820408.png"
+    out_path = "{}._no_bg-out.png".format(path)
+    r.get_image_cut(path, out_file_path=out_path)

+ 1004 - 0
python/service/run_main.py

@@ -0,0 +1,1004 @@
+import settings
+from module.view_control.generate_goods_no_detail_pic import detail_func
+import json
+from base import *
+from import_qt_mode import *
+from module.view_control.match_and_cutout_mode_control.base_deal_image_v2 import BaseDealImage
+from module.view_control.MineQWidget import DialogShow, WorkerOneThread
+import threading
+from concurrent.futures import ThreadPoolExecutor
+import concurrent.futures
+from module.view_control.generate_goods_no_detail_pic.data import DataModeGenerateDetail, DataModeUploadPic
+from module.view_control.generate_goods_no_detail_pic.detail_func import create_folder
+import time
+from PIL import Image
+from io import BytesIO
+import os, re
+from functools import partial
+# from multiprocessing import Process, Queue
+import pickle
+from  base_deal import BaseDealImage
+
+
+class RunMain(QThread):
+    run_end_sign = Signal(dict)
+    show_dialog_sign = Signal(dict)
+    show_progress_detail_sign = Signal(str)
+
+    # 定义一个信号用于请求显示对话框
+    show_dialog_signal = Signal()
+    # 定义一个信号用于返回对话框的结果
+    dialog_result_signal = Signal(str)
+
+    # dialog_result_signal = Signal(str)
+
+    def __init__(self, windows):
+        super().__init__()
+        self.windows = windows
+        self.dialog_result = ""
+        self.data_mode_generate_detail = DataModeGenerateDetail()
+        self.dialog_result_signal.connect(self.on_dialog_result)
+        self.event = threading.Event()
+
+    def set_state(self, state_value: int):
+        # 0禁用  1进行中  2已结束
+        if state_value not in [0, 1, 2, 99]:
+            return
+        # self.windows.change_state_sign.emit(state_value)
+        # self.windows.set_state(state_value)
+
+    # 抠图前先做数据规整处理;类似详情图生成逻辑
+    def check_before_cutout(self, config_data):
+        return_data = {
+            "code": 99,
+            "message": "",
+            "data": {
+                "all_goods_art_no_folder_data": [],
+                "config_data": config_data,
+            },
+        }
+
+        image_dir = config_data["image_dir"]
+        image_order = config_data["image_order"]
+        is_check_number = config_data["is_check_number"]
+        is_filter = config_data["cutout_is_pass"]
+        resize_image_view = config_data["resize_image_view"]
+        logo_path = config_data["logo_path"]
+        cutout_mode = config_data["cutout_mode"]  # 是否精细化抠图,默认为普通抠图
+        special_goods_art_no_folder_line = config_data["special_goods_art_no_folder_line"]
+
+        # 自动处理红蜻蜓货号,进行重命名
+        if settings.PROJECT == "红蜻蜓":
+            # 规整红蜻蜓货号图
+            all_goods_art_no_folder_data = get_all_goods_art_no_folder(path=image_dir)
+            BaseDealImage().rename_folder_for_hqt(all_goods_art_no_folder_data=all_goods_art_no_folder_data)
+
+        # 重新获取文件夹信息
+        all_goods_art_no_folder_data = get_all_goods_art_no_folder(path=image_dir)
+
+        f = True
+        is_do_other = False
+        if is_do_other:
+            for i in all_goods_art_no_folder_data:
+                i["label"] = "不处理"
+            is_filter = False
+            specified_goods_art_no_folder = special_goods_art_no_folder_line
+            specified_goods_art_no_folder = specified_goods_art_no_folder.strip()
+            specified_goods_art_no_folder = specified_goods_art_no_folder.replace(",", ",")
+            specified_goods_art_no_folder_list = specified_goods_art_no_folder.split(",")
+            specified_goods_art_no_folder_list = [x for x in specified_goods_art_no_folder_list if x]
+            if not specified_goods_art_no_folder_list:
+                return_data["message"] += '请手动输入文件夹名称(多选),或关闭指定文件夹模式\n'
+            else:
+                for i in all_goods_art_no_folder_data:
+                    if i["folder_path"] in specified_goods_art_no_folder_list:
+                        i["label"] = "待处理"
+            # 哪些数据不合规
+            all_folder_name_list = [x["folder_name"] for x in all_goods_art_no_folder_data]
+            for goods_art_no_folder_name in specified_goods_art_no_folder_list:
+                if goods_art_no_folder_name not in all_folder_name_list:
+                    return_data["message"] += '文件夹:{},在您选的目录下不存在\n'.format(goods_art_no_folder_name)
+                    f = False
+
+        if not f:
+            self.set_state(state_value=2)
+            return
+
+        # 清空指定文件夹的已抠图文件
+        if is_do_other:
+            for folder_data in all_goods_art_no_folder_data:
+                goods_art_no_folder_path = "{}/原始图_已抠图".format(folder_data["folder_path"])
+                if os.path.exists(goods_art_no_folder_path):
+                    remove_all_file(goods_art_no_folder_path)
+
+        return_data["data"]["succeed_folder_list"] = 1
+
+        # ==================检查填写的图片视角是否符合要求
+        res = BaseDealImage().getImageOrder(image_order=image_order, resize_image_view=resize_image_view)
+        if res['code'] != 0:
+            return_data["message"] += "{}\n".format(res['msg'])
+            return return_data
+        else:
+            # 图片命名顺序
+            image_order_list = res['imageOrderList']
+            for goods_art_no_folder_data in all_goods_art_no_folder_data:
+                if goods_art_no_folder_data["label"] != "待处理":
+                    continue
+                goods_art_no_folder_data["image_order_list"] = image_order_list
+
+        # ================是否过滤已有生成的文件夹
+        if is_filter:
+            for goods_art_no_folder_data in all_goods_art_no_folder_data:
+                if goods_art_no_folder_data["label"] != "待处理":
+                    continue
+                folder_path = goods_art_no_folder_data["folder_path"]
+                _p = "{}/800x800".format(folder_path)
+                if os.path.exists(_p):
+                    if len(os.listdir(_p)):
+                        goods_art_no_folder_data["label"] = "不处理"
+
+        # ================检查每个货号文件夹图片数量是否符合要求
+        all_goods_art_no_folder_data, message = BaseDealImage().check_folders_image_amount(all_goods_art_no_folder_data,
+                                                                                           image_order_list)
+        if message:
+            return_data["message"] += "{}\n".format(message)
+        return_data["code"] = 0
+        return_data["data"]["all_goods_art_no_folder_data"] = all_goods_art_no_folder_data
+        return_data["data"]["image_dir"] = image_dir
+        return_data["data"]["resize_image_view"] = resize_image_view
+        return_data["data"]["cutout_mode"] = cutout_mode
+        return_data["data"]["image_order_list"] = image_order_list
+        return_data["data"]["logo_path"] = logo_path
+        return return_data
+
+    # 抠图校验后的回调函数处理
+    def check_for_cutout_image_first_call_back(self, return_data):
+        # return_data = {
+        #     "code": 99,
+        #     "message": "",
+        #     "data": {
+        #         "all_goods_art_no_folder_data": [],
+        #     },
+        # }
+        code = return_data["code"]
+        config_data = return_data["data"]["config_data"]
+        config_data["sign_text"] = ""
+        # if code != 0:
+        #     # self.windows.show_message(return_data["message"])
+        #     # self.show_progress_detail(return_data["message"])
+        #     _dialog_dict = {"text": return_data["message"],
+        #                     "windows": self,
+        #                     }
+        #     self.show_dialog_sign.emit(_dialog_dict)
+        #     self.event.wait()
+        #     return
+
+        do_next = False
+        text = ""
+        all_goods_art_no_folder_data = return_data["data"]["all_goods_art_no_folder_data"]
+        button_1, button_2, button_3 = None, None, None
+        text += return_data["message"]
+        # 存在错误文件夹
+        error_folder = [x for x in all_goods_art_no_folder_data if x["label"] == "错误"]
+        todo_folder = [x for x in all_goods_art_no_folder_data if x["label"] == "待处理"]
+        if error_folder:
+            button_2 = "移除错误文件"
+        if error_folder and todo_folder:
+            button_1 = "移除并继续"
+            button_3 = "继续(忽略其他)"
+        if not error_folder and todo_folder:
+            button_1 = "继续"
+            # do_next = True
+        text += "\n==================\n错误数据:{}个,校验无误数据:{}个".format(len(error_folder), len(todo_folder))
+        self.show_progress_detail(text)
+        if button_1 is None and button_2 is None and button_3 is None:
+            pass
+        elif button_1 == "继续" and button_2 is None and button_3 is None:
+            do_next = True
+        else:
+            # print("runmain  179----------------")
+            # # print(self)
+            # # print(self.windows)
+            # _dialog_dict = {"text": text,
+            #                 "button_1": button_1,
+            #                 "button_2": button_2,
+            #                 "button_3": button_3,
+            #                 "windows": self,
+            #                 }
+            # self.show_dialog_sign.emit(_dialog_dict)
+            # # self.exec_()
+            # # 等待事件被设置
+            # self.event.wait()
+            # print("self.dialog_result", self.dialog_result)
+            # #
+            # # my_dialog = DialogShow(self.windows, text=text, button_1=button_1, button_2=button_2,
+            # #                        button_3=button_3)
+            # # ret = my_dialog.exec()
+            # print("460  ===============my_dialog.flag_name===============")
+            # print(my_dialog.flag_name)
+
+            if "移除" in self.dialog_result:
+                for error_folder_data in [x for x in all_goods_art_no_folder_data if x["label"] == "错误"]:
+                    self.move_error_folders(
+                        one_path=error_folder_data["folder_path"],
+                        target_folder="{}/软件-处理失败".format(config_data["image_dir"]),
+                    )
+            if "继续" in self.dialog_result:
+                do_next = True
+
+        if do_next:
+            all_goods_art_no_folder_data = [x for x in all_goods_art_no_folder_data if x["label"] == "待处理"]
+            print("===============all_goods_art_no_folder_data===============")
+            print(all_goods_art_no_folder_data)
+
+            new_func = partial(self.do_run_cutout_image,
+                               all_goods_art_no_folder_data=all_goods_art_no_folder_data,
+                               callback_func=self.show_progress_detail,
+                               image_order_list=return_data["data"]["image_order_list"],
+                               cutout_mode=return_data["data"]["cutout_mode"],
+                               resize_image_view=return_data["data"]["resize_image_view"],
+                               windows=self.windows,
+                               logo_path=return_data["data"]["logo_path"],
+                               config_data=config_data)
+            self._w_3 = WorkerOneThread(func=new_func, name="_w_3")
+            self._w_3.start()
+
+            # self.t = threading.Thread(target=self.do_run_cutout_image,
+            #                           kwargs={"all_goods_art_no_folder_data": all_goods_art_no_folder_data,
+            #                                   "callback_func": self.show_progress_detail,
+            #                                   "image_order_list": return_data["data"]["image_order_list"],
+            #                                   "cutout_mode": return_data["data"]["cutout_mode"],
+            #                                   "resize_image_view": return_data["data"]["resize_image_view"],
+            #                                   "windows": self.windows,
+            #                                   "logo_path": return_data["data"]["logo_path"],
+            #                                   "config_data": config_data,
+            #                                   })
+            # self.t.start()
+
+        else:
+            config_data["sign_text"] = "已结束抠图处理"
+            self.run_end_sign.emit(config_data)
+
+    def do_run_cutout_image(self,
+                            all_goods_art_no_folder_data,
+                            callback_func,
+                            image_order_list,
+                            cutout_mode,
+                            resize_image_view,
+                            windows,
+                            logo_path,
+                            config_data):
+
+        BaseDealImage().run_main(all_goods_art_no_folder_data=all_goods_art_no_folder_data,
+                                 callback_func=callback_func,
+                                 image_order_list=image_order_list,
+                                 cutout_mode=cutout_mode,
+                                 resize_image_view=resize_image_view,
+                                 windows=windows,
+                                 logo_path=logo_path,
+                                 )
+
+        # ==============完成处理==============
+        # self.set_state(state_value=2)
+        callback_func("已结束抠图处理")
+        config_data["sign_text"] = "已结束抠图处理"
+        self.run_end_sign.emit(config_data)
+
+    def do_run_cutout_image1111(self, all_goods_art_no_folder_data,
+                                callback_func,
+                                image_order_list,
+                                cutout_mode,
+                                resize_image_view,
+                                windows,
+                                logo_path,
+                                config_data):
+
+        max_workers = 1
+        with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
+            futures = []
+            futures.append(executor.submit(
+                BaseDealImage().run_main,
+                all_goods_art_no_folder_data=all_goods_art_no_folder_data,
+                callback_func=callback_func,
+                image_order_list=image_order_list,
+                cutout_mode=cutout_mode,
+                resize_image_view=resize_image_view,
+                windows=windows,
+                logo_path=logo_path,
+            ))
+
+            # 使用 wait 方法等待所有任务完成
+            done, not_done = concurrent.futures.wait(futures, timeout=60)
+            # 处理完成的任务
+            for future in done:
+                if settings.IS_TEST:
+                    result = future.result()
+                else:
+                    try:
+                        result = future.result()
+                    except BaseException as e:
+                        print("2039 图片处理失败.{}".format(e))
+                        callback_func("2039 处理失败.{}".format(e))
+
+        # ==============完成处理==============
+
+        # self.set_state(state_value=2)
+        callback_func("已结束抠图处理")
+        config_data["sign_text"] = "已结束抠图处理"
+        self.run_end_sign.emit(config_data)
+
+    def check_before_detail(self, config_data):
+
+        # =============
+        # 整体数据校验,返回错误内容,以及
+        # temp_name:模板名称
+        """
+        步骤:
+        1、 整体文件夹检查,并输出数据结构
+        2、数据进行对应规整(可能有excel,红蜻蜓等)
+        3、执行单个款数据处理
+        """
+        return_data = {
+            "code": 99,
+            "message": "",
+            "data": {
+                "error_folder_list": [],
+                "goods_no_dict": {},
+                "succeed_folder_list": [],
+                "temp_name": config_data["temp_name"],
+                "temp_name_list": config_data["temp_name_list"],
+                "assigned_page_dict": config_data["assigned_page_dict"],
+                "excel_temp_goods_no_data": {},  # 表格数据可能存在多模板,数据结构为一个款号下的多个模板的数据列表
+                "finally_goods_no_need_temps": {},  # 每个款号需要生成的模板数据
+                "config_data": config_data,
+            },
+        }
+        temp_name = config_data["temp_name"]
+        temp_name_list = config_data["temp_name_list"]
+        assigned_page_dict = config_data["assigned_page_dict"]
+        image_dir = config_data["image_dir"]
+        is_use_excel = config_data["is_use_excel"]
+        excel_path = config_data["excel_path"]
+        temp_class = config_data["temp_class"]
+        is_check_color_is_all = config_data["is_check_color_is_all"]
+        detail_is_pass = config_data["detail_is_pass"]
+
+        error_folder_list = []
+        # 遍历货号获取所有货号--可能为编号
+        folder_name_list = detail_func.get_all_dir_info(image_dir=image_dir)
+        if not folder_name_list:
+            return_data["message"] += "不存在任何货号/编号文件夹\n"
+            print("不存在任何货号/编号文件夹")
+            return return_data
+
+        # =========================组装数据---数据来源多种途径=========================
+        _result = {"code": 99, "message": "无法解析到数据,请检查登录企业"}
+        if not is_use_excel:
+            if settings.PROJECT == "红蜻蜓":
+                # goods_no_dict输出为文件夹下涉及到的所有款为key的字典,后续通过解析字典,进行提取对应文件夹
+                _result = self.data_mode_generate_detail.get_basic_goods_art_data_by_hqt_and_hlm(
+                    folder_name_list
+                )
+
+            elif settings.PROJECT == "惠利玛":
+                if settings.Company:
+                    if "惠利玛" in settings.Company:
+                        _result = self.data_mode_generate_detail.get_basic_goods_art_data_by_hqt_and_hlm(
+                            folder_name_list
+                        )
+        else:
+            keys = settings.keys
+            _result = (
+                self.data_mode_generate_detail.get_basic_goods_art_data_form_excel(
+                    folder_name_list,
+                    excel_path,
+                    keys,
+                )
+            )
+
+        if _result["code"] == 0:
+            remote_data = _result["data"]
+        else:
+            return_data["message"] += _result["message"] + "\n"
+            return return_data
+        # print(json.dumps(remote_data))
+        # =========================拼接数据组合为款数据=========================
+        """
+        1、获取所有文件夹基础数据内容
+        2、结合上述返回结果进行数据组合拼接
+        3、输出结果为按款为主键信息
+        4、注意模板ID
+        """
+        # 获取所有文件夹基础数据内容  检查不满足要求的文件不满足要求移动到错误文件夹
+        need_view_list = temp_class[temp_name].need_view
+        _all_dir_info_data = detail_func.get_all_dir_info_and_pic_info(
+            image_dir, folder_name_list, need_view_list
+        )
+        all_dir_info_data = {}
+        for one_folder, value in _all_dir_info_data.items():
+            if "message" in value:
+                if value["message"]:
+                    error_folder_list.append(
+                        {
+                            "folder_name": one_folder,
+                            "folder_path": "{}/{}".format(image_dir, one_folder),
+                        }
+                    )
+                    return_data["message"] += "文件夹:{} 结构错误:{}\n".format(
+                        one_folder, value["message"]
+                    )
+                    continue
+
+            # 符合要求的数据处理
+            all_dir_info_data[one_folder] = value
+
+        # 结合上述返回结果进行数据组合拼接
+        # 返回可能存在的情况:1、存在文件夹不匹配数据,2、存在货号数据缺失
+        goods_no_dict, error_folder_name_list = detail_func.merge_local_and_remote_data(
+            all_dir_info_data=all_dir_info_data, remote_data=remote_data
+        )
+        # 文件在系统中没有匹配的结果
+        if error_folder_name_list:
+            for one_folder in error_folder_name_list:
+                error_folder_list.append(
+                    {
+                        "folder_name": one_folder,
+                        "folder_path": "{}/{}".format(image_dir, one_folder),
+                    }
+                )
+                return_data["message"] += "文件夹:{} 找不到对应数据\n".format(
+                    one_folder
+                )
+
+        print("===============goods_no_dict==================")
+        if settings.IS_TEST:
+            _text = json.dumps(goods_no_dict)
+            print(goods_no_dict)
+            create_folder("qt_test")
+            with open("qt_test/goods_no_dict.txt", 'w', encoding='utf-8') as file:
+                file.write(_text)
+        print("===============goods_no_dict==================")
+
+        # ===================================是否齐色检查=============================================
+        # 如为红蜻蜓企业则还需要检查同款下是否齐全;数据返回结果为款号列表
+        error_data_dict = {}
+        if is_check_color_is_all:
+            if not is_use_excel:
+                if settings.PROJECT == "红蜻蜓":
+                    error_data_dict = (
+                        self.data_mode_generate_detail.check_goods_is_not_deficiency(
+                            goods_no_dict
+                        )
+                    )
+            else:
+                error_data_dict = self.data_mode_generate_detail.check_goods_is_not_deficiency_form_excel(
+                    goods_no_dict, excel_path
+                )
+
+        if error_data_dict:
+            print("----error_data_dict----")
+            print(json.dumps(error_data_dict))
+
+            # 款号反向映射;因为部分key键格式为KUM9999999
+            _x = {}
+            for i, v in goods_no_dict.items():
+                _x[v["款号"]] = i
+
+            for goods_no, value in error_data_dict.items():
+                if goods_no in _x:
+                    goods_no = _x[goods_no]
+                if goods_no in goods_no_dict:
+                    # =====移动到错误文件夹======
+                    for _folder_name in [
+                        x["文件夹名称"] for x in goods_no_dict[goods_no]["货号资料"]
+                    ]:
+                        error_folder_list.append(
+                            {
+                                "folder_name": _folder_name,
+                                "folder_path": "{}\{}".format(
+                                    image_dir, _folder_name
+                                ),
+                            }
+                        )
+                        return_data["message"] += "文件夹:{};{}\n".format(
+                            _folder_name, value["message"]
+                        )
+                    # 从 正确 款下面进行数据剔除
+                    goods_no_dict.pop(goods_no)
+
+        print("-----------------1goods_no_dict---------------")
+        print(json.dumps(goods_no_dict, ensure_ascii=False))
+        print("-----------------1goods_no_dict---------------")
+
+        # 如果没有有效数据则进行退出
+        if not goods_no_dict:
+            return_data["message"] += "没有任何有效数据\n"
+            return return_data
+
+        # 校验无误的文件夹数据  goods_no_dict为最终有效数据
+        for goods_no, value in goods_no_dict.items():
+            return_data["data"]["succeed_folder_list"].extend(
+                [x["文件夹名称"] for x in goods_no_dict[goods_no]["货号资料"]]
+            )
+
+        # 如果是表格数据,则获取表格数据中需要生成的货号
+        # 数据结构
+        # {“款号1”:
+        # {“模板名称1”:data_dict1,
+        # “模板名称2”:data_dict2,
+        # }}
+
+        if is_use_excel:
+            excel_temp_goods_no_data = self.data_mode_generate_detail.get_basic_template_information(
+                _goods_no_dict=goods_no_dict, excel_path=excel_path)
+        else:
+            excel_temp_goods_no_data = {}
+
+        print("731===================excel_temp_goods_no_data,is_use_excel:{}".format(is_use_excel))
+        print(json.dumps(excel_temp_goods_no_data))
+
+        # ===========数据组装,统计每个款需要生成哪些模板==============
+        # 不适用表格指定模板
+        goods_no_need_temps = {}
+        # isUseTemplate 为false 表示使用excel表格数据
+        if not is_use_excel:
+            for i in goods_no_dict:
+                goods_no_need_temps[i] = [temp_name]
+        else:
+            for i in goods_no_dict:
+                # i 为款号
+                if i in excel_temp_goods_no_data:
+                    # 获取 某个款号的所有允许生成的模板列表
+                    goods_no_need_temps[i] = list(excel_temp_goods_no_data[i].keys())
+
+        # ========开始执行详情图生成===================
+        # _goods_no_dict = goods_no_dict
+        _goods_no_dict = {}
+
+        # assigned_page_dict 如存在对应模板的,则不管是否有过滤都需要生成
+        # 检查是否已存在模板,已存在的需要进行跳过;可能部分模板已存在,部分不存在。
+        finally_goods_no_need_temps = {}
+        for goods_no, value in goods_no_dict.items():
+            if goods_no not in goods_no_need_temps:
+                continue
+            for __temp_name in goods_no_need_temps[goods_no]:
+                _path = "{}/{}/{}/{}".format(image_dir, "软件-详情图生成", __temp_name, goods_no)
+                if not os.path.exists(_path):
+                    print("款号详情图不存在", _path)
+                    if goods_no not in finally_goods_no_need_temps:
+                        finally_goods_no_need_temps[goods_no] = []
+                        _goods_no_dict[goods_no] = value  # 需要生成的数据
+                    finally_goods_no_need_temps[goods_no].append(__temp_name)
+                else:
+                    print("款号详情图存在", _path)
+                    # 如果在指定模板中存在,则也需要生成
+                    if __temp_name in assigned_page_dict:
+                        print("指定模板需要更新", _path)
+                        if goods_no not in finally_goods_no_need_temps:
+                            finally_goods_no_need_temps[goods_no] = []
+                            _goods_no_dict[goods_no] = value  # 需要生成的数据
+                        finally_goods_no_need_temps[goods_no].append(__temp_name)
+                    else:
+                        if detail_is_pass:
+                            return_data["message"] += "\n款号:{},模板:{} 已存在".format(goods_no, __temp_name)
+                        else:
+                            if goods_no not in finally_goods_no_need_temps:
+                                finally_goods_no_need_temps[goods_no] = []
+                                _goods_no_dict[goods_no] = value  # 需要生成的数据
+                            finally_goods_no_need_temps[goods_no].append(__temp_name)
+
+            pass
+            # _path = "{}/{}".format(self.image_dir, "软件-详情图生成")
+            # if os.path.exists(_path):
+            #     _goods_no_dict = {}
+            #     # 数据返回为 已有的款数据,为款号列表
+            #     is_pass_goods_no = detail_func.get_all_detail_info(_path)
+            #     for goods_no, value in goods_no_dict.items():
+            #         if "软件" in goods_no:
+            #             continue
+            #
+            #         if value["模板名称"] in assigned_page_dict:
+            #             need_todo = True
+            #         else:
+            #             if goods_no in is_pass_goods_no:
+            #                 need_todo = False
+            #             else:
+            #                 need_todo = True
+            #         if need_todo:
+            #             _goods_no_dict[goods_no] = value
+
+        print("-----------------2goods_no_dict---------------")
+        print(json.dumps(_goods_no_dict, ensure_ascii=False))
+        print("-----------------2goods_no_dict---------------")
+        return_data["data"]["error_folder_list"] = error_folder_list
+
+        if len(_goods_no_dict) == 0:
+            return_data["message"] += "\n没有任何文件夹需要执行"
+
+        return_data["data"]["goods_no_dict"] = _goods_no_dict
+        return_data["data"]["excel_temp_goods_no_data"] = excel_temp_goods_no_data
+        return_data["data"]["finally_goods_no_need_temps"] = finally_goods_no_need_temps
+
+        return_data["code"] = 0
+        return return_data
+
+    def move_error_folders(self, one_path, target_folder, message=""):
+        if os.path.exists(one_path):
+            check_path(target_folder)
+            move_folders(path_list=[one_path], target_folder=target_folder)
+
+    def check_for_detail_first_call_back(self, data):
+        # 首次数据校验的信息返回
+        # self.show_message(text="22222222222222222222222")
+        # QMessageBox.critical(self, "警告", "1111111", QMessageBox.Ok)
+        code = data["code"]
+        config_data = data["data"]["config_data"]
+        target_error_folder = config_data["target_error_folder"]
+        print("635  check_for_detail_first_call_back")
+        print(data)
+        if code != 0:
+            # self.windows.show_message(data["message"])
+            # my_dialog = DialogShow(self.windows.windows, text=data["message"])
+            # ret = my_dialog.exec()
+            self.show_progress_detail(text=data["message"])
+            _dialog_dict = {"text": data["message"],
+                            "windows": self,
+                            }
+            self.show_dialog_sign.emit(_dialog_dict)
+            self.event.wait()
+
+            # self.windows.set_state(2)
+            config_data["sign_text"] = "已结束详情处理"
+            self.run_end_sign.emit(config_data)
+            return
+
+        do_next = False
+        if data["message"]:
+            button_1, button_2, button_3 = None, None, None
+            text = data["message"]
+            if code == 0:
+                if data["data"]:
+                    if data["data"]["error_folder_list"]:
+                        print("22----------error_folder_list------------")
+                        print(json.dumps(data["data"]["error_folder_list"]))
+                        button_2 = "移除错误文件"
+                    if data["data"]["goods_no_dict"]:
+                        if button_2:
+                            button_1 = "移除并继续"
+                            button_3 = "继续(忽略其他)"
+                        else:
+                            button_1 = "继续"
+                    if data["data"]["succeed_folder_list"]:
+                        text += "\n==================\n校验无误数据:{}个文件夹".format(
+                            len(data["data"]["succeed_folder_list"])
+                        )
+            self.show_progress_detail(text=data["message"])
+            if button_1 is None and button_2 is None and button_3 is None:
+                pass
+            elif button_1 == "继续" and button_2 is None and button_3 is None:
+                do_next = True
+            else:
+                print("runmain  642----------------")
+                # todo 弹框修改处理等
+                _dialog_dict = {"text": text,
+                                "button_1": button_1,
+                                "button_2": button_2,
+                                "button_3": button_3,
+                                "windows": self,
+                                }
+                self.show_dialog_sign.emit(_dialog_dict)
+                # 等待事件被设置
+                self.event.wait()
+                print("self.dialog_result", self.dialog_result)
+
+                # my_dialog = DialogShow(
+                #     self.windows, text=text, button_1=button_1, button_2=button_2, button_3=button_3
+                # )
+                print("my_dialog.flag_name", self.dialog_result)
+                if "移除" in self.dialog_result:
+                    for error_folder_data in data["data"]["error_folder_list"]:
+                        self.move_error_folders(
+                            one_path=error_folder_data["folder_path"],
+                            target_folder=target_error_folder,
+                        )
+                if "继续" in self.dialog_result:
+                    do_next = True
+
+        if data["data"]["goods_no_dict"] and not data["data"]["error_folder_list"]:
+            do_next = True
+
+        if do_next:
+            # self.set_state(state_value=1)
+            getAllData = data["data"]
+            base_temp_name = getAllData["temp_name"]
+            set_temp_name = getAllData.get("template_name", "")
+            kwargs = {
+                "config_data": config_data,
+                "_goods_no_dict": data["data"]["goods_no_dict"],
+                "temp_name": base_temp_name,
+                "temp_name_list": data["data"]["temp_name_list"],
+                "assigned_page_dict": data["data"]["assigned_page_dict"],
+                "excel_temp_goods_no_data": data["data"]["excel_temp_goods_no_data"],
+                # 表格数据可能存在多模板,数据结构为一个款号下的多个模板的数据列表
+                "finally_goods_no_need_temps": data["data"]["finally_goods_no_need_temps"],  # 每个款号需要生成的模板数据
+            }
+            # todo work
+            new_func = partial(self.detail_run_by_thread,
+                               config_data=kwargs["config_data"],
+                               _goods_no_dict=kwargs["_goods_no_dict"],
+                               temp_name=kwargs["temp_name"],
+                               temp_name_list=kwargs["temp_name_list"],
+                               assigned_page_dict=kwargs["assigned_page_dict"],
+                               excel_temp_goods_no_data=kwargs["excel_temp_goods_no_data"],
+                               finally_goods_no_need_temps=kwargs["finally_goods_no_need_temps"])
+            self._w_3 = WorkerOneThread(func=new_func, name="_w_3")
+            self._w_3.start()
+            # threading.Thread(target=self.detail_run_by_thread, kwargs=kwargs).start()
+        else:
+            config_data["sign_text"] = "已结束详情处理"
+            self.run_end_sign.emit(config_data)
+
+    def detail_run_by_thread(self, config_data, _goods_no_dict, temp_name, temp_name_list, assigned_page_dict,
+                             excel_temp_goods_no_data,
+                             finally_goods_no_need_temps):
+        """
+        excel_temp_goods_no_data: {},  # 表格数据可能存在多模板,数据结构为一个款号下的多个模板的数据列表
+        finally_goods_no_need_temps: {},  # 每个款号需要生成的模板数据
+        """
+
+        # 开始处理
+        self.n = 0
+        self.total_num = len(_goods_no_dict)
+        self.fail_num = 0
+        is_use_excel = config_data["is_use_excel"]
+        image_dir = config_data["image_dir"]
+
+        # 详情图生成结果文件夹
+        out_put_dir = "{}\软件-详情图生成".format(image_dir)
+        if settings.IS_TEST:
+            print("==============_goods_no_dict  打印=================")
+
+            print(json.dumps(_goods_no_dict))
+
+            print("==============_goods_no_dict  打印-end=================")
+
+        all_detail_path_list = []
+        for goods_no, temp_name_list in finally_goods_no_need_temps.items():
+            try:
+                for _temp_name in temp_name_list:
+                    # if _temp_name != "xiaosushuoxie-4":
+                    #     continue
+                    assigned_page_list = []
+                    if _temp_name in assigned_page_dict:
+                        assigned_page_list = assigned_page_dict[_temp_name]
+                    # 如果为使用表格,则获取表格中的数据作为款号的基础数据
+                    temp_info_data = copy.copy(_goods_no_dict[goods_no])
+                    if is_use_excel:
+                        # 将表格中的数据进行替换
+                        if goods_no in excel_temp_goods_no_data:
+                            if _temp_name in excel_temp_goods_no_data[goods_no]:
+                                # 将表格中的特定的模板的行,替换到goods_no的data中,因为不同的模板有数据特殊性
+                                for _key, _key_value in excel_temp_goods_no_data[goods_no][_temp_name].items():
+                                    if _key in temp_info_data:
+                                        temp_info_data[_key] = _key_value
+                    print("temp_info_data")
+                    print("goods_no:{},_temp_name:{}".format(goods_no, _temp_name))
+                    all_detail_path_list.append("{}/{}/".format(out_put_dir, _temp_name, goods_no))
+                    # continue
+                    self.detail_deal_one_data(goods_no=goods_no,
+                                              value=temp_info_data,
+                                              out_put_dir=out_put_dir,
+                                              temp_name=_temp_name,
+                                              assigned_page_list=assigned_page_list)
+            except BaseException as e:
+                self.show_progress_detail(
+                    "款:{}生成详情异常:{}".format(goods_no, e))
+                print(e)
+
+        # ==============完成处理==============
+        self.set_state(state_value=2)
+        if self.total_num:
+            if self.fail_num:
+                self.show_progress_detail("处理完成,-----处理失败数据:{}个款".format(self.fail_num))
+            else:
+                self.show_progress_detail("处理完成")
+        else:
+            self.show_progress_detail("没有任何数据")
+
+        config_data["sign_text"] = "已结束详情处理"
+        config_data["all_detail_path_list"] = all_detail_path_list
+        # 打开文件夹
+        os.startfile(out_put_dir)
+
+        self.run_end_sign.emit(config_data)
+
+    def detail_run_by_thread11111(self, config_data, _goods_no_dict, temp_name, temp_name_list, assigned_page_dict,
+                                  excel_temp_goods_no_data,
+                                  finally_goods_no_need_temps):
+        """
+        excel_temp_goods_no_data: {},  # 表格数据可能存在多模板,数据结构为一个款号下的多个模板的数据列表
+        finally_goods_no_need_temps: {},  # 每个款号需要生成的模板数据
+        """
+
+        # 开始处理
+        self.n = 0
+        self.total_num = len(_goods_no_dict)
+        self.fail_num = 0
+        is_use_excel = config_data["is_use_excel"]
+        image_dir = config_data["image_dir"]
+
+        # 详情图生成结果文件夹
+        out_put_dir = "{}\软件-详情图生成".format(image_dir)
+        if settings.IS_TEST:
+            print("==============_goods_no_dict  打印=================")
+
+            print(json.dumps(_goods_no_dict))
+
+            print("==============_goods_no_dict  打印-end=================")
+
+        if settings.IS_TEST:
+            max_workers = 1
+        else:
+            max_workers = 1
+
+        all_detail_path_list = []
+
+        with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
+            futures = []
+            for goods_no, temp_name_list in finally_goods_no_need_temps.items():
+                for _temp_name in temp_name_list:
+                    # if _temp_name != "xiaosushuoxie-4":
+                    #     continue
+
+                    assigned_page_list = []
+                    if _temp_name in assigned_page_dict:
+                        assigned_page_list = assigned_page_dict[_temp_name]
+                    # 如果为使用表格,则获取表格中的数据作为款号的基础数据
+                    temp_info_data = copy.copy(_goods_no_dict[goods_no])
+                    if is_use_excel:
+                        # 将表格中的数据进行替换
+                        if goods_no in excel_temp_goods_no_data:
+                            if _temp_name in excel_temp_goods_no_data[goods_no]:
+                                # 将表格中的特定的模板的行,替换到goods_no的data中,因为不同的模板有数据特殊性
+                                for _key, _key_value in excel_temp_goods_no_data[goods_no][_temp_name].items():
+                                    if _key in temp_info_data:
+                                        temp_info_data[_key] = _key_value
+
+                    print("temp_info_data")
+                    print("goods_no:{},_temp_name:{}".format(goods_no, _temp_name))
+                    all_detail_path_list.append("{}/{}/".format(out_put_dir, _temp_name, goods_no))
+                    # continue
+                    futures.append(executor.submit(
+                        self.detail_deal_one_data,
+                        goods_no=goods_no,
+                        value=temp_info_data,
+                        out_put_dir=out_put_dir,
+                        temp_name=_temp_name,
+                        assigned_page_list=assigned_page_list,
+                    ))
+
+            # for goods_no, value in _goods_no_dict.items():
+            #     _temp_name = temp_name
+            #     # 使用自定义的表格数据
+            #     if self.isUseTemplate is False:
+            #         if "模板名称" in value:
+            #             if value["模板名称"] in temp_name_list:
+            #                 _temp_name = value["模板名称"]
+            #     assigned_page_list = []
+            #     if _temp_name in assigned_page_dict:
+            #         assigned_page_list = assigned_page_dict[_temp_name]
+            #
+            #     futures.append(executor.submit(
+            #         self.deal_one_data,
+            #         goods_no=goods_no,
+            #         value=value,
+            #         out_put_dir=out_put_dir,
+            #         temp_name=_temp_name,
+            #         assigned_page_list=assigned_page_list,
+            #     ))
+
+            # 使用 wait 方法等待所有任务完成
+            done, not_done = concurrent.futures.wait(futures)
+
+            # 处理完成的任务
+            for future in done:
+                if settings.IS_TEST:
+                    result = future.result()
+
+        # ==============完成处理==============
+        self.set_state(state_value=2)
+        if self.total_num:
+            if self.fail_num:
+                self.show_progress_detail(
+                    "处理完成,-----------处理失败数据:{}个款".format(self.fail_num)
+                )
+            else:
+                self.show_progress_detail("处理完成")
+        else:
+            self.show_progress_detail("没有任何数据")
+
+        config_data["sign_text"] = "已结束详情处理"
+        config_data["all_detail_path_list"] = all_detail_path_list
+        self.run_end_sign.emit(config_data)
+
+    def show_progress_detail(self, text):
+        # self.show_progress_detail_sign.emit(text)
+        # self.windows.show_progress_detail(text)
+
+    def detail_deal_one_data(self, goods_no, value, out_put_dir, temp_name, assigned_page_list):
+        if self.windows.state == 99:
+            self.show_progress_detail("用户主动取消:{}".format(goods_no))
+            return
+
+        self.show_progress_detail("正在生成:{},模板:{}".format(goods_no, temp_name))
+        is_deal_success = False
+        print("=================deal_one_data=====================")
+        print("goods_no", goods_no)
+        print("模板:", temp_name)
+        print("value:", value)
+
+        if settings.IS_TEST:
+            d = self.windows.temp_class[temp_name](goods_no, value,
+                                                   out_put_dir=out_put_dir,
+                                                   windows=self.windows,
+                                                   assigned_page_list=assigned_page_list)
+            is_deal_success = True
+        else:
+            try:
+                # # 处理图片详情图生成
+                d = self.windows.temp_class[temp_name](goods_no, value,
+                                                       out_put_dir=out_put_dir,
+                                                       windows=self.windows,
+                                                       assigned_page_list=assigned_page_list)
+                is_deal_success = True
+            except BaseException as e:
+                self.show_progress_detail("{}处理失败".format(goods_no))
+                error_text = "{}".format(e)
+                if "Unable to allocate" in error_text:
+                    error_text = "电脑内存不足,生成失败"
+
+                self.show_progress_detail("失败原因:{}".format(error_text))
+                self.fail_num += 1
+
+        self.n += 1
+
+        if not is_deal_success:
+            goods_art_no_list = value["货号资料"]
+            self.show_progress_detail("处理失败")
+            self.show_progress_detail(
+                "相关货号:{}".format([x["货号"] for x in goods_art_no_list])
+            )
+            # 将相关的文件夹统一移动至错误文件夹
+            detail_func.move_folders(
+                path_list=[
+                    "{}/{}".format(self.windows.image_dir, x)
+                    for x in [x["文件夹名称"] for x in goods_art_no_list]
+                ],
+                target_folder=self.windows.target_error_folder,
+            )
+            pass
+        # 更新进度
+        print(self.n, self.total_num)
+        self.windows.progress_sign.emit(
+            {
+                "type": "详情图生成",
+                "progress_bar_value": int(self.n / self.total_num * 100),
+            }
+        )
+
+    def check_serializable(self, obj):  # 检查某个对象其中的属性哪些不能被序列化
+        for attr_name in dir(obj):
+            if not attr_name.startswith('__'):
+                try:
+                    attr_value = getattr(obj, attr_name)
+                    serialized = pickle.dumps(attr_value)
+                    print(f"属性 {attr_name} 是可序列化的。")
+                except (TypeError, pickle.PicklingError):
+                    print(f"属性 {attr_name} 不可序列化。")
+
+    def on_dialog_result(self, result):
+        """处理对话框结果"""
+        self.dialog_result = result
+        print("972  处理对话框结果:{}".format(result))
+        # self.quit()  # 结束事件循
+        self.event.set()

+ 460 - 0
python/service/upload_pic.py

@@ -0,0 +1,460 @@
+from import_qt_mode import *
+from module.view_control.generate_goods_no_detail_pic.data import DataModeUploadPic
+from module.view_control.MineQWidget import DialogShow, WorkerOneThread
+import os
+import threading
+import time
+import concurrent.futures
+import re
+from PIL import Image
+from io import BytesIO
+
+
+# 详情图上传
+class UploadPic(QObject):
+    signal_data = Signal(dict)
+    run_end_sign = Signal(dict)
+    show_progress_detail_sign = Signal(str)
+
+    def __init__(self, windows, to_deal_dir, config_data):
+        super().__init__()
+        self.windows = windows
+        self.data_mode_upload_pic = DataModeUploadPic()
+        self.goods_no_data = {}
+        # ------------------
+        # 0未选择文件,1已选文件未开始,2进行中
+        self.state = 0
+        self.state_change(self.state)
+        self.to_deal_dir = to_deal_dir
+        self.config_data = config_data
+
+    def check_path(self, _path):
+        if not os.path.exists(_path):
+            os.mkdir(_path)
+        return True
+
+    def run_by_thread(self):
+        self.run_by_thread_real()
+        self.config_data["sign_text"] = "结束"
+        self.run_end_sign.emit(self.config_data)
+
+    def run(self):
+        self._w_5 = WorkerOneThread(func=self.run_by_thread, name="_w_5")
+        self._w_5.start()
+
+    def run_by_thread_real(self, *args):
+        self.show_info("====开始处理====")
+        # 统计哪些需要处理
+        total_num = 0
+        flag = True
+        # group_folders 分组文件夹
+        for group_folders in os.listdir(self.to_deal_dir):
+            path = "{}\{}".format(self.to_deal_dir, group_folders)
+            if not os.path.isdir(path):
+                continue
+            for goods_no in os.listdir(path):
+                # 检测是不是货号或款号文件夹
+                goods_no_path = "{}\{}\{}".format(self.to_deal_dir, group_folders, goods_no)
+                if not os.path.isdir(goods_no_path):
+                    continue
+
+                total_num += 1
+                # 检查每个款号文件夹是不是合规
+                # 款的一级目录文件
+                files = os.listdir(goods_no_path)
+                if "details" not in files:
+                    continue
+
+                for f in files:
+                    f_path = "{}\{}\{}\{}".format(self.to_deal_dir, group_folders, goods_no, f)
+                    if os.path.isdir(f_path):
+                        # 处理货号文件夹
+                        if "details" != f and "货号素材" not in f and "main" not in f and "拼接图" not in f:
+                            # 是否货号文件夹判断
+                            if not self.is_goods_art_dir(f, goods_no):
+                                self.show_info("{}文件夹下  {} {} 疑似不是货号".format(group_folders, goods_no, f))
+                                flag = False
+        if not flag:
+            self.show_info("请解决以上问题后重试")
+            return
+
+        if total_num == 0:
+            self.show_info("没有任何款号文件夹 需要上传")
+            return
+
+        self.lock = threading.Lock()
+
+        error_data = []
+        n = 0
+        for group_folders in os.listdir(self.to_deal_dir):
+            path = "{}\{}".format(self.to_deal_dir, group_folders)
+            if not os.path.isdir(path):
+                continue
+
+            for goods_no in os.listdir(path):
+                goods_no_path = "{}\{}\{}".format(self.to_deal_dir, group_folders, goods_no)
+                if not os.path.isdir(goods_no_path):
+                    self.show_info("{} 不是文件夹".format(goods_no))
+                    continue
+                if goods_no in self.data_mode_upload_pic.is_deal_goods_no:
+                    continue
+                else:
+                    self.data_mode_upload_pic.is_deal_goods_no.append(goods_no)
+
+                # ================处理单个款号信息,图片上传处理============
+                self.show_info("开始处理  {}".format(goods_no))
+                try:
+                    if self.deal_goods_pic(goods_no, goods_no_path):
+                        # 移动文件到已完成
+                        # dest = "{}\{}\{}".format(self.is_deal_dir, group_folders, goods_no)
+                        # if os.path.exists(dest):
+                        #     self.show_info("{} 文件夹在目标目录已存在".format(goods_no))
+                        # else:
+                        #     shutil.move(goods_no_path, dest)
+                        self.show_info("{} 上传成功".format(goods_no))
+                    else:
+                        self.show_info("{} 处理失败".format(goods_no))
+                        error_data.append(goods_no)
+                        pass
+                except BaseException as e:
+                    self.show_info("上传异常:{},{}".format(goods_no, e))
+
+                # n += 1
+                # self.show_info({"type": "show_p",
+                #                 "data": int(n * 100 / total_num)})
+
+        self.show_info("========已结束========== ")
+        if error_data:
+            self.show_info("以下编号/款号处理失败")
+            for data in error_data:
+                self.show_info("{}".format(data))
+
+        # self.show_info({"type": "change_state",
+        #                 "data": 2})
+
+    def deal_goods_pic(self, goods_no, goods_no_path):
+        """
+        两种模式:货号模式、编号模式
+        :param goods_no:
+        :param goods_no_path:
+        :return:
+        """
+        print("==========开始上传系统=============")
+
+        s = time.time()
+
+        self.show_info("{} 开始上传 ".format(goods_no))
+
+        if "NUM" in goods_no.upper():
+            mode = "编号"
+        else:
+            mode = "货号"
+
+        # 校验所有编号是否存在,是否是同个款
+        _numbers = []
+        if mode == "编号":
+            _numbers.append(goods_no)
+
+        self.goods_no_data[goods_no] = {}
+        # 收集该款号下可能存在的货号或编号
+        files = os.listdir(goods_no_path)
+        if "details" not in files:
+            self.show_info("{} 缺少details 文件夹".format(goods_no, ))
+            return False
+        for f in files:
+            f_path = "{}\{}".format(goods_no_path, f)
+            if os.path.isdir(f_path):
+                if "details" != f and "货号素材" not in f and "main" not in f and "拼接图" not in f:
+                    if not self.is_goods_art_dir(f, goods_no):
+                        self.show_info("{} {} 疑似不是货号".format(goods_no, f))
+                        # return False
+                        continue
+                    else:
+                        _numbers.append(f)
+
+                if "货号素材" in f:
+                    if "_" not in f:
+                        self.show_info("{} 货号素材文件夹 格式错误".format(goods_no))
+                        return False
+
+        # 校验所有编号是否存在,是否是同个款
+        if mode == "编号":
+            _numbers = [x.replace("KNUM", "").replace("NUM", "") for x in _numbers]
+            _numbers = list(set(_numbers))
+            goods_number_data = self.data_mode_upload_pic.get_goods_art_no_info(numbers_list=_numbers)
+
+        else:
+            goods_number_data = self.data_mode_upload_pic.get_goods_art_no_info(goods_art_list=_numbers)
+
+        t = True
+        for num in _numbers:
+            if num not in goods_number_data:
+                t = False
+                if mode == "编号":
+                    self.show_info("{} 编号在系统中不存在".format(num))
+                else:
+                    self.show_info("{} 货号在系统中不存在".format(num))
+            else:
+                if self.config_data["upload_is_pass"]:
+                    if self.data_mode_upload_pic.check_is_uploaded(num):
+                        t = False
+                        self.show_info("{} 货号在系统已有详情页,已跳过".format(num))
+                        return True
+
+        if not t:
+            return False
+
+        _ = set([goods_number_data[x]["款号"] for x in goods_number_data])
+        if len(_) != 1:
+            self.show_info("{} 存在多个不同的款号".format(_))
+            return False
+
+        # 重新定义款号
+        goods_no = _.pop()
+
+        # 上传所有图片
+        self.pic_data = {}
+        ignore_text = ["main", "拼接图", "货号素材"]
+        _Type = ['.png', '.PNG', '.jpg', '.JPG', '.gif', '.GIF']
+        for dirpath, dirnames, filenames in os.walk(goods_no_path):
+            for file in filenames:
+                if os.path.splitext(file)[1] in _Type:
+                    # 获取当前路径的文件夹名称
+                    f_path = dirpath + '/' + file
+                    f = True
+                    for i in ignore_text:
+                        if i in f_path:
+                            f = False
+                            break
+                    if f:
+                        print(f_path)
+                        f_path = f_path.replace("\\", "/")
+                        self.pic_data[f_path] = {"url": ""}
+
+        with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
+            for key in self.pic_data:
+                if "货号素材" in key:
+                    # 货号素材图不上传
+                    is_resize = False
+                    continue
+                else:
+                    is_resize = True
+                executor.submit(self.to_upload_pic, file_path=key, is_resize=is_resize)
+
+        for key in self.pic_data:
+            if "货号素材" in key:
+                is_resize = False
+                continue
+
+            if not self.pic_data[key]["url"]:
+                self.show_info("{} {} 图片未上传完成".format(key, goods_no))
+                return
+
+        up_data = {"goods_art_no": [],
+                   "details": [],
+                   "goods_no": [],
+                   "goods_art_no_material": []
+                   }
+
+        details_index = 0
+        goods_no_index = 0
+        # 款号转换
+        # files 为当前款下的所有文件或文件夹
+        for f in files:
+            f_path = "{}\{}".format(goods_no_path, f)
+            f_path = f_path.replace("\\", "/")
+            # 如果为文件夹则
+            if os.path.isdir(f_path):
+                if "details" == f:
+                    # 处理详情图
+                    for pic in os.listdir(f_path):
+                        pic_path = "{}\{}".format(f_path, pic)
+                        pic_path = pic_path.replace("\\", "/")
+
+                        details_index += 1
+                        data = {
+                            "index": details_index,
+                            "name": goods_no,
+                            "number": goods_no,
+                            "status": "success",
+                            "type": "2",  # 2 为详情图
+                            "uid": "",
+                            "url": self.pic_data[pic_path]["url"],
+                        }
+                        up_data["details"].append(data)
+                    pass
+                elif "货号素材" in f:
+                    continue
+                    x = f.split("_")[0]
+                    if mode == "编号":
+                        x = f.replace("NUM", "")
+                        x = x.replace("_货号素材", "")
+                        goods_art_no = goods_number_data[x]["商品货号"]
+                    else:
+                        goods_art_no = x
+                    n = 0
+                    for pic in os.listdir(f_path):
+                        pic_path = "{}\{}".format(f_path, pic)
+                        pic_path = pic_path.replace("\\", "/")
+                        n += 1
+                        data = {
+                            "index": n,
+                            "name": goods_art_no,
+                            "number": goods_art_no,
+                            "status": "success",
+                            "type": "3",
+                            "uid": "",
+                            "url": self.pic_data[pic_path]["url"],
+                        }
+                        up_data["goods_art_no_material"].append(data)
+                else:
+                    if "拼接图" in f:
+                        continue
+                    if "main" in f:
+                        continue
+
+                    # 处理货号图
+                    if mode == "编号":
+                        x = f.replace("NUM", "")
+                        goods_art_no = goods_number_data[x]["商品货号"]
+                    else:
+                        goods_art_no = f
+
+                    for pic in os.listdir(f_path):
+                        pic_path = "{}\{}".format(f_path, pic)
+                        pic_path = pic_path.replace("\\", "/")
+                        data = {
+                            "index": 1,
+                            "name": goods_art_no,
+                            "number": goods_art_no,
+                            "status": "success",
+                            "type": "0",  # 0为货号图
+                            "uid": "",
+                            "url": self.pic_data[pic_path]["url"],
+                        }
+                        up_data["goods_art_no"].append(data)
+                        break
+            else:
+                # 处理款号图
+                goods_no_index += 1
+                data = {
+                    "index": goods_no_index,
+                    "name": goods_no,
+                    "number": goods_no,
+                    "status": "success",
+                    "type": "1",
+                    "uid": "",
+                    "url": self.pic_data[f_path]["url"],
+                }
+                up_data["goods_no"].append(data)
+                pass
+
+        # -----------上传数据-----------------
+        # print(up_data["goods_no"])
+        # print(up_data["goods_art_no_material"])
+        # print(up_data["details"])
+
+        # 上传图片
+        for key, value in up_data.items():
+            if value:
+                if not self.data_mode_upload_pic.upload_pic_list_data(data={"images": value}):
+                    self.show_info("{} {}上传错误".format(goods_no, key))
+                    return False
+
+        print("---{}  {} 完成".format(goods_no, time.time() - s))
+        return True
+
+    def to_upload_pic(self, file_path, is_resize=True):
+        file_name = os.path.split(file_path)[1]
+        e = os.path.splitext(file_name)[1][1:]
+
+        im = Picture(file_path)
+        if im.x > 1200:
+            im.resize(width=1200)
+
+        # print(file_name, e)
+        # if e == "png":
+        #     im.im.show()
+        # return
+        _ = {"jpg": "JPEG",
+             "JPG": "JPEG",
+             "JPEG": "JPEG",
+             "jpeg": "JPEG",
+             "png": "PNG",
+             "PNG": "PNG", }
+        e = _[e]
+        image_io = im.save_to_io(e)
+
+        # if is_resize:
+        #     im = Picture(file_path)
+        #     if im.x > 1200:
+        #         im.resize(width=1200)
+        #     image_io = im.save_to_io()
+        #     e = "JPEG"
+        # else:
+        #     file_name = os.path.split(file_path)[1]
+        #     e = os.path.splitext(file_name)[0][1:]
+        #     image_io = open(file_path, 'rb')
+
+        # image_io = open(file_path, 'rb')
+
+        # self.lock.acquire()
+        # self.pic_data[file_path]["url"] = "1111"
+        # self.lock.release()
+
+        # return
+        goods_data = {"file_path": os.path.split(file_path)[1],
+                      "image_io": image_io,
+                      "e": e
+                      }
+
+        url = self.data_mode_upload_pic.upload_pic(goods_data=goods_data)
+        self.lock.acquire()
+        self.pic_data[file_path]["url"] = url
+        self.lock.release()
+
+    def is_goods_art_dir(self, file, goods_no):
+        if goods_no in file:
+            return True
+        # 判断是不是货号文件夹
+        _r_text = re.findall("[a-zA-Z0-9]+", file)
+        if _r_text:
+            # print(_r_text)
+            if _r_text[0] != file:
+                # 文件夹中包含有非字母或数字判断不是货号
+                return False
+            else:
+                return True
+        else:
+            # 非字母和数字 判断为非货号
+            return False
+
+    def show_info(self, text: str):
+        self.show_progress_detail_sign.emit(text)
+        # self.windows.show_progress_detail(text)
+
+    def show_text_browser(self, text):
+        pass
+
+    def state_change(self, to_state: int):
+        self.state = to_state
+
+
+class Picture:
+
+    def __init__(self, in_path):
+        self.im = Image.open(in_path)
+        self.x, self.y = self.im.size
+        # print(self.x, self.y)
+
+    def resize(self, width):
+        re_x = int(width)
+        re_y = int(self.y * re_x / self.x)
+        self.im = self.im.resize((re_x, re_y), Image.Resampling.LANCZOS)
+        self.x, self.y = self.im.size
+
+    def save_to_io(self, format):
+        img = BytesIO()
+        self.im.save(img, format=format)  # format: PNG or JPEG
+        img.seek(0)  # rewind to the start
+        return img

+ 118 - 0
python/settings.py

@@ -0,0 +1,118 @@
+from dotenv import load_dotenv, find_dotenv
+from pathlib import Path  # Python 3.6+ only
+import configparser
+def get_config_by_items(config_dict):
+    __config_dict = {}
+    for i, k in config_dict:
+        __config_dict[i] = k
+    return __config_dict
+
+
+def get_dict_value(_dict, key, default=None):
+    if key in _dict:
+        return _dict[key]
+    else:
+        return default
+MACHINE_LEVEL = "二档"
+IS_TEST = False
+IS_MCU = True
+IS_LIN_SHI_TEST = False
+PhotographSeconds = float(0.3)  # 拍照停留时间
+def moveSpeed(level: str = None):
+    config = {
+        "一档": {
+            "camera_high_motor": {
+                "max_speed": 10000,
+                "up_speed": 800,
+                "down_speed": 700,
+            },
+            "turntable_steering": {
+                "max_speed": 6000,
+                "up_speed": 500,
+                "down_speed": 400,
+            },
+        },
+        "二档": {
+            "camera_high_motor": {
+                "max_speed": 7000,
+                "up_speed": 600,
+                "down_speed": 500,
+            },
+            "turntable_steering": {
+                "max_speed": 4500,
+                "up_speed": 350,
+                "down_speed": 300,
+            },
+        },
+        "三档": {
+            "camera_high_motor": {
+                "max_speed": 3500,
+                "up_speed": 400,
+                "down_speed": 300,
+            },
+            "turntable_steering": {
+                "max_speed": 3000,
+                "up_speed": 200,
+                "down_speed": 200,
+            },
+        },
+    }
+
+    if level is None:
+        return config[MACHINE_LEVEL]
+    else:
+        return config[level]
+
+
+config = configparser.ConfigParser()
+config_name = "config.ini"
+config.read(config_name, encoding="utf-8")
+# 应用名称
+APP_NAME = config.get("app", "app_name")
+# 应用版本号
+APP_VERSION = config.get("app", "version")
+# 是否开启调试模式
+IS_DEBUG = config.get("app", "debug")
+# 应用端口号
+PORT = config.get("app", "port")
+# 应用线程数
+APP_WORKS = config.get("app", "works")
+# 应用host地址
+APP_HOST = config.get("app", "host")
+# 应用服务启动名称
+APP_RUN = config.get("app", "app_run")
+
+# 日志名称
+LOG_FILE_NAME = config.get("log", "log_file_name")
+# 最大字节数
+MAX_BYTES = config.get("log", "max_bytes")
+print("Max bytes is", MAX_BYTES)
+# 备份数量
+BACKUP_COUNTS = config.get("log", "backup_counts")
+# 远程服务器地址
+HLM_HOST = config.get("log", "hlm_host")
+
+PROJECT = config.get("log", "project")
+
+
+# ----------------------------------
+mcu_config_dict = config.items("mcu_config")
+_mcu_config_dict = {}
+for i, k in mcu_config_dict:
+    _mcu_config_dict[i] = int(k)
+# print(_mcu_config_dict)
+_config_mcu_config = get_config_by_items(config.items("mcu_config"))
+
+LEFT_FOOT_ACTION = _mcu_config_dict["left_foot_action"]
+LEFT_FOOT_PHOTOGRAPH = _mcu_config_dict["left_foot_photograph"]
+LEFT_FOOT_ACTION_1 = _mcu_config_dict["left_foot_action_1"]
+LEFT_FOOT_ACTION_2 = _mcu_config_dict["left_foot_action_2"]
+RIGHT_FOOT_ACTION = _mcu_config_dict["right_foot_action"]
+RIGHT_FOOT_PHOTOGRAPH = _mcu_config_dict["right_foot_photograph"]
+RIGHT_FOOT_ACTION_1 = _mcu_config_dict["right_foot_action_1"]
+RIGHT_FOOT_ACTION_2 = _mcu_config_dict["right_foot_action_2"]
+NEXT_STEP = int(get_dict_value(_config_mcu_config, "next_step", 6))  # 下一步
+
+MOVE_UP = _mcu_config_dict["move_up"]
+MOVE_DOWN = _mcu_config_dict["move_down"]
+STOP = _mcu_config_dict["stop"]

+ 25 - 0
python/setup.py

@@ -0,0 +1,25 @@
+from cx_Freeze import setup, Executable
+
+# 创建可执行文件的配置
+executableApp = Executable(
+    script="index.py",
+    target_name="pyapp",
+)
+
+# 打包的参数配置
+options = {
+    "build_exe": {
+        "build_exe": "./dist/",
+        "excludes": ["*.txt"],
+        "include_files": ["config.ini","action.json","sys_configs.json"],
+        "optimize": 2,
+    }
+}
+
+setup(
+    name="pyapp",
+    version="1.0",
+    description="python app",
+    options=options,
+    executables=[executableApp],
+)

+ 2 - 0
python/sockets/__init__.py

@@ -0,0 +1,2 @@
+from .socket_client import socket_manager, sendMsg
+from .connect_manager import ConnectionManager

+ 39 - 0
python/sockets/connect_manager.py

@@ -0,0 +1,39 @@
+from models import WebSocket
+from logger import logger
+import json
+from starlette.websockets import WebSocketState
+class ConnectionManager:
+    is_connected = False
+    def __init__(self):
+        self.active_connections: list[WebSocket] = []
+
+    def jsonMessage(self, code=0, msg="", data: object = None):
+        """json字符串数据"""
+        jsonData = {"code": code, "msg": msg, "data": data}
+        return jsonData
+
+    async def connect(self, websocket: WebSocket):
+        '''连接事件'''
+        await websocket.accept()
+        self.is_connected = True
+        self.active_connections.append(websocket)
+        logger.info("socket 已连接")
+
+    def disconnect(self, websocket: WebSocket):
+        '''断开连接事件'''
+        self.active_connections.remove(websocket)
+        logger.info("socket 连接断开")
+
+    async def send_personal_message(self, message: str, websocket: WebSocket):
+        '''向用户发送消息'''
+        print("ready message successfully")
+        await websocket.send_json(message)
+        print("sent message successfully")
+
+    async def broadcast(self, message: str):
+        """广播消息"""
+        for connection in self.active_connections:
+            if connection.client_state == WebSocketState.DISCONNECTED:
+                continue
+            print("connection", connection.client_state)
+            await connection.send_json(message)

+ 108 - 0
python/sockets/message_handler.py

@@ -0,0 +1,108 @@
+from .connect_manager import ConnectionManager
+from models import WebSocket
+import json, asyncio
+from mcu.DeviceControl import DeviceControl, checkMcuConnection
+from mcu.BlueToothMode import BlueToothMode
+from databases import DeviceConfig,SqlQuery,CRUD
+from mcu.capture.module_digicam import DigiCam
+
+# socket消息发送逻辑处理方法
+async def handlerSend(
+    manager: ConnectionManager, receiveData: str, websocket: WebSocket
+):
+    loop = asyncio.get_event_loop()
+    receiveData = json.loads(receiveData)
+    # 处理消息发送逻辑
+    receiveData = json.loads(receiveData.get("text"))
+    jsonType = receiveData.get("type")
+    code = receiveData.get("code")
+    msg = receiveData.get("msg")
+    data = receiveData.get("data")
+    print("receiveData", receiveData)
+    print("jsonType", jsonType)
+    match jsonType:
+        case "ping":
+            """发送心跳"""
+            data = manager.jsonMessage("pong")
+            await manager.send_personal_message(data, websocket)
+        case "pong":
+            """发送心跳"""
+            pass
+        case "forward_message":
+            data = receiveData.get("data")
+            dictMsg = {"code":code,"msg":msg,"data":data}
+            await manager.broadcast(dictMsg)
+        case "connect_mcu":
+            device_ctrl = DeviceControl(websocket_manager=manager)
+            # if device_ctrl.serial_ins.check_connect():
+            #     print("未连接")
+            loop.create_task(checkMcuConnection(device_ctrl), name="mcu")
+        case "connect_bluetooth":
+            blue_tooth = BlueToothMode(websocket_manager=manager)
+            # await  blue_tooth.main_func()
+            print("连接蓝牙信息")
+            loop.create_task(blue_tooth.main_func(), name="blue_tooth")
+            # loop.close()
+        case "init_mcu":
+            device_ctrl = DeviceControl(websocket_manager=manager)
+            loop.create_task(device_ctrl.initDevice(), name="init_mcu")
+        case "control_mcu":
+            device_name = data.get("device_name")
+            value = data.get("value")
+            if (device_name == "" or device_name == None) or (
+                value == "" or value == None
+            ):
+                data = manager.jsonMessage(code=1,msg="参数错误")
+                await manager.send_personal_message(data, websocket)
+                return
+            device_ctrl = DeviceControl(websocket_manager=manager)
+            device_ctrl.controlDevice(device_name, value)
+        case "run_mcu":
+            try:
+                # 判断拍照软件是否初始化
+                digicam = DigiCam()
+                camera_is_connect = digicam.checkCameraConnect()
+                if camera_is_connect is not True:
+                    data = manager.jsonMessage(code=1, msg="相机未连接,请检查")
+                    await manager.send_personal_message(data, websocket)
+                    return
+                digicam.getCaptureFolderPath()
+            except:
+                data = manager.jsonMessage(code=1, msg="digicam未初始化,请检查")
+                await manager.send_personal_message(data, websocket)
+                return
+            action_info = data.get("action", "执行左脚程序")
+            goods_art_no = data.get("goods_art_no", None)
+            if goods_art_no == None or goods_art_no =="":
+                # 判断货号是否存在
+                data = manager.jsonMessage(code=1, msg="goods_art_no不能为空")
+                await manager.send_personal_message(data, websocket)
+                return
+            session = SqlQuery()
+            crud = CRUD(DeviceConfig)
+            condtions = {"mode_type": action_info, "action_status": True}
+            all_devices = crud.read_all(
+                session, conditions=condtions, order_by="action_index", ascending=True
+            )
+            if len(all_devices) == 0:
+                # 判断是否有可用配置
+                data = manager.jsonMessage(code=1, msg="当前没有可用配置")
+                await manager.send_personal_message(data, websocket)
+                return
+            action_list = [device.model_dump() for device in all_devices]
+            print("action_list", action_list)
+            device_ctrl = DeviceControl(websocket_manager=manager)
+            loop.create_task(
+                device_ctrl.run_mcu_config(action_list, goods_art_no, action_info),
+                name="run_mcu_config",
+            )
+        case "handler_take_picture":
+            blue_tooth = BlueToothMode(websocket_manager=manager)
+            loop.create_task(
+                blue_tooth.remote_control_v2.handlerTakePhoto(),
+                name="run_mcu_config",
+            )
+        case _:
+            data = manager.jsonMessage(code=1, msg="未知消息")
+            await manager.send_personal_message(data, websocket)
+            return

+ 44 - 0
python/sockets/socket_client.py

@@ -0,0 +1,44 @@
+# socket_manager.py
+from enum import Flag
+import socket, json, asyncio
+import websockets
+from settings import APP_HOST,PORT
+from middleware import UnicornException
+class SocketClient:
+
+    def __init__(self, uri="ws://127.0.0.1:7074"):
+        self.uri = uri
+        self.websocket = None
+
+    async def connect(self):
+        if self.websocket == None:
+            self.websocket = await websockets.connect(self.uri)
+            print(f"Local Socket Connected to {self.uri}")
+
+    async def send_message(self, code=0, msg="", data: object = None):
+        if self.websocket:
+            # 本地客户端发送消息,在软件异步处理过程需要有客户端转发消息到应用客户端
+            json_data = json.dumps(
+                {"code": code, "type": "forward_message", "msg": msg, "data": data}
+            )
+            await self.websocket.send(json_data)
+            print("Message sent:", json_data)
+        else:
+            print("WebSocket client is not connected")
+
+    async def close(self):
+        if self.websocket:
+            await self.websocket.close()
+            print("WebSocket connection closed")
+
+    def jsonMessage(self, code=0, msg="", data: object = None):
+        """json字符串数据"""
+        jsonData = {"code": code, "msg": msg, "data": data}
+        return json.dumps(jsonData)
+
+
+# 创建全局 SocketManager 实例
+socket_manager = SocketClient(f"ws://{APP_HOST}:{PORT}/ws")
+def sendMsg(code=0, msg="", data=None):
+    '''通用客户端发送消息机制'''
+    asyncio.run(socket_manager.send_message(code=code, msg=msg, data=data))

+ 75 - 0
python/sockets/socket_server.py

@@ -0,0 +1,75 @@
+import json
+import asyncio
+from models import *
+from .connect_manager import ConnectionManager
+from .message_handler import *
+from mcu.DeviceControl import DeviceControl,checkMcuConnection
+from mcu.BlueToothMode import BlueToothMode
+import time
+from .socket_client import socket_manager
+conn_manager = ConnectionManager()
+active_connections = set()
+device_ctrl = DeviceControl(websocket_manager=conn_manager)
+blue_tooth = BlueToothMode(websocket_manager=conn_manager)
+from utils.common import message_queue
+@app.websocket("/ws")
+async def websocket_endpoint(websocket: WebSocket):
+    await conn_manager.connect(websocket)
+    active_connections.add(websocket)
+
+    try:
+        # await socket_manager.connect()
+        async def handler_messages():
+            while True:
+                try:
+                    byteDats = await websocket.receive()
+                    socket_type = byteDats.get("type")
+                    if socket_type == "websocket.disconnect":
+                        device_ctrl.close_connect()
+                        diviceList = blue_tooth.devices
+                        if len(diviceList) == 0:
+                            break
+                        diviceAddress = list(diviceList.keys())[0]
+                        if diviceAddress != "":
+                            print(diviceList.get(diviceAddress))
+                            diviceName = diviceList[diviceAddress]["name"]
+                            blue_tooth.disconnect_device(diviceAddress, diviceName)
+                        break
+                    print("byteDats", byteDats)
+                    await handlerSend(conn_manager, json.dumps(byteDats), websocket)
+                except Exception as e:
+                    print(e)
+                    break
+        # async def checkConnMcu():
+        #     await checkMcuConnection(device_ctrl)
+        # async def connectBlueTooth():
+        #     await blue_tooth.main_func()
+        async def send_message():
+            while True:
+                try:
+                    message = await message_queue.get()
+                    await websocket.send_json(message)
+                except Exception as e:
+                    print(e)
+                    break
+
+        await asyncio.gather(handler_messages(), send_message())
+    except WebSocketDisconnect:
+        # socket_manager.close()
+        print("Client disconnected")
+    finally:
+        active_connections.discard(websocket)
+        # if websocket:
+        #     await websocket.close()
+
+
+@app.on_event("shutdown")
+async def shutdown_event():
+    print("Shutting down...")
+    # socket_manager.close()
+    # 清理操作
+    for connection in list(active_connections):
+        try:
+            await connection.close()
+        except Exception as e:
+            print(f"Error closing connection: {e}")

+ 14 - 0
python/sys_configs.json

@@ -0,0 +1,14 @@
+[
+    {
+        "key": "basic_configs",
+        "value": "{\"main_image_size\":1600,\"image_out_format\":\"png\",\"image_sharpening\":1.0}"
+    },
+    {
+        "key": "take_photo_configs",
+        "value": "{\"repart_take_photo_warning\":false,\"single_photo_warning\":11,\"total_photo_warning\":1.0,\"camera_delay\":0.5}"
+    },
+    {
+        "key": "other_configs",
+        "value": "{\"product_type\":\"鞋类\",\"cutout_mode\":\"普通抠图\",\"device_speed\":\"二档\",\"running_mode\":\"普通模式\"}"
+    }
+]

+ 13 - 0
python/temp.py

@@ -0,0 +1,13 @@
+from databases import CRUD, SqlQuery, PhotoRecord, insert_photo_records
+from datetime import datetime
+
+session = SqlQuery()
+crud = CRUD(PhotoRecord)
+record = crud.read(session=session, order_by="id", ascending=False)
+if record == None:
+    # 发送失败消息
+    pass
+else:
+    insert_photo_records(
+        record.image_deal_mode, record.goods_art_no, record.image_index+1
+    )

+ 12 - 0
python/utils/SingletonType.py

@@ -0,0 +1,12 @@
+import threading
+class SingletonType(type):
+    _instance_lock = threading.Lock()
+
+    def __call__(cls, *args, **kwargs):
+        if not hasattr(cls, "_instance"):
+            with SingletonType._instance_lock:
+                if not hasattr(cls, "_instance"):
+                    cls._instance = super(SingletonType, cls).__call__(*args, **kwargs)
+        return cls._instance
+
+

+ 4 - 0
python/utils/common.py

@@ -0,0 +1,4 @@
+
+import asyncio
+
+message_queue = asyncio.Queue()

+ 22 - 0
python/utils/hlm_http_request.py

@@ -0,0 +1,22 @@
+import requests
+from middleware import UnicornException
+def forward_request(target_url, params=None, method="GET", headers=None):
+    """
+    转发HTTP请求到目标URL
+
+    :param target_url: 目标接口的URL
+    :param params: 请求参数(字典格式)
+    :param method: 请求方法(GET或POST)
+    :param headers: 请求头(字典格式)
+    :return: 目标接口的响应
+    """
+    try:
+        if method.upper() == "GET":
+            response = requests.get(target_url, params=params, headers=headers)
+        elif method.upper() == "POST":
+            response = requests.post(target_url, data=params, headers=headers)
+        else:
+            raise UnicornException("仅支持GET和POST方法")
+        return response.json()  # 假设目标接口返回JSON格式的数据
+    except requests.RequestException as e:
+        raise UnicornException(e)

+ 139 - 0
python/utils/utils_func.py

@@ -0,0 +1,139 @@
+import os
+from hashlib import sha256, md5
+from datetime import datetime
+import requests
+from natsort import ns, natsorted
+
+def get_md5(file_path):
+    data_md5 = None
+    if os.path.isfile(file_path):
+        f = open(file_path, 'rb')
+        md5_obj = md5()
+        md5_obj.update(f.read())
+        hash_code = md5_obj.hexdigest()
+        f.close()
+        data_md5 = str(hash_code).lower()
+    return data_md5
+
+def get_modified_time(file_path):
+    # 获取文件最后修改的时间戳
+    timestamp = os.path.getmtime(file_path)
+    # 将时间戳转换为datetime对象,并格式化为指定格式的字符串
+    modified_time = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:%M:%S')
+    return modified_time
+
+def compare_two_times(time_str1, time_str2):
+    # 定义时间格式
+    time_format = "%Y-%m-%d %H:%M:%S"
+    # 将时间字符串解析为datetime对象
+    time1 = datetime.strptime(time_str1, time_format)
+    time2 = datetime.strptime(time_str2, time_format)
+
+    # 比较两个时间
+    if time1 > time2:
+        # print(f"{time_str1} 比 {time_str2} 新。")
+        return "left_new"
+    elif time1 < time2:
+        # print(f"{time_str2} 比 {time_str1} 新。")
+        return "right_new"
+    else:
+        # print("两个时间相等。")
+        return "is_same"
+
+
+async def download_file(url, file_path):
+    try:
+        root_path, file_name = os.path.split(file_path)
+        check_path(root_path)
+        response = requests.get(url)
+        _content = response.content
+        with open(file_path, 'wb') as f:
+            f.write(_content)
+        print("下载成功:{}".format(file_path))
+    except:
+        print("下载失败:{}".format(file_path))
+
+def check_path(_path):
+    if not os.path.exists(_path):
+        # 创建多级目录
+        os.makedirs(_path, exist_ok=True)
+        # os.mkdir(_path)
+    return True
+
+
+# from module.view_control.MineQWidget import MineQWidget
+def list_dir(path):
+    listdir = os.listdir(path)
+    return natsorted(listdir, alg=ns.PATH)
+
+
+def get_folder(path):
+    folder_list = []
+    for _file in list_dir(path):
+        file_path = "{}/{}".format(path, _file)
+        if os.path.isdir(file_path):
+            folder_list.append(
+                {
+                    "folder_path": file_path,
+                    "folder_name": _file,
+                    "root_path": path,
+                    "label": "待处理",  # 是否需要继续处理
+                }
+            )
+    return folder_list
+
+
+def get_images(path):
+    _Type = [".png", ".PNG", ".jpg", ".JPG", ".gif", ".GIF", ".jpge", ".JPGE", ".CR2"]
+    image_list = []  # 过滤非图片数据
+    for _file in list_dir(path):
+        file_name, e = os.path.splitext(_file)
+        file_path = "{}/{}".format(path, _file)
+        if os.path.isdir(file_path):
+            continue
+        if e in _Type and "mask" not in file_name:
+            image_list.append(
+                {
+                    "file_path": file_path,
+                    "file_name": file_name,
+                    "file": _file,
+                    "root_path": path,
+                    "e": e,
+                }
+            )
+    return image_list
+
+
+def httpGetHandler(url, params=None, headers=None):
+    """
+        发送GET请求
+        :param endpoint: 请求的端点
+        :param params: 查询参数
+        :param headers: 请求头
+        :return: 响应对象
+        """
+    try:
+        response = requests.get(url, params=params, headers=headers)
+        response.raise_for_status()  # 如果响应状态码不是200,抛出异常
+        return response
+    except requests.exceptions.RequestException as e:
+        print(f"GET请求失败: {e}")
+        return None
+
+
+def httpPosthandler(url, data=None, json=None, headers=None):
+    """
+        发送POST请求
+        :param endpoint: 请求的端点
+        :param data: 表单数据
+        :param json: JSON数据
+        :param headers: 请求头
+        :return: 响应对象
+        """
+    try:
+        response = requests.post(url, data=data, json=json, headers=headers)
+        response.raise_for_status()  # 如果响应状态码不是200,抛出异常
+        return response
+    except requests.exceptions.RequestException as e:
+        print(f"POST请求失败: {e}")
+        return None