FactoryTool-2.1源码+编译后的工具+k8产测脚本
authorsnickersun <snickersun@163.com>
Thu, 21 May 2026 06:54:42 +0000 (14:54 +0800)
committersnickersun <snickersun@163.com>
Thu, 21 May 2026 06:54:42 +0000 (14:54 +0800)
26 files changed:
产测工具/FactoryTool-2.1.zip [new file with mode: 0644]
产测工具/FactoryTool-2.1/.gitignore [new file with mode: 0644]
产测工具/FactoryTool-2.1/.gitmodules [new file with mode: 0644]
产测工具/FactoryTool-2.1/LICENSE [new file with mode: 0644]
产测工具/FactoryTool-2.1/README.md [new file with mode: 0644]
产测工具/FactoryTool-2.1/Test-Result.xlsx [new file with mode: 0644]
产测工具/FactoryTool-2.1/build.bat [new file with mode: 0644]
产测工具/FactoryTool-2.1/build.py [new file with mode: 0644]
产测工具/FactoryTool-2.1/cloud_upload.py [new file with mode: 0644]
产测工具/FactoryTool-2.1/config.ini [new file with mode: 0644]
产测工具/FactoryTool-2.1/factorytool.exe [new file with mode: 0644]
产测工具/FactoryTool-2.1/file_handler.py [new file with mode: 0644]
产测工具/FactoryTool-2.1/k8_module_test.py [new file with mode: 0644]
产测工具/FactoryTool-2.1/k8_sort_setting.json [new file with mode: 0644]
产测工具/FactoryTool-2.1/locale/en/LC_MESSAGES/English.mo [new file with mode: 0644]
产测工具/FactoryTool-2.1/locale/en/LC_MESSAGES/English.po [new file with mode: 0644]
产测工具/FactoryTool-2.1/locale/zh_CN/LC_MESSAGES/Chinese.mo [new file with mode: 0644]
产测工具/FactoryTool-2.1/locale/zh_CN/LC_MESSAGES/Chinese.po [new file with mode: 0644]
产测工具/FactoryTool-2.1/log_redirect.py [new file with mode: 0644]
产测工具/FactoryTool-2.1/main.py [new file with mode: 0644]
产测工具/FactoryTool-2.1/media/quectel.ico [new file with mode: 0644]
产测工具/FactoryTool-2.1/module_test.py [new file with mode: 0644]
产测工具/FactoryTool-2.1/requirements.txt [new file with mode: 0644]
产测工具/FactoryTool-2.1/serial_handler.py [new file with mode: 0644]
产测工具/FactoryTool-2.1/serial_list.py [new file with mode: 0644]
产测工具/FactoryTool-2.1/sort_setting.json [new file with mode: 0644]

diff --git a/产测工具/FactoryTool-2.1.zip b/产测工具/FactoryTool-2.1.zip
new file mode 100644 (file)
index 0000000..db0436e
Binary files /dev/null and "b/\344\272\247\346\265\213\345\267\245\345\205\267/FactoryTool-2.1.zip" differ
diff --git a/产测工具/FactoryTool-2.1/.gitignore b/产测工具/FactoryTool-2.1/.gitignore
new file mode 100644 (file)
index 0000000..0329cbb
--- /dev/null
@@ -0,0 +1,131 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+pip-wheel-metadata/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+#  Usually these files are written by a python script from a template
+#  before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+
+# Translations
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+#   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
+#   However, in case of collaboration, if having platform-specific dependencies or dependencies
+#   having no cross-platform support, pipenv may install dependencies that don't work, or not
+#   install all needed dependencies.
+#Pipfile.lock
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
+.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# log
+log/
\ No newline at end of file
diff --git a/产测工具/FactoryTool-2.1/.gitmodules b/产测工具/FactoryTool-2.1/.gitmodules
new file mode 100644 (file)
index 0000000..3a95fa1
--- /dev/null
@@ -0,0 +1,3 @@
+[submodule "QuecPython_Ymodem"]
+       path = QuecPython_Ymodem
+       url = git@github.com:supremeys/QuecPython_Ymodem.git
diff --git a/产测工具/FactoryTool-2.1/LICENSE b/产测工具/FactoryTool-2.1/LICENSE
new file mode 100644 (file)
index 0000000..34f8805
--- /dev/null
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 瓜袁
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/产测工具/FactoryTool-2.1/README.md b/产测工具/FactoryTool-2.1/README.md
new file mode 100644 (file)
index 0000000..2759bd8
--- /dev/null
@@ -0,0 +1,190 @@
+# QuecPython产测工具使用指导手册
+
+## 简介
+
+QuecPython产测工具是专用于QuecPython项目对模块功能测试的工具,方便对出厂前已经烧录正式固件的模块进行相关的功能测试,本文档主要介绍如何使用该工具,在电脑使用Windows7及以上系统都可使用。
+
+适用模块:
+
+所有使用QuecPython开发的模块均可使用
+
+## 功能介绍
+1. 支持4台设备同步测试
+2. 支持自动测试和人工断点测试
+3. 可自定义测试脚本,测试方式(自动测试和人工测试)
+
+## 工具获取
+
+[FactoryTool-Tags](https://github.com/QuecPython/FactoryTool/tags)
+
+github上进入FactoryTool项目最新的tag,可以下载可执行包( FactoryTool.zip ) 和 源代码( Source code(zip/tar.gz) )
+
+## 页面介绍
+
+**菜单栏** 包括编辑菜单和日志菜单
+
+**编辑菜单** 用于编辑测试脚本和Excel日志文件
+
+**日志菜单** 用于查看测试日志文件和工具运行日志文件    
+
+**加载栏** 包括 加载测试文件和配置文件,其中测试文件为要测试的测试文件,配置文件为需要测试function,测试的顺序,测试项的名称,测试的方式(人工测试/自动测试)
+
+**测试区域** 包括 端口、测试项名称、测试的方式、测试结果,工具有四个测试栏位最多可一次性同时测试四个设备,也可以单独测试某个设备。
+
+> **注意:测试的模块需要保证交互口不处于堵塞状态,否则测试将失败**
+
+![1692598668142](./media/1692598668142.jpg)
+
+## 测试脚本
+
+测试脚本编写时需要注意不得改动原有模板代码结构,建议编写完之后先在模块中手动运行测试,确认运行结果是否满足预期,测试脚本模板中需要改动的位置有两个
+
+**导入模块**:导入测试中用到的python库
+
+**测试函数**:自行编写测试函数,函数名可以自定义,但是需要是静态方法且无法传参,函数体内容根据测试需求自定义,函数需要有返回值,必须是bool值
+
+测试脚本示例:
+
+```python
+# Write the modules that need to be imported here
+import sim
+import net
+import uos
+import utime
+
+class TestBase(object):
+    # ------This area is for testing code------
+    @staticmethod
+    def det_signal():
+        utime.sleep(2)
+        if sim.getStatus() == 1:
+            if net.getConfig()[0] == 5:
+                return True
+            else:
+                return False
+        else:
+            return False
+
+    @staticmethod
+    def det_file_space():
+        utime.sleep(2)
+        if uos.statvfs('usr')[3] > 5:
+            return True
+        else:
+            return False
+    # ------This area is for testing code------
+```
+
+## 配置文件
+配置文件为json文件,需要配置三维数据,分别为: 测项名称, 测项的方法名称, 测试方式(1为人工测试 0为自动测试)
+
+
+最后一列        含义
+0           自动开始 + 自动判结果
+1           OK 确认开始 + 人工判结果
+2           OK 确认开始 + 自动判结果(20s 超时)
+
+测试脚本示例:
+
+```json
+//             测项名称     function      测试方式: 1为人工测试 0为自动测试
+{
+    "info": [["Memory test", "det_file_space", 0, 1],
+             ["Signal test", "det_signal", 0 , 0 ]]
+}
+```
+
+
+## 测试原理
+
+产测工具利用python脚本的形式来测试QuecPython产品的业务功能,通过CDC口来执行测试脚本,测试脚本根据测试需求可以自行调整以达成完成生产测试的功能
+
+1. 通过QuecPython的API接口来获取模组参数和运行状态
+2. 通过访问正在运行的业务代码中的对象来获取业务运行情况
+3. 通过调用业务代码提供的接口才测试业务功能或者硬件功能
+4. 通过QuecPython API接口传输配置文件或者写入产品参数
+
+## 测试步骤
+
+**步骤一**:编辑测试文件和配置文件
+
+编辑测试**module_test.py**代码 和 配置文件**sort_setting.json**,测试代码和配置文件案例如上图所示
+
+**步骤二**:打开工具,选择测试文件和配置文件
+
+打开工具后,点击 **选择** 按钮,分别选择上一步编辑好的测试代码和配置文件,若有检测到串口,则该串口下会显示每一项测试名称以及测试方式
+
+![1692599729648](./media/1692599729648.jpg)
+
+**步骤三**:开始测试
+
+点击 **全部开始** 按钮: 开始测试所有已接入的模块
+
+点击 **开始** 按钮: 开始测试对应的那一个模块
+
+若是人工检测会弹窗提示是否成功,可人工点击 **是/否** 来确认该项结果
+![1692599919725](./media/1692599919725.jpg)
+
+**步骤四**:查看测试结果
+
+可在测试结果栏位直接看到测试结果:
+1. 通过: 绿色背景色,测试结果栏位标记: 通过
+2. 不通过: 红色背景色,测试结果栏位标记: 不通过 
+3. 测试中: 黄色背景色,表示该测项正在检测中
+
+![1692599801694](./media/1692599801694.jpg)
+
+**步骤五**:查看测试日志
+
+在工具同级目录下生成**Test-Result.xlsx**文件,包含测试项目内容以及测试结果日志
+
+## 测试结果
+
+每一个模块的的测试结果会写入到excel中导出,可以通过菜单栏的 **编辑** 中的 **编辑Excel文件**菜单打开该excel文件
+
+每一次测试会在excel中生成一条测试记录(无论测试成功或者失败),多次测试会有多条记录,根据测试设备的COM口区分
+
+该文件会保存在工具同级目录下,测试过程中自动追加
+
+
+## 如何拉取仓库
+
+若有二次开发需求,可直接拉取代码仓库
+
+```shell
+git clone --recurse-submodules https://github.com/QuecPython/FactoryTool.git -b interventionable
+```
+
+## 环境配置
+
+**安装依赖库**
+
+```shell
+pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
+# 如果上面方式安装失败,可以使用单独安装的形式
+pip install wxpython -i https://pypi.tuna.tsinghua.edu.cn/simple
+pip install pypubsub -i https://pypi.tuna.tsinghua.edu.cn/simple
+```
+**运行代码**
+
+```shell
+python main.py
+```
+**编译成可执行程序**
+
+```shell
+# 安装打包依赖(首次)
+pip install pyinstaller polib -i https://pypi.tuna.tsinghua.edu.cn/simple
+
+# 方式一:一键脚本(推荐,生成 factorytool.exe 到本目录)
+python build.py
+# 或双击 build.bat
+
+# 方式二:手动命令
+pyinstaller -F -w --clean --name factorytool --icon media/quectel.ico --distpath . --add-data "locale;locale" main.py
+
+# 方式三:使用 spec
+pyinstaller --clean --distpath . main.spec
+```
+
+打包完成后,可执行文件为 **`factorytool.exe`**(与 `config.ini`、测试脚本、json 配置放在同一目录运行)。
diff --git a/产测工具/FactoryTool-2.1/Test-Result.xlsx b/产测工具/FactoryTool-2.1/Test-Result.xlsx
new file mode 100644 (file)
index 0000000..1dc9d23
Binary files /dev/null and "b/\344\272\247\346\265\213\345\267\245\345\205\267/FactoryTool-2.1/Test-Result.xlsx" differ
diff --git a/产测工具/FactoryTool-2.1/build.bat b/产测工具/FactoryTool-2.1/build.bat
new file mode 100644 (file)
index 0000000..ba46b7a
--- /dev/null
@@ -0,0 +1,12 @@
+@echo off
+chcp 65001 >nul
+cd /d "%~dp0"
+echo ===== FactoryTool 打包 =====
+python build.py
+if errorlevel 1 (
+    echo 打包失败。
+    pause
+    exit /b 1
+)
+echo.
+pause
diff --git a/产测工具/FactoryTool-2.1/build.py b/产测工具/FactoryTool-2.1/build.py
new file mode 100644 (file)
index 0000000..22b1a9e
--- /dev/null
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+"""
+打包 FactoryTool-2.1 为单文件 factorytool.exe(输出到本目录)。
+用法: python build.py
+"""
+
+from __future__ import print_function
+
+import os
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parent
+EXE_NAME = "factorytool"
+MAIN_PY = ROOT / "main.py"
+ICON = ROOT / "media" / "quectel.ico"
+
+
+def ensure_pyinstaller():
+    try:
+        import PyInstaller  # noqa: F401
+    except ImportError:
+        print("正在安装 PyInstaller ...")
+        subprocess.check_call(
+            [sys.executable, "-m", "pip", "install", "pyinstaller", "-q"],
+            cwd=str(ROOT),
+        )
+
+
+def compile_po_files():
+    """将 locale 下 .po 编译为 .mo,供 gettext 在 exe 中使用。"""
+    po_files = list((ROOT / "locale").rglob("*.po"))
+    if not po_files:
+        return
+    try:
+        import polib
+    except ImportError:
+        print("正在安装 polib(编译语言包)...")
+        subprocess.check_call(
+            [sys.executable, "-m", "pip", "install", "polib", "-q"],
+            cwd=str(ROOT),
+        )
+        import polib
+
+    for po in po_files:
+        mo = po.with_suffix(".mo")
+        if mo.exists() and mo.stat().st_mtime >= po.stat().st_mtime:
+            continue
+        polib.pofile(str(po)).save_as_mofile(str(mo))
+        print("compiled:", mo.relative_to(ROOT))
+
+
+def run_pyinstaller():
+    sep = os.pathsep
+    add_data = "locale{sep}locale".format(sep=sep)
+
+    cmd = [
+        sys.executable,
+        "-m",
+        "PyInstaller",
+        "-F",
+        "-w",
+        "--clean",
+        "--noconfirm",
+        "--name",
+        EXE_NAME,
+        "--icon",
+        str(ICON),
+        "--distpath",
+        str(ROOT),
+        "--workpath",
+        str(ROOT / "build"),
+        "--specpath",
+        str(ROOT),
+        "--add-data",
+        add_data,
+        "--hidden-import",
+        "serial.tools.list_ports",
+        "--hidden-import",
+        "pubsub",
+        "--hidden-import",
+        "pubsub.core",
+        str(MAIN_PY),
+    ]
+
+    print("执行:", " ".join(cmd))
+    subprocess.check_call(cmd, cwd=str(ROOT))
+
+
+def locate_exe():
+    candidates = [
+        ROOT / (EXE_NAME + ".exe"),
+        ROOT / "dist" / (EXE_NAME + ".exe"),
+    ]
+    for path in candidates:
+        if path.is_file():
+            return path
+    return None
+
+
+def main():
+    os.chdir(str(ROOT))
+
+    if not MAIN_PY.is_file():
+        print("错误: 找不到 main.py")
+        sys.exit(1)
+    if not ICON.is_file():
+        print("错误: 找不到图标 media/quectel.ico")
+        sys.exit(1)
+
+    ensure_pyinstaller()
+    compile_po_files()
+    run_pyinstaller()
+
+    built = locate_exe()
+    if not built:
+        print("错误: 未生成 {0}.exe".format(EXE_NAME))
+        sys.exit(1)
+
+    target = ROOT / (EXE_NAME + ".exe")
+    if built.resolve() != target.resolve():
+        shutil.copy2(str(built), str(target))
+
+    dist_dir = ROOT / "dist"
+    if dist_dir.is_dir() and not any(dist_dir.iterdir()):
+        dist_dir.rmdir()
+
+    print("")
+    print("打包完成:", target)
+    print("运行: 双击 factorytool.exe 或在本目录放置 config.ini / 测试脚本 / json 配置")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/产测工具/FactoryTool-2.1/cloud_upload.py b/产测工具/FactoryTool-2.1/cloud_upload.py
new file mode 100644 (file)
index 0000000..08be496
--- /dev/null
@@ -0,0 +1,10 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+"""
+@Project:Factory_test 
+@File:cloud_upload.py
+@Author:rivern.yuan
+@Date:2022/9/7 10:06 
+"""
+
+# TODO 执行结果上传云端
diff --git a/产测工具/FactoryTool-2.1/config.ini b/产测工具/FactoryTool-2.1/config.ini
new file mode 100644 (file)
index 0000000..00a540c
--- /dev/null
@@ -0,0 +1,4 @@
+[software]\r
+py_file = D:\workspace\EG810\ASR1603_JSWM3\产测工具\FactoryTool-2.1\k8_module_test.py\r
+json_file = D:\workspace\EG810\ASR1603_JSWM3\产测工具\FactoryTool-2.1\k8_sort_setting.json\r
+\r
diff --git a/产测工具/FactoryTool-2.1/factorytool.exe b/产测工具/FactoryTool-2.1/factorytool.exe
new file mode 100644 (file)
index 0000000..c4ded81
Binary files /dev/null and "b/\344\272\247\346\265\213\345\267\245\345\205\267/FactoryTool-2.1/factorytool.exe" differ
diff --git a/产测工具/FactoryTool-2.1/file_handler.py b/产测工具/FactoryTool-2.1/file_handler.py
new file mode 100644 (file)
index 0000000..cec7c9d
--- /dev/null
@@ -0,0 +1,143 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+"""
+@Project:Factory_test 
+@File:file_handler.py
+@Author:rivern.yuan
+@Date:2022/9/7 10:09 
+"""
+import serial_handler
+import openpyxl
+import codecs
+import json
+import sys
+import re
+import os
+
+
+class ExcelHandler(object):
+    """
+    handle test result Excel
+    excel_file: filename include full path
+    """
+    def __init__(self, excel_file):
+        self.file = excel_file
+        if os.path.exists(self.file):
+            pass
+        else:
+            wb = openpyxl.Workbook()
+            wb.save(self.file)
+        self.wb = openpyxl.load_workbook(self.file)
+        self.ws = self.wb[self.wb.sheetnames[0]]
+
+
+    def get_rows_columns(self):
+        rows = self.ws.max_row
+        columns = self.ws.max_column
+        return rows, columns
+
+    def get_cell_value(self, row, column):
+        return self.ws.cell(row=row, column=column).value
+
+    def get_col_values(self, column):
+        rows = self.ws.max_row
+        columns_data = []
+        for i in range(1, rows + 1):
+            columns_data.append(self.ws.cell(row=i, column=column).value)
+            return columns_data
+
+    def get_row_values(self, row):
+        columns = self.ws.max_column
+        rows_data = []
+        for i in range(1, columns + 1):
+            rows_data.append(self.ws.cell(row=row, column=i).value)
+            return rows_data
+
+    def set_cell_value(self, row, column, cell_value):
+        try:
+            self.ws.cell(row=row, column=column).value = cell_value
+        except KeyError as e:
+            self.ws.cell(row=row, column=column).value = "Write Fail"
+            print(e)
+        finally:
+            self.wb.save(self.file)
+
+    def close(self):
+        self.wb.close()
+
+
+class JsonHandler(object):
+    """
+        json_file: filename include full path
+    """
+    def __init__(self, json_file):
+        self._json_name = json_file
+
+    def read_json(self):
+        with codecs.open(self._json_name, 'r', 'utf-8') as f:
+            data = json.load(f)
+        return data
+
+    @staticmethod
+    def write_json(data, json_name):
+        with codecs.open(json_name, 'w', 'utf-8') as f:
+            json.dump(data, f, ensure_ascii=False, indent=2)
+
+
+class ScriptHandler(object):
+    """
+    QuecPython test script
+    script_file: filename include full path
+    """
+    def __init__(self, script_file: str):
+        self.script_file = script_file
+        self.pattern_py = {re.compile(r'#.*'), re.compile(r'(""".*?""")*|(\'\'\'.*?\'\'\')*'), }
+
+    """compile py script"""
+    def script_check(self):
+        try:
+            if self.script_file[-3:] == ".py":
+                with open(self.script_file, "r", encoding='utf-8') as f:
+                    script_data = f.read()
+                compile(script_data, '', 'exec')
+                return True
+            else:
+                raise TypeError("File %s is not python file!" % self.script_file)
+        except:
+            compileInfo = sys.exc_info()
+            print("测试脚本语法错误" + str(compileInfo[0]) + ":" + str(compileInfo[1]))
+
+    """get effective py cmd"""
+    def script_parse(self):
+        content = []
+        with open(self.script_file, "r") as f:
+            for line in f.readlines():
+                result = re.sub(line, "", line.split("\n")[0])
+                if result != "":
+                    content.append(result)
+        return content
+
+
+if __name__ == '__main__':
+    excel_test = ExcelHandler("test\\Test-Result.xlsx")
+    print(excel_test.get_rows_columns())
+    # excel_test.set_cell_value(1, 1, "No.")
+    # excel_test.set_cell_value(1, 2, "SerialNum")
+    # print(excel_test.get_col_values(1))
+    # print(excel_test.get_row_values(1))
+    # print(excel_test.get_cell_value(1, 1))
+
+    result = ["com63", "972658346523","128934928734","success", " True ,False"]
+    rows, columns = excel_test.get_rows_columns()
+    excel_test.set_cell_value(rows + 1, 1, rows)
+    for i, value in enumerate(result):
+        excel_test.set_cell_value(rows + 1, i + 2, value)
+
+
+    excel_test.close()
+
+    # py_test = ScriptHandler("module\\module_test.py")
+    # if py_test.script_check():
+    #     print(py_test.script_parse())
+    # ser = serial_handler.SerialHandler("COM63")
+    # print(ser.run_cmd(py_test.script_parse()))
diff --git a/产测工具/FactoryTool-2.1/k8_module_test.py b/产测工具/FactoryTool-2.1/k8_module_test.py
new file mode 100644 (file)
index 0000000..e9c00c0
--- /dev/null
@@ -0,0 +1,179 @@
+import sim
+import net
+import uos
+import utime
+try :
+    import quecgnss
+except Exception as e:
+    print("quecgnss :{}".format(e)) 
+import wifiScan
+from misc import USB,Power
+import modem
+import ql_fs
+import ujson
+from machine import I2C
+import audio
+from machine import Pin 
+from machine import ExtInt
+
+usb = USB()
+
+
+def extint_cb(args):
+        pass
+class TestBase(object):
+
+    @staticmethod
+    def Wifi_test():
+        wifiScan.control(1)
+        Wifi_s = wifiScan.start()
+        if Wifi_s != -1:
+            wifiScan.control(0)
+            return True
+        else:
+            wifiScan.control(0)
+            return False  
+
+    @staticmethod
+    def usb_callback():
+        status = usb.getStatus()
+        if status != -1:
+            return True
+        else:
+            return False
+        
+    @staticmethod
+    def modem_imei():
+        imei_value = modem.getDevImei()
+        print("modem_imei:{}".format(imei_value))
+        if imei_value != 864430010001091:
+            return True
+        else:
+            if imei_value == 864430010001091:
+                return False
+    
+    @staticmethod
+    def fw_version():
+        try:
+            import version_util
+            version = version_util.get_unified_version()
+        except Exception as e:
+            print("version_util :{}".format(e)) 
+            return False
+        print("product_version:{}".format(version))
+        return True
+        
+    @staticmethod
+    def write_test():
+        if ql_fs.path_exists("/ext") == False:
+            ldev = uos.VfsLfs1(32, 32, 32, "ext_fs", 0, 3)
+            uos.mount(ldev, '/ext')
+        write_data = {"aaa": "aba"}
+        with open("/ext/tmp.json", "w", encoding="utf-8") as fd:
+            ujson.dump(write_data, fd)
+        read_data = []
+        with open("/ext/tmp.json", "r") as fd:
+            read_data = ujson.load(fd)
+        if len(write_data) == len(read_data):
+            return True
+        else:
+            return False
+    
+    @staticmethod
+    def sim_test():
+        if sim.getIccid() == -1:
+            return False
+        print("sim_test:{}".format(sim.getIccid()))
+        return True
+    
+    @staticmethod
+    def signal_test():
+        signal = net.csqQueryPoll()
+        print("signal_test:{}".format(signal))
+        if  signal > 20 and signal<=31:
+            return True
+        print("signal_test:测试失败! 信号不在(20,31]范围signal:{}".format(signal))
+        return False
+    
+    @staticmethod
+    def microphone_test():
+        # 16k 流式录放(与 vc_session 一致),人工听音
+        SR = 16000
+        aud = audio.Audio(0)
+        aud.setVolume(11)
+        aud.stopAll()
+        rec = audio.Record()
+        rec.gain_set(4, 10)
+        buf = bytearray(4096)
+        pcm = bytearray()
+        rec.stream_start(audio.Record.WAV, SR, 0)
+        print("microphone_test: speak...")
+        for _ in range(20):
+            n = rec.stream_read(buf, 4096)
+            if n > 0:
+                pcm.extend(buf[:n])
+            utime.sleep_ms(100)
+        rec.stream_stop()
+        rec.stop()
+        aud.stopAll()
+        utime.sleep_ms(50)
+        play = audio.Audio.PCM(0, 1, SR, 1, 1, 2)
+        play.stream_write_start()
+        off = 0
+        while off < len(pcm):
+            play.stream_write(pcm[off:off + 4096], min(4096, len(pcm) - off))
+            off += 4096
+        utime.sleep_ms(max(500, len(pcm) // 32))
+        play.stream_stop()
+        play.close()
+        print("microphone_test: done, bytes={}".format(len(pcm)))
+        return True
+    
+    @staticmethod
+    def power_key_test():
+        extint = ExtInt(ExtInt.GPIO26, ExtInt.IRQ_RISING_FALLING, ExtInt.PULL_DISABLE, extint_cb)
+        extint.disable()
+        key26 = Pin(Pin.GPIO26, Pin.IN, Pin.PULL_PU, 0)
+        i = 1000
+        key_v = key26.read()
+        print("power_key_test:",key_v)
+        while 1:
+            if key26.read() != key_v:
+                return True
+            utime.sleep_ms(20)
+            i-=1
+            if i <= 0:
+                break
+        return False 
+    
+    @staticmethod
+    def key1_test():
+        extint = ExtInt(ExtInt.GPIO22, ExtInt.IRQ_RISING_FALLING, ExtInt.PULL_DISABLE, extint_cb)
+        extint.disable()
+        key20 = Pin(Pin.GPIO22, Pin.IN, Pin.PULL_PU, 0)
+        i = 1000
+        key_v = key20.read()
+        print("key1_test1:",key_v)
+        while 1:
+            print("key1_test:",key20.read())
+            if key20.read() != key_v:
+                return True
+            utime.sleep_ms(20)
+            i-=1
+            if i <= 0:
+                break
+        return False 
+            
+    def motor_test():
+        motor_pin = Pin(Pin.GPIO20, Pin.OUT, Pin.PULL_PD, 0)
+        motor_pin.write(1)
+        utime.sleep_ms(1000)
+        motor_pin.write(0)
+        return True    
+    def det_changedir_end():
+        #uos.remove("/usr/test.py")
+        Power.powerRestart()
+        
+        return True
+
+
diff --git a/产测工具/FactoryTool-2.1/k8_sort_setting.json b/产测工具/FactoryTool-2.1/k8_sort_setting.json
new file mode 100644 (file)
index 0000000..c56dc73
--- /dev/null
@@ -0,0 +1,30 @@
+{
+    "info": [
+                ["固件版本", "fw_version()", 0,0],
+                ["充电测试", "usb_callback()", 0,0],
+                ["RF校准", "modem_imei()", 0,0],
+                ["flash读写", "write_test()", 0,0],
+                ["SIM卡测试","sim_test()",0,0],
+                ["4G信号测试","signal_test()",0,0],
+                ["电源键测试","power_key_test()",0,2],
+                ["按键1测试","key1_test()",0,2],
+                ["麦克风喇叭测试","microphone_test()",0,1],
+                ["震动马达测试","motor_test()",0,1],
+                ["测试结束","det_changedir_end()",0,2]
+                
+            ],
+    "tips": {
+                "固件版本":"正在测试固件版本...",
+                "充电测试":"正在测试充电...",
+                "RF校准":"正在确认RF参数是否已写入...",
+                "flash读写":"正在外部flash读写测试...",
+                "SIM卡测试":"需插卡测试!!!正在读取Iccid...",
+                "4G信号测试":"需插卡测试!!!正在读取信号强度...",
+                "电源键测试":"点击确认后,请在20秒内按下电源按键",
+                "按键1测试":"点击确认后,请在20秒内按下按键1",
+                "麦克风喇叭测试":"点击确认后,请对着麦克风说话,并留意喇叭是否有声音输出!!!!",
+                "震动马达测试":"震动马达测试...",
+                "测试结束":"测试完毕,将自动重启,请留意测试结果"
+
+            }
+}
\ No newline at end of file
diff --git a/产测工具/FactoryTool-2.1/locale/en/LC_MESSAGES/English.mo b/产测工具/FactoryTool-2.1/locale/en/LC_MESSAGES/English.mo
new file mode 100644 (file)
index 0000000..c206697
Binary files /dev/null and "b/\344\272\247\346\265\213\345\267\245\345\205\267/FactoryTool-2.1/locale/en/LC_MESSAGES/English.mo" differ
diff --git a/产测工具/FactoryTool-2.1/locale/en/LC_MESSAGES/English.po b/产测工具/FactoryTool-2.1/locale/en/LC_MESSAGES/English.po
new file mode 100644 (file)
index 0000000..e4f539e
--- /dev/null
@@ -0,0 +1,124 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"POT-Creation-Date: 2020-11-04 11:03+0800\n"
+"PO-Revision-Date: 2022-05-05 15:33+0800\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"Language: en\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=cp936\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+"X-Generator: Poedit 3.0.1\n"
+
+msgid "±à¼­PyÎļþ"
+msgstr "Edit Py File"
+
+msgid "±à¼­ExcelÎļþ"
+msgstr "Edit Excel File"
+
+msgid "±à¼­"
+msgstr "Edit"
+
+msgid "¹¤¾ßÈÕÖ¾"
+msgstr "Tool Log"
+
+msgid "Ä£×éÈÕÖ¾"
+msgstr "Module Log"
+
+msgid "ÈÕÖ¾"
+msgstr "Log"
+
+msgid "Ñ¡Ôñ"
+msgstr "Load"
+
+msgid "È«²¿¿ªÊ¼"
+msgstr "Start All"
+
+msgid "¿ªÊ¼"
+msgstr "Start"
+
+msgid "¹¤³§²âÊÔ"
+msgstr "Factory Test"
+
+msgid "Ñ¡ÔñÅäÖÃÎļþ"
+msgstr "Load Config File"
+
+msgid "Ñ¡Ôñ²âÊԽű¾"
+msgstr "Load Test Script"
+
+msgid "¶Ë¿Ú"
+msgstr "Port"
+
+msgid "IMEI"
+msgstr "IMEI"
+
+msgid "ICCID"
+msgstr "ICCID"
+
+msgid "²âÊÔʱ¼ä"
+msgstr "Test Time"
+
+msgid "²âÊÔ½á¹û"
+msgstr "T-Result"
+
+msgid "²é¿´ÈÕÖ¾"
+msgstr "View Log"
+
+
+msgid "py´úÂëÓÐÓï·¨´íÎó"
+msgstr "The py code has syntax errors"
+
+msgid "ÇëÏÈÑ¡Ôñ²âÊÔÎļþ"
+msgstr "Please select test file"
+
+msgid "²âÊÔÈÕÖ¾"
+msgstr "Test log"
+
+msgid "µ±Ç°×´Ì¬: ¾ÍÐ÷..."
+msgstr " Current status: ready..."
+
+msgid "»¶Ó­Ê¹ÓÃQuecPython"
+msgstr "  Welcome to QuecPython"
+
+msgid "Ä£¿éÈÕÖ¾"
+msgstr "Module Log"
+
+msgid "²âÏî"
+msgstr "Test-Item"
+
+msgid "²âÊÔ·½Ê½"
+msgstr "T-Method"
+
+msgid "È˹¤¼ì²â"
+msgstr "Manual Test"
+
+msgid "×Ô¶¯¼ì²â"
+msgstr "Automated Test"
+
+msgid "ͨ¹ý"
+msgstr "Pass"
+
+msgid "²»Í¨¹ý"
+msgstr "Fail"
+
+msgid "²âÊÔÖÐ"
+msgstr "Testing"
+
+msgid "      µ±Ç°²âÊÔÏîΪ:  "
+msgstr "      The current test item is:  "
+
+msgid "\r\n\r\n      ÇëÈ·ÈϸòâÊÔÏî ¡¾ÊÇ·ñͨ¹ý¡¿"
+msgstr "\r\n\r\n      Please confirm the test result [Passed or Not]"
+
+msgid "ÊÇ·ñ»Ö¸´ÉÏ´ÎÎļþ"
+msgstr "Do you want to restore the last file"
+
+msgid "Îļþ²»´æÔÚ"
+msgstr "File does not exist"
+
diff --git a/产测工具/FactoryTool-2.1/locale/zh_CN/LC_MESSAGES/Chinese.mo b/产测工具/FactoryTool-2.1/locale/zh_CN/LC_MESSAGES/Chinese.mo
new file mode 100644 (file)
index 0000000..a90c065
Binary files /dev/null and "b/\344\272\247\346\265\213\345\267\245\345\205\267/FactoryTool-2.1/locale/zh_CN/LC_MESSAGES/Chinese.mo" differ
diff --git a/产测工具/FactoryTool-2.1/locale/zh_CN/LC_MESSAGES/Chinese.po b/产测工具/FactoryTool-2.1/locale/zh_CN/LC_MESSAGES/Chinese.po
new file mode 100644 (file)
index 0000000..6ad8702
--- /dev/null
@@ -0,0 +1,17 @@
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR ORGANIZATION
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: \n"
+"POT-Creation-Date: 2020-11-04 11:03+0800\n"
+"PO-Revision-Date: 2020-11-04 20:56+0800\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=cp936\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Generated-By: pygettext.py 1.5\n"
+"Last-Translator: \n"
+"Language-Team: \n"
+"Language: en\n"
+"X-Generator: Poedit 2.4.1\n"
\ No newline at end of file
diff --git a/产测工具/FactoryTool-2.1/log_redirect.py b/产测工具/FactoryTool-2.1/log_redirect.py
new file mode 100644 (file)
index 0000000..450cafb
--- /dev/null
@@ -0,0 +1,64 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+"""
+@Project:FactoryTool 
+@File:log_redirect.py
+@Author:rivern.yuan
+@Date:2022/10/11 9:47 
+"""
+
+import os
+import datetime
+
+
+class RedirectErr:
+    """"""
+
+    def __init__(self, obj, logpath):
+        """Constructor"""
+        if not os.path.exists(logpath + "\\logs\\software\\err\\"):
+            os.makedirs(logpath + "\\logs\\software\\err\\")
+
+        file_path = logpath + "\\logs\\software\\err\\" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + "_run_err.log"
+        self.filename = open(file_path, "a", encoding='utf-8')
+
+    def write(self, text):
+        """"""
+        if self.filename.closed:
+            pass
+        else:
+            curr_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+            self.filename.write('[' + str(curr_time) + '] ' + text)
+            self.filename.flush()
+
+    def flush(self):
+        pass
+
+
+class RedirectStd:
+    """"""
+
+    def __init__(self, obj, logpath):
+        """Constructor"""
+        if not os.path.exists(logpath + "\\logs\\software\\std\\"):
+            os.makedirs(logpath + "\\logs\\software\\std\\")
+        file_path = logpath + "\\logs\\software\\std\\" + datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + "_run_std.log"
+        self.filename = open(file_path, "a", encoding='utf-8')
+
+    def write(self, text):
+        """"""
+        if self.filename.closed:
+            pass
+        else:
+            curr_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+            if text != "":
+                self.filename.write('[' + str(curr_time) + '] ' + text)
+                self.filename.flush()
+
+    def flush(self):
+        pass
+
+
+# class StandardOutWrite:
+#     def write(self, x):
+#         old_std.write(x.replace("\n", " [[%s]]\n" % str(datetime.datetime.now())))
diff --git a/产测工具/FactoryTool-2.1/main.py b/产测工具/FactoryTool-2.1/main.py
new file mode 100644 (file)
index 0000000..05f9127
--- /dev/null
@@ -0,0 +1,749 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+"""
+@Project:Factory_test
+@File:serial_handler.py
+@Author:rivern.yuan
+@Date:2022/9/15 21:09
+"""
+
+import sys
+import os
+import time
+import wx
+import locale
+import gettext
+import asyncio
+import datetime
+import subprocess
+import threading
+import file_handler
+import serial_list
+import serial_handler
+import configparser
+from log_redirect import RedirectErr, RedirectStd
+from pubsub import pub
+from queue import Queue
+import codecs
+import json
+
+if hasattr(sys, '_MEIPASS'):
+    PROJECT_ABSOLUTE_PATH = os.path.dirname(os.path.realpath(sys.executable))
+else:
+    PROJECT_ABSOLUTE_PATH = os.path.dirname(os.path.abspath(__file__))
+
+
+INTERACTIVE_TEST_TIMEOUT = 20  # flag==2:确认开始后再计时,超时无串口回显则失败
+
+TEST_MODE_AUTO = 0         # 自动开始,自动判定结果
+TEST_MODE_MANUAL = 1       # 弹窗确认开始,人工判定结果
+TEST_MODE_SEMI_AUTO = 2    # 弹窗确认开始,自动判定结果(开始后计时)
+
+
+def _get_test_mode(test_function):
+    """配置 [名称, 函数, ?, flag],flag 在 testFunctions 最后一列。"""
+    if not test_function:
+        return TEST_MODE_AUTO
+    if len(test_function) >= 3:
+        return int(test_function[2])
+    if len(test_function) >= 2:
+        return int(test_function[1])
+    return TEST_MODE_AUTO
+
+
+def _test_method_label(mode):
+    if mode == TEST_MODE_MANUAL:
+        return _(u"人工检测")
+    return _(u"自动检测")
+
+
+def _build_test_cmd(test_function):
+    """JSON 中函数名可能已带 (),避免拼出 fw_version()()。"""
+    func = test_function[0].strip()
+    if func.endswith("()"):
+        func = func[:-2]
+    return "TestBase." + func + "()"
+
+
+def _parse_test_result(test_result, cmd):
+    lines = [ln.strip() for ln in test_result.replace("\r\n", "\n").split("\n") if ln.strip()]
+    for line in reversed(lines):
+        if line in ("True", "False"):
+            return line == "True"
+        if line == cmd:
+            return True
+    if "Traceback" in test_result or "TypeError" in test_result:
+        return False
+    return False
+
+
+def _repl_finished(test_result):
+    return test_result.endswith(">>> ")
+
+
+def _is_restart_test(test_function):
+    """会触发模组重启、串口不会再返回 >>> 的测项。"""
+    func = test_function[0].strip().lower()
+    return "det_changedir_end" in func or "powerrestart" in func
+
+
+def _system_language_tag():
+    """检测系统语言,供 gettext 选择 zh_CN / en(不用已废弃的 getdefaultlocale)。"""
+    cats = []
+    if hasattr(locale, 'LC_MESSAGES'):
+        cats.append(locale.LC_MESSAGES)
+    cats.extend((locale.LC_ALL, locale.LC_CTYPE, None))
+    for cat in cats:
+        try:
+            loc, _enc = locale.getlocale(cat) if cat is not None else locale.getlocale()
+        except Exception:
+            loc = None
+        if loc:
+            s = loc.lower().replace('-', '_')
+            if s.startswith('zh') or 'chinese' in s:
+                return 'zh_CN'
+            if s.startswith('en') or 'english' in s:
+                return 'en'
+            break
+    lang = os.environ.get('LANG', '') or os.environ.get('LC_ALL', '')
+    if lang.lower().startswith('zh'):
+        return 'zh_CN'
+    return 'en'
+
+
+def set_config(PROJECT_ABSOLUTE_PATH, frame):
+    if not os.path.exists(PROJECT_ABSOLUTE_PATH+"\\config.ini"):
+        return
+    conf = configparser.ConfigParser(interpolation=None)
+    conf.read(PROJECT_ABSOLUTE_PATH+"\\config.ini", encoding='utf-8')
+    py_file = conf.get("software", "py_file")
+    json_file = conf.get("software", "json_file")
+
+    if py_file or json_file:
+        no_exist = []
+        msg = wx.MessageBox(_(u'是否恢复上次文件'), _(u'提示'), wx.YES_NO)
+        if msg == wx.YES:
+            if py_file:
+                if os.path.exists(py_file):
+                    frame.text_ctrl_py.SetValue(py_file)
+                else:
+                    conf.set("software", "py_file", "")
+                    wx.MessageBox(py_file + _(u"文件不存在"), _(u"提示"), wx.YES_DEFAULT | wx.ICON_INFORMATION)
+
+            if json_file:
+                if os.path.exists(py_file):
+                    frame.text_ctrl_json.SetValue(json_file)
+                    frame.load_json_file(0, json_file)
+                else:
+                    conf.set("software", "json_file", "")
+                    wx.MessageBox(json_file + _(u"文件不存在"), _(u"提示"), wx.YES_DEFAULT | wx.ICON_INFORMATION)
+
+            with open(PROJECT_ABSOLUTE_PATH + "\\config.ini", "w+", encoding='utf-8') as f:
+                conf.write(f)
+
+        elif msg == wx.NO:
+            conf.set("software", "py_file", "")
+            conf.set("software", "json_file", "")
+            with open(PROJECT_ABSOLUTE_PATH + "\\config.ini", "w+", encoding='utf-8') as f:
+                conf.write(f)
+
+
+class FactoryFrame(wx.Frame):
+    def __init__(self, *args, **kwargs):
+        kwargs["style"] = kwargs.get("style", 0) | wx.DEFAULT_FRAME_STYLE
+        wx.Frame.__init__(self, *args, **kwargs)
+        self.SetMinSize((1000, 820))
+        self.SetSize((1120, 900))
+        self.Center()
+        # stdout log
+        sys.stderr = RedirectErr(self, PROJECT_ABSOLUTE_PATH)
+        sys.stdout = RedirectStd(self, PROJECT_ABSOLUTE_PATH)
+        # module log
+        self.logger = ModuleLog()
+        self.testTips = {}
+        # Menu Bar
+        self.menuBar = wx.MenuBar()
+        wxg_tmp_menu = wx.Menu()
+        # wxg_tmp_menu.Append(1001, u"Auto detect Port", "")
+        # self.Bind(wx.EVT_MENU, self.__menu_handler, id=1001)
+        # wxg_tmp_menu.AppendSeparator()
+        wxg_tmp_menu.Append(1002, _(u"编辑Py文件"), "")
+        self.Bind(wx.EVT_MENU, self.__menu_handler, id=1002)
+        wxg_tmp_menu.Append(1003, _(u"编辑Excel文件"), "")
+        self.Bind(wx.EVT_MENU, self.__menu_handler, id=1003)
+        self.menuBar.Append(wxg_tmp_menu, _(u"编辑"))
+        wxg_tmp_menu_1 = wx.Menu()
+        wxg_tmp_menu_1.Append(2001, _(u"工具日志"), "")
+        self.Bind(wx.EVT_MENU, self.__menu_handler, id=2001)
+        wxg_tmp_menu_1.AppendSeparator()
+        wxg_tmp_menu_1.Append(2002, _(u"模块日志"), "")
+        self.Bind(wx.EVT_MENU, self.__menu_handler, id=2002)
+        self.menuBar.Append(wxg_tmp_menu_1, _(u"日志"))
+        self.SetMenuBar(self.menuBar)
+        # Menu Bar end
+        # self.statusBar = self.CreateStatusBar(3)
+        self.main_panel = wx.Panel(self, wx.ID_ANY)
+        self.load_py = wx.Button(self.main_panel, wx.ID_ANY, _("选择"))
+        self.text_ctrl_py = wx.TextCtrl(self.main_panel, wx.ID_ANY, "", style=wx.TE_READONLY)
+        self.load_json = wx.Button(self.main_panel, wx.ID_ANY, _("选择"))
+        self.text_ctrl_json = wx.TextCtrl(self.main_panel, wx.ID_ANY, "", style=wx.TE_READONLY)
+
+        self.button_all_start = wx.Button(self.main_panel, wx.ID_ANY, _("全部开始"))
+
+        self.button_start_list = []
+        self.ListCtrl_list = []
+        self.log_ctrl_list = []
+        self.port_ctrl_list = []
+        self.port_imei_list = []
+        self.port_iccid_list = []
+        for i in range(4):
+            ListCtrl = wx.ListCtrl(self.main_panel, 200+i, style=wx.LC_HRULES | wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_VRULES)
+            ListCtrl.SetMinSize((-1, 300))
+            ListCtrl.AppendColumn(_(u"测项"), format=wx.LIST_FORMAT_LEFT, width=130)
+            ListCtrl.AppendColumn(_(u"测试方式"), format=wx.LIST_FORMAT_LEFT, width=70)
+            ListCtrl.AppendColumn(_(u"测试结果"), format=wx.LIST_FORMAT_LEFT, width=70)
+
+            log_ctrl = wx.TextCtrl(
+                self.main_panel, 400+i, "",
+                style=wx.TE_MULTILINE | wx.TE_READONLY | wx.TE_WORDWRAP)
+            log_ctrl.SetMinSize((-1, 88))
+            log_ctrl.SetForegroundColour(wx.Colour(220, 0, 0))
+
+            button = wx.Button(self.main_panel, 100+i, _("开始"))
+            text_ctrl = wx.TextCtrl(self.main_panel, 300+i, "", style=wx.TE_READONLY)
+
+            self.ListCtrl_list.append(ListCtrl)
+            self.log_ctrl_list.append(log_ctrl)
+            self.button_start_list.append(button)
+            self.port_ctrl_list.append(text_ctrl)
+            # self.port_imei_list.append(text_ctrl_imei)
+            # self.port_iccid_list.append(text_ctrl_iccid)
+
+        self.__exec_py_cmd_list = ["from usr.test import TestBase"]
+        self.__init_excel()
+
+        # event message queue
+        self.message_queue = Queue(maxsize=10)
+        self._channel_list = []
+        threading.Thread(target=self.port_process_handler, args=()).start()
+
+        # bind Func
+        self.Bind(wx.EVT_BUTTON, self.__load_py_file, self.load_py)
+        self.Bind(wx.EVT_BUTTON, self.load_json_file, self.load_json)
+        self.Bind(wx.EVT_BUTTON, self.test_all_start, self.button_all_start)
+
+
+        self.procese_num = 0
+
+        for i in self.button_start_list:
+            self.Bind(wx.EVT_BUTTON, self.test_start, i)
+        self.Bind(wx.EVT_CLOSE, self.close_window)
+
+        # pub message
+        pub.subscribe(self.port_update, "serialUpdate")
+
+        self.conf = configparser.ConfigParser(interpolation=None)
+        if not os.path.exists(PROJECT_ABSOLUTE_PATH+"\\config.ini"):
+            self.initConfigFile()
+        self.conf.read(PROJECT_ABSOLUTE_PATH+"\\config.ini", encoding='utf-8')
+
+        self.__set_properties()
+        self.__do_layout()
+
+    def __set_properties(self):
+        for i in range(4):
+            self.button_start_list[i].SetMinSize((150, 50))
+        self.button_all_start.SetMinSize((150, 50))
+
+    def __do_layout(self):
+        """垂直:文件选择(固定高度) -> 工厂测试区(随窗口拉伸) -> 全部开始(底栏固定)。"""
+        root = wx.BoxSizer(wx.VERTICAL)
+        main = wx.BoxSizer(wx.VERTICAL)
+
+        sizer_3 = wx.StaticBoxSizer(
+            wx.StaticBox(self.main_panel, wx.ID_ANY, _("选择测试脚本")), wx.HORIZONTAL)
+        sizer_3.Add(self.load_py, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 8)
+        sizer_3.Add(self.text_ctrl_py, 1, wx.EXPAND | wx.ALL, 8)
+
+        sizer_4 = wx.StaticBoxSizer(
+            wx.StaticBox(self.main_panel, wx.ID_ANY, _("选择配置文件")), wx.HORIZONTAL)
+        sizer_4.Add(self.load_json, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT, 8)
+        sizer_4.Add(self.text_ctrl_json, 1, wx.EXPAND | wx.ALL, 8)
+
+        main.Add(sizer_3, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 6)
+        main.Add(sizer_4, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 6)
+
+        sizer_5 = wx.StaticBoxSizer(
+            wx.StaticBox(self.main_panel, wx.ID_ANY, _("工厂测试")), wx.HORIZONTAL)
+        for i in range(4):
+            col = wx.BoxSizer(wx.VERTICAL)
+            col.Add(self.port_ctrl_list[i], 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.TOP, 6)
+            # 测项表占主要拉伸比例,日志与按钮在其下方
+            col.Add(self.ListCtrl_list[i], 5, wx.EXPAND | wx.ALL, 4)
+            col.Add(self.log_ctrl_list[i], 2, wx.EXPAND | wx.LEFT | wx.RIGHT, 4)
+            col.Add(self.button_start_list[i], 0,
+                    wx.ALIGN_CENTER_HORIZONTAL | wx.TOP | wx.BOTTOM, 8)
+            sizer_5.Add(col, 1, wx.EXPAND | wx.ALL, 4)
+
+        main.Add(sizer_5, 1, wx.EXPAND | wx.ALL, 6)
+
+        btn_row = wx.BoxSizer(wx.HORIZONTAL)
+        btn_row.AddStretchSpacer(1)
+        btn_row.Add(self.button_all_start, 0, wx.ALIGN_CENTER_VERTICAL)
+        btn_row.AddStretchSpacer(1)
+        main.Add(btn_row, 0, wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, 12)
+
+        self.main_panel.SetSizer(main)
+        root.Add(self.main_panel, 1, wx.EXPAND)
+        self.SetSizer(root)
+        self.Layout()
+
+    def __menu_handler(self, event):  # wxGlade: FactoryFrame.<event_handler>
+        if event.GetId() == 1001:
+            DialogControl(self, "demo").Show()
+
+        elif event.GetId() == 1002:
+            file_path, app_path = "notepad.exe", PROJECT_ABSOLUTE_PATH + "\\module_test.py"
+            raw_file_path = r'%s' % file_path
+            raw_app_path = r'%s' % app_path
+            subprocess.call([raw_file_path, raw_app_path])
+        elif event.GetId() == 1003:
+            excel_file = subprocess.Popen(["start", "/WAIT", PROJECT_ABSOLUTE_PATH + "\\Test-Result.xlsx"], shell=True)
+            # psutil.Process(excel_file.pid).get_children()[0].kill()
+            excel_file.poll()
+        elif event.GetId() == 2001:
+            p = subprocess.Popen("explorer.exe " + PROJECT_ABSOLUTE_PATH + "\\logs\\software\\", shell=True)
+        elif event.GetId() == 2002:
+            p = subprocess.Popen("explorer.exe " + PROJECT_ABSOLUTE_PATH + "\\logs\\apps\\", shell=True)
+        event.Skip()
+
+    def initConfigFile(self):
+        self.conf.add_section("software")
+        self.conf.set("software", "py_file", "")
+        self.conf.set("software", "json_file", "")
+        with open(PROJECT_ABSOLUTE_PATH + "\\config.ini", "w+", encoding='utf-8') as f:
+            self.conf.write(f)
+
+    def __load_py_file(self, event):
+        defDir, defFile = '', ''  # default dir/ default file
+        dlg = wx.FileDialog(self, u'Open Py File', defDir, defFile, 'Python file (*.py)|*.py',
+                            wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
+        if dlg.ShowModal() == wx.ID_CANCEL:
+            return
+        py_cmd = file_handler.ScriptHandler(dlg.GetPath())
+        if py_cmd.script_check():
+            self.text_ctrl_py.SetValue(dlg.GetPath())
+            self.conf.set("software", "py_file", dlg.GetPath())
+            with open(PROJECT_ABSOLUTE_PATH + "\\config.ini", "w+", encoding='utf-8') as f:
+                self.conf.write(f)
+        else:
+            wx.MessageBox(_(u'py代码有语法错误'), u'Warn', wx.YES_DEFAULT | wx.ICON_INFORMATION)
+
+    def load_json_file(self, event, json_file=None):
+        print("json button is press")
+        if event != 0:
+            defDir, defFile = '', ''  # default dir/ default file
+            dlg = wx.FileDialog(self, u'Open Config File', defDir, defFile, 'Config file (*.json)|*.json',
+                                wx.FD_OPEN | wx.FD_FILE_MUST_EXIST)
+            if dlg.ShowModal() == wx.ID_CANCEL:
+                return
+            json_file = dlg.GetPath()
+        try:
+            with codecs.open(json_file, 'r', 'utf-8') as f:
+                data = json.load(f)
+                print(data)
+                info = data["info"]
+                self.testFunctions = [i[1:] for i in info]
+                self.testMessages = [i[0] for i in info]
+                self.testTips = data.get("tips", {})
+            self.text_ctrl_json.SetValue(json_file)
+            self.conf.set("software", "json_file", json_file)
+            with open(PROJECT_ABSOLUTE_PATH + "\\config.ini", "w+", encoding='utf-8') as f:
+                self.conf.write(f)
+
+            for i, port_ctrl in enumerate(self.port_ctrl_list):
+                if port_ctrl.GetValue():
+                    self.ListCtrl_list[i].DeleteAllItems()
+                    for j, element in enumerate(self.testFunctions):
+                        ListCtrl = self.ListCtrl_list[i]
+                        # testFunction = self.testFunctions[j]
+                        ListCtrl.InsertItem(j, j)
+                        ListCtrl.SetItem(j, 0, self.testMessages[j])
+                        ListCtrl.SetItem(j, 1, _test_method_label(_get_test_mode(element)))
+        except Exception as e:
+            print(e)
+            wx.MessageBox(_(u'请选择正确的配置文件'), u'提示', wx.YES_DEFAULT | wx.ICON_INFORMATION)
+            return
+
+    def test_start(self, event):
+        button_event_id = event.GetId() - 100  # button ID 100开始
+
+        if self.text_ctrl_json.GetValue():
+            pass
+        else:
+            wx.MessageBox(_(u'请先选择配置文件'), u'提示', wx.YES_DEFAULT | wx.ICON_INFORMATION)
+            return
+
+        if self.text_ctrl_py.GetValue():
+            if self.port_ctrl_list[button_event_id].GetValue():
+                # 点击开始时再初始化
+                self.__port_det(True)  # 检测停止
+
+                self.procese_num += 1
+                self.button_all_start.Enable(False)
+                self.button_start_list[button_event_id].Enable(False)
+                self.message_queue.put(
+                    {"id": button_event_id, "msg_id": "PortInit", "PortInfo": self.port_ctrl_list[button_event_id].GetValue()})
+            else:
+                return None
+        else:
+            wx.MessageBox(_(u'请先选择测试文件'), u'提示', wx.YES_DEFAULT | wx.ICON_INFORMATION)
+
+    def test_all_start(self, event):
+        self.__port_det(True)  # 检测停止
+
+        if self.text_ctrl_json.GetValue():
+            pass
+        else:
+            wx.MessageBox(_(u'请先选择配置文件'), u'提示', wx.YES_DEFAULT | wx.ICON_INFORMATION)
+            return
+
+        if self.text_ctrl_py.GetValue():
+            pass
+        else:
+            wx.MessageBox(_(u'请先选择测试文件'), u'提示', wx.YES_DEFAULT | wx.ICON_INFORMATION)
+            return None
+
+        for i in self.port_ctrl_list:
+            button_event_id = i.GetId() - 300
+            if i.GetValue():
+                self.procese_num += 1
+                self.button_all_start.Enable(False)
+                self.button_start_list[button_event_id].Enable(False)
+                self.message_queue.put(
+                    {"id": button_event_id, "msg_id": "PortInit",
+                     "PortInfo": self.port_ctrl_list[button_event_id].GetValue()})
+            else:
+                return None
+
+    def port_update(self, arg1):
+        print("message:{}".format(arg1))
+        self.message_queue.put({"msg_id": "PortUpdate", "PortInfo": arg1})
+
+    def port_update_handler(self, arg1):
+        # TODO 设置log按钮状态
+        port_list = arg1["PortInfo"][:4] if len(arg1["PortInfo"]) > 4 else arg1["PortInfo"]
+        if len(port_list) < 4:
+            self.button_all_start.Enable(False) if len(port_list) == 0 else self.button_all_start.Enable(True)
+            port_list = port_list + [""] * (4 - len(port_list))
+        if self._channel_list == [] or self._channel_list != port_list:
+            self._channel_list = port_list
+            for port_ctrl in self.port_ctrl_list:
+                port_ctrl.SetValue("")
+            for ListCtrl in self.ListCtrl_list:
+                ListCtrl.DeleteAllItems()
+
+            for i, element in enumerate(self.button_start_list):
+                if self._channel_list[i]:
+                    self.port_ctrl_list[i].SetValue(self._channel_list[i])
+                    self.button_start_list[i].Enable(True)
+
+                    if self.text_ctrl_json.GetValue():
+                        for j, element in enumerate(self.testFunctions):
+                            ListCtrl = self.ListCtrl_list[i]
+                            # testFunction = self.testFunctions[j]
+                            ListCtrl.InsertItem(j, j)
+                            ListCtrl.SetItem(j, 0, self.testMessages[j])
+                            ListCtrl.SetItem(j, 1, _test_method_label(_get_test_mode(element)))
+                else:
+                    self.button_start_list[i].Enable(False)
+
+    def _set_channel_tip(self, channel_id, tip, countdown=None):
+        """在通道日志区显示当前测项 tip(红色)。"""
+        lines = []
+        if countdown is not None:
+            lines.append(_(u"倒计时: ") + str(countdown))
+        if tip:
+            lines.append(tip)
+        self.log_ctrl_list[channel_id].SetValue("\n".join(lines))
+
+    def _clear_channel_tip(self, channel_id):
+        self.log_ctrl_list[channel_id].SetValue("")
+
+    def _confirm_start_test(self, port_title, tip):
+        """弹窗确认开始测试,仅一个确认键。"""
+        msg = tip if tip else _(u"请点击确认开始测试")
+        wx.MessageBox(msg, port_title, wx.OK | wx.ICON_INFORMATION)
+
+    def _confirm_manual_result(self, port_title, message):
+        """人工判定测试结果。"""
+        prompt = _(u"      当前测试项为:  ") + message + _(u"\r\n\r\n      请确认该测试项 【是否通过】")
+        return wx.MessageBox(prompt, port_title, wx.YES_NO | wx.ICON_QUESTION) == wx.YES
+
+    def _ret_result_countdown(self, ser, channel_id, tip, timeout_sec):
+        """等待串口回显,mode==2 时在日志区刷新倒计时。"""
+        data = ""
+        deadline = time.time() + timeout_sec
+        while time.time() < deadline:
+            remaining = max(0, int(deadline - time.time()) + 1)
+            self._set_channel_tip(channel_id, tip, countdown=remaining)
+            waiting = ser._conn.inWaiting()
+            if waiting:
+                data += ser._conn.read(waiting).decode("utf-8", errors="ignore")
+            if data.endswith(">>> "):
+                break
+            time.sleep(0.1)
+        return data
+
+    def _apply_test_row_result(self, channel_id, row, passed):
+        if passed:
+            self.ListCtrl_list[channel_id].SetItem(row, 2, _(u"通过"))
+            self.ListCtrl_list[channel_id].SetItemBackgroundColour(row, "Green")
+        else:
+            self.ListCtrl_list[channel_id].SetItem(row, 2, _(u"不通过"))
+            self.ListCtrl_list[channel_id].SetItemBackgroundColour(row, "Red")
+
+    def port_init_handler(self, arg1):
+        ID = arg1["id"]
+        self._clear_channel_tip(ID)
+        for i in range(len(self.testFunctions)):
+            ListCtrl = self.ListCtrl_list[ID]
+            ListCtrl.SetItem(i, 2, "")
+            ListCtrl.SetItemBackgroundColour(i, (255, 255, 255, 255))
+
+        # 重复测试结果重置
+        # ser = serial_handler.SerialHandler(arg1["PortInfo"])
+        # imei, iccid = asyncio.run(ser.init_module(arg1["imei"], arg1["iccid"]))
+        self.__py_test_fp = open(self.text_ctrl_py.GetValue(), "r", encoding="utf8")
+        self.message_queue.put(
+            {"id": arg1["id"], "msg_id": "PortTest", "PortInfo": self.port_ctrl_list[arg1["id"]].GetValue(),
+             "script": self.__py_test_fp})
+
+    def port_test_handler(self, arg1):
+        print("start test")
+        ID = arg1["id"]
+        try:
+            ser = serial_handler.SerialHandler(arg1["PortInfo"])
+            ser.write_module(arg1["script"], self.__exec_py_cmd_list)  # 写入脚本开始测试
+            ser.ret_result()
+        except Exception as e:
+            print(arg1["PortInfo"], e)
+            wx.MessageBox(arg1["PortInfo"]+"串口异常,测试脚本写入失败, error %s"%str(e), u'Error', wx.YES_DEFAULT | wx.ICON_INFORMATION)
+            self.message_queue.put({"id": arg1["id"], "msg_id": "PortTestEnd", "result": 1})
+            self.button_start_list[arg1["id"]].Enable(True)
+            self.procese_num -= 1
+            if self.procese_num == 0:
+                self.button_all_start.Enable(True)
+            return
+
+        ret_result = []
+        port_title = self.port_ctrl_list[ID].GetValue()
+        tips = getattr(self, "testTips", {})
+        self._clear_channel_tip(ID)
+        for i, testFunction in enumerate(self.testFunctions):
+            passed = False
+            message = self.testMessages[i]
+            mode = _get_test_mode(testFunction)
+            tip = tips.get(message, "")
+            try:
+                cmd = _build_test_cmd(testFunction)
+                self._set_channel_tip(ID, tip)
+                self.ListCtrl_list[ID].SetItem(i, 2, _(u"测试中"))
+                self.ListCtrl_list[ID].SetItemBackgroundColour(i, "Yellow")
+
+                if mode in (TEST_MODE_MANUAL, TEST_MODE_SEMI_AUTO):
+                    self._confirm_start_test(port_title, tip)
+
+                if _is_restart_test(testFunction):
+                    # 重启类测项:指令发出即完成,不能等待 >>>
+                    ser.exec_cmd(cmd)
+                    time.sleep(0.8)
+                    try:
+                        pending = ser._conn.inWaiting()
+                        if pending:
+                            ser._conn.read(pending)
+                    except Exception:
+                        pass
+                    test_result = cmd + "\r\n(restart sent, no repl)\r\n"
+                    self.logger.write_file(arg1["PortInfo"], test_result)
+                    passed = True
+                elif mode == TEST_MODE_SEMI_AUTO:
+                    ser.exec_cmd(cmd)
+                    test_result = self._ret_result_countdown(
+                        ser, ID, tip, INTERACTIVE_TEST_TIMEOUT)
+                    self.logger.write_file(arg1["PortInfo"], test_result)
+                    passed = _repl_finished(test_result) and _parse_test_result(test_result, cmd)
+                else:
+                    ser.exec_cmd(cmd)
+                    test_result = ser.ret_result()
+                    self.logger.write_file(arg1["PortInfo"], test_result)
+                    passed = _parse_test_result(test_result, cmd)
+                    if mode == TEST_MODE_MANUAL:
+                        passed = self._confirm_manual_result(port_title, message)
+
+            except Exception as e:
+                print(e)
+                passed = False
+
+            self._apply_test_row_result(ID, i, passed)
+            ret_result.append("True" if passed else "False")
+
+        test_results = []
+        for i in ret_result:
+            if i == "True":
+                test_results.append(_(u"通过"))
+            elif i == "False":
+                test_results.append(_(u"不通过"))
+            else:
+                test_results.append(i)
+
+        test_method = [_test_method_label(_get_test_mode(i)) for i in self.testFunctions]
+
+        # TODO excel写入不要列表形式
+        if "False" not in ret_result:
+            self.message_queue.put({"id": arg1["id"], "msg_id": "PortTestEnd", "result": 1})
+            self.__excel_write([arg1["PortInfo"], "Success", str(list(zip(self.testMessages, ret_result)))])
+        else:
+            self.message_queue.put({"id": arg1["id"], "msg_id": "PortTestEnd", "result": 2})
+            self.__excel_write([arg1["PortInfo"], "Fail", str(list(zip(self.testMessages, ret_result)))])
+        self._clear_channel_tip(ID)
+        self.button_start_list[arg1["id"]].Enable(True)
+        self.procese_num -= 1
+        if self.procese_num == 0:
+            self.button_all_start.Enable(True)
+
+    def port_test_end_handler(self, arg1):
+        self.__port_det(False)
+
+    def __init_excel(self):
+        self.__excel_handler = file_handler.ExcelHandler(PROJECT_ABSOLUTE_PATH + "\\Test-Result.xlsx")  # Init Excel
+        rows, columns = self.__excel_handler.get_rows_columns()
+        if rows == 1 and columns == 1:
+            self.__excel_handler.set_cell_value(1, 1, "No.")
+            self.__excel_handler.set_cell_value(1, 2, "Com Port")
+            self.__excel_handler.set_cell_value(1, 3, "Test Result")
+            self.__excel_handler.set_cell_value(1, 4, "Test Log")
+
+    def __excel_write(self, result):
+        if self.__excel_handler:
+            rows, columns = self.__excel_handler.get_rows_columns()
+            self.__excel_handler.set_cell_value(rows + 1, 1, rows)
+            for i, value in enumerate(result):
+                self.__excel_handler.set_cell_value(rows + 1, i + 2, value)
+
+    def port_process_handler(self):
+        while True:
+            try:
+                message = self.message_queue.get(True, 5)
+            except Exception as e:
+                message = None
+            try:
+                if message:
+                    # print("message:{}".format(message))
+                    msg_id = message.get("msg_id")
+                    if msg_id == "exit":
+                        pass
+                    elif msg_id == "PortUpdate":
+                        # threading.Thread(target=self.port_update_handler, args=(message,)).start()
+                        self.port_update_handler(message)
+                    elif msg_id == "PortInit":
+                        # threading.Thread(target=self.port_init_handler, args=(message,)).start()
+                        self.port_init_handler(message)
+                    elif msg_id == "PortTest":
+                        threading.Thread(target=self.port_test_handler, args=(message,)).start()
+                        # self.port_test_handler(message)
+                    elif msg_id == "PortTestEnd":
+                        threading.Thread(target=self.port_test_end_handler, args=(message,)).start()
+                        # self.port_test_end_handler(message)
+                    else:
+                        pass
+            except Exception as e:
+                print(e)
+
+    @staticmethod
+    def __port_det(flag):
+        if flag:
+            tSerialDet.exit_event()
+        else:
+            tSerialDet.enter_event()
+
+    # end of class FactoryFrame
+    def close_window(self, event):
+        self.Destroy()
+        try:
+            self.__excel_handler.close()
+        except Exception as e:
+            pass
+        time.sleep(0.2)
+        wx.GetApp().ExitMainLoop()
+        wx.Exit()
+        process_name = "taskkill /F /IM " + file_name
+        p = subprocess.Popen(process_name, shell=True)
+
+
+class MyApp(wx.App):
+    languageTab = _system_language_tag()
+    print("languageTab: ", languageTab)
+    # languageTab = "en"
+    # 根据系统语言自动设置语言
+    if languageTab == "zh_CN":
+        t = gettext.translation('Chinese', PROJECT_ABSOLUTE_PATH + "\\locale", languages=["zh_CN"])
+        t.install()
+    elif languageTab == "en":
+        t = gettext.translation('English', PROJECT_ABSOLUTE_PATH + "\\locale", languages=["en"])
+        t.install()
+    else:
+        languageTab = "en"
+        t = gettext.translation('English', PROJECT_ABSOLUTE_PATH + "\\locale", languages=["en"])
+        t.install()
+
+    def __init__(self):
+        # print("init-----")
+        wx.App.__init__(self, redirect=False, filename="", useBestVisual=True, clearSigInt=True)
+        # wx.App.__init__(self, redirect=False, filename=PROJECT_ABSOLUTE_PATH + "\\logs\\software\\run.log",
+        #                 useBestVisual=True, clearSigInt=True)
+        # print("init finish----")
+
+    def OnInit(self):
+        self.factory_frame = FactoryFrame(None, wx.ID_ANY, "")
+        self.SetTopWindow(self.factory_frame)
+        self.factory_frame.Show()
+
+        # 串口检测线程
+        global tSerialDet
+        tSerialDet = serial_list.SerialDetection()
+        tSerialDet.setDaemon(True)  # set as deamon, stop thread while main frame exit
+        tSerialDet.start()
+
+        set_config(PROJECT_ABSOLUTE_PATH, self.factory_frame)
+        return True
+
+
+class ModuleLog(object):
+    """log output file"""
+    def __init__(self):
+        if not os.path.exists(PROJECT_ABSOLUTE_PATH + "\\logs\\apps\\"):
+            os.makedirs(PROJECT_ABSOLUTE_PATH + "\\logs\\apps\\")
+        for i in os.walk(PROJECT_ABSOLUTE_PATH + "\\logs\\apps\\"):
+            self._log_file_list = i[2]
+        if len(self._log_file_list) >= 50:
+            os.remove(PROJECT_ABSOLUTE_PATH + "\\logs\\apps\\" + self._log_file_list[0])
+        self._log_name = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") + ".log"
+
+    def write_file(self, port, data):
+        try:
+            tmp_data = "[" + port + "]\n" + data + "\n"
+            with open(PROJECT_ABSOLUTE_PATH + "\\logs\\apps\\" + self._log_name, "a+", encoding="utf-8")as f:
+                f.write(tmp_data)
+            return True
+        except:
+            info = sys.exc_info()
+            print("write file error.")
+            print(info[0], info[1])
+            return False
+
+
+if __name__ == "__main__":
+    file_name = os.path.basename(sys.executable)
+    app = MyApp()
+    app.MainLoop()
\ No newline at end of file
diff --git a/产测工具/FactoryTool-2.1/media/quectel.ico b/产测工具/FactoryTool-2.1/media/quectel.ico
new file mode 100644 (file)
index 0000000..a9b82f9
Binary files /dev/null and "b/\344\272\247\346\265\213\345\267\245\345\205\267/FactoryTool-2.1/media/quectel.ico" differ
diff --git a/产测工具/FactoryTool-2.1/module_test.py b/产测工具/FactoryTool-2.1/module_test.py
new file mode 100644 (file)
index 0000000..cc45f29
--- /dev/null
@@ -0,0 +1,28 @@
+# 这里写需要导入的模块
+import sim
+import net
+import uos
+import utime
+
+class TestBase(object):
+    # ------该区域为测试代码------
+    @staticmethod
+    def det_signal():
+        utime.sleep(2)
+        if sim.getStatus() == 1:
+            if net.getConfig()[0] == 5:
+                return True
+            else:
+                return False
+        else:
+            return False
+
+    @staticmethod
+    def det_file_space():
+        utime.sleep(2)
+        if uos.statvfs('usr')[3] > 5:
+            return True
+        else:
+            return False
+    # ------该区域为测试代码------
+
diff --git a/产测工具/FactoryTool-2.1/requirements.txt b/产测工具/FactoryTool-2.1/requirements.txt
new file mode 100644 (file)
index 0000000..6208bef
--- /dev/null
@@ -0,0 +1,5 @@
+Pypubsub==4.0.3
+wxPython>=4.2.0
+openpyxl==3.0.7
+pyserial==3.5
+
diff --git a/产测工具/FactoryTool-2.1/serial_handler.py b/产测工具/FactoryTool-2.1/serial_handler.py
new file mode 100644 (file)
index 0000000..0c1fbe8
--- /dev/null
@@ -0,0 +1,144 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+"""
+@Project:Factory_test 
+@File:serial_handler.py
+@Author:rivern.yuan
+@Date:2022/9/6 15:03 
+"""
+import serial
+import time
+import asyncio
+
+
+class SerialPort(object):
+    '''
+    class for serial operation
+    '''
+    def __init__(self, port, baud):
+        self._port = port
+        self._baud = baud
+        self._conn = None
+
+    def _open_conn(self):
+        self._conn = serial.Serial(self._port, self._baud)
+        return self._conn
+
+    def _close_conn(self):
+        self._conn.close()
+        self._conn = None
+
+    def _send_cmd(self, cmd):
+        self._conn.write((cmd + '\r\n').encode())
+
+    def _recv_cmd(self):
+        time.sleep(.2)
+        byte_n = self._conn.inWaiting()
+        if byte_n > 0:
+            return self._conn.read(byte_n)
+
+    def _test_conn(self):
+        self._send_cmd("1")
+        # recv: b'1\r\n1\r\n>>> ' 测试正常交互
+        test_recv = self._recv_cmd()
+        if test_recv:
+            if len(test_recv) == 10:
+                return True
+            else:
+                print(test_recv)
+                raise SerialError(self._port, "串口持续输出中,请检查运行状态")
+        else:
+            raise SerialError(self._port, "串口堵塞,请检查串口连通性")
+
+
+class SerialHandler(SerialPort):
+    """write test script & read test result """
+    def __init__(self, port, baud=115200):
+        super(SerialHandler, self).__init__(port, baud)
+        self.init()
+
+    def write_module(self, source, py_cmd, filename="test.py"):
+        self._test_conn()
+
+        self._conn.write(b"\x01")
+        self._send_cmd("f=open('/usr/" + filename + "','wb')")  # 写入文件
+        self._send_cmd("w=f.write")
+        while True:
+            time.sleep(.1)
+            data = source.read(255)
+            if not data:
+                break
+            else:
+                self._send_cmd("w(" + repr(data) + ")")
+                self._conn.write(b"\x04")
+        self._send_cmd("f.close()")
+        self._conn.write(b"\x04")
+        self._conn.write(b"\x02")
+        self._conn.write(b"\x04")
+
+        source.close()
+        self.exec_cmd(py_cmd[0])
+        return True
+
+    def exec_py(self, cmd):
+        self._send_cmd(cmd[0])
+        self._conn.flushInput()
+        self._send_cmd(cmd[1])
+        time.sleep(1)
+        return self.ret_result()
+
+    def exec_cmd(self, cmd):
+        self._send_cmd(cmd)
+
+    async def run_cmd(self,source:list, log_text_ctrl):
+        result_list = ""
+        for i in source:
+            self._send_cmd(i)
+            await asyncio.sleep(.1)
+            result_list += self.ret_result()
+            log_text_ctrl.SetValue(result_list)
+        return result_list
+
+    def ret_result(self, timeout_sec=None):
+        data = ""
+        if timeout_sec is None:
+            # 单条自动测项最长约 15s,避免异常时卡住 30s
+            for i in range(30):
+                data += self._conn.read(self._conn.inWaiting()).decode("utf-8", errors="ignore")
+                if data.endswith(">>> "):
+                    break
+                time.sleep(0.5)
+        else:
+            deadline = time.time() + timeout_sec
+            while time.time() < deadline:
+                waiting = self._conn.inWaiting()
+                if waiting:
+                    data += self._conn.read(waiting).decode("utf-8", errors="ignore")
+                if data.endswith(">>> "):
+                    break
+                time.sleep(0.1)
+        return data
+
+    def exit_test(self):
+        self._close_conn()
+
+    def init(self):
+        self._open_conn()
+
+
+class SerialError(Exception):
+    '''
+    exception for serial blocking connection
+    '''
+    def __init__(self, _port, _error):
+        self._port = _port
+        self._error = _error
+
+    def __str__(self):
+        return self._port + " " + self._error
+
+
+if __name__ == '__main__':
+    ser = SerialHandler("COM63")
+
+
diff --git a/产测工具/FactoryTool-2.1/serial_list.py b/产测工具/FactoryTool-2.1/serial_list.py
new file mode 100644 (file)
index 0000000..18fa335
--- /dev/null
@@ -0,0 +1,80 @@
+#!/usr/bin/env python
+# -*- coding: UTF-8 -*-
+"""
+@Project:Factory_test 
+@File:serial_list.py
+@Author:rivern.yuan
+@Date:2022/9/6 15:59 
+"""
+
+import time
+import threading
+import serial.tools.list_ports
+from pubsub import pub
+
+
+class SerialDetection(threading.Thread):
+    """
+    class for detection port list
+    """
+    def __init__(self):
+        super(SerialDetection, self).__init__()
+        self.serPort = serial.tools.list_ports
+        self._exit = False
+
+    def exit_event(self):
+        self._exit = True
+
+    def enter_event(self):
+        self._exit = False
+
+    def get_com_number(self, vid_pid, location):
+        port_list = []
+        vid = int(vid_pid.split(":")[0], 16)
+        pid = int(vid_pid.split(":")[1], 16)
+        for p in list(self.serPort.comports()):
+            if p.vid == vid and p.pid == pid and p.location and location in p.location:
+                port_list.append(p.device)
+        return port_list
+
+    def get_repl_ports(self):
+        """按 Quectel REPL 口描述识别产测串口(兼容不同 USB 接口编号)。"""
+        port_list = []
+        for p in list(self.serPort.comports()):
+            if p.vid == 0x2C7C and p.description and "REPL" in p.description:
+                port_list.append(p.device)
+        return port_list
+
+    def scan_test_ports(self):
+        """汇总产测可用 REPL 串口,兼容旧版与 EG810 等新模组 USB 布局。"""
+        serial_port = self.get_repl_ports()
+        if serial_port:
+            return serial_port
+
+        serial_port = self.get_com_number("0x2C7C:0x0901", "x.8")
+        serial_port.extend(self.get_com_number("0x2C7C:0x6002", "x.5"))
+        serial_port.extend(self.get_com_number("0x2C7C:0x6002", "x.20"))  # EG810 等 REPL
+        serial_port.extend(self.get_com_number("0x2C7C:0x6005", "x.5"))
+        serial_port.extend(self.get_com_number("0x2C7C:0x6005", "x.8"))
+        return list(dict.fromkeys(serial_port))
+
+    def run(self):
+        serial_list = []
+        last_ports = []
+        while True:
+            if self._exit:
+                pass
+            else:
+                if serial_list == [] or serial_list != self.serPort.comports():
+                    serial_list = self.serPort.comports()
+                    serial_port = self.scan_test_ports()
+                    if serial_port != last_ports:
+                        last_ports = serial_port
+                        pub.sendMessage('serialUpdate', arg1=serial_port)
+            time.sleep(1)
+
+
+if __name__ == '__main__':
+    ser = SerialDetection()
+    ser.setDaemon(False)
+    ser.start()
diff --git a/产测工具/FactoryTool-2.1/sort_setting.json b/产测工具/FactoryTool-2.1/sort_setting.json
new file mode 100644 (file)
index 0000000..f4b6e97
--- /dev/null
@@ -0,0 +1,4 @@
+{
+    "info": [["Memory test", "det_file_space", 1],
+             ["Signal test", "det_signal", 0]]
+}
\ No newline at end of file