app_base.py 40.1 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910
from appium.webdriver.common.appiumby import AppiumBy
from time import sleep
from appium.options.android import UiAutomator2Options
from django.db.models.fields import return_None
from hytest import *
from pywinauto.mouse import click
from selenium import webdriver

# 创建一个函数,用于初始化Appium驱动程序
def app_setup_driver(platformName, platformVersion, deviceName, appPackage, appActivity, udid):
    """
    根据提供的参数设置 Appium 驱动程序。

    参数:
    - platformName: 操作系统名称
    - platformVersion: 操作系统版本
    - deviceName: 设备名称
    - appPackage: 应用程序包名
    - appActivity: 应用程序活动名
    - udid: 设备唯一标识符

    返回:
    - driver: Appium 驱动程序对象
    """
    # 定义设备和应用的相关参数,以便 Appium 能够识别和控制设备
    desired_caps = {
        'platformName': platformName,  # 被测手机是安卓
        'platformVersion': platformVersion,  # 手机安卓版本,如果是鸿蒙系统,依次尝试 12、11、10 这些版本号
        'deviceName': deviceName,  # 设备名,安卓手机可以随意填写
        'appPackage': appPackage,  # 启动APP Package名称
        'appActivity': appActivity,  # 启动Activity名称
        'unicodeKeyboard': True,  # 自动化需要输入中文时填True
        'resetKeyboard': True,  # 执行完程序恢复原来输入法
        'noReset': True,  # 不要重置App
        'newCommandTimeout': 6000,
        'automationName': 'UiAutomator2',
        'skipUnlock': True,
        'autoGrantPermissions': True,
        'udid':udid
    }
    # 记录 desired_caps 参数信息
    logging.info(f"desired_caps参数:{desired_caps}")

    try:
        # 记录初始化 Appium 驱动程序的过程
        logging.info("正在初始化 Appium 驱动程序...")
        # 创建 Appium 驱动程序对象
        driver = webdriver.Remote('http://localhost:4723/wd/hub',
                                  options=UiAutomator2Options().load_capabilities(desired_caps))
        # 记录 Appium 驱动程序初始化成功
        logging.info("Appium 驱动程序初始化成功。")
        # 返回 Appium 驱动程序对象
        return driver
    except Exception as e:
        # 记录初始化驱动程序失败的错误信息
        logging.error(f"初始化驱动程序失败: {e}")
        # 重新抛出异常
        raise

# 封装滑动操作
def swipe_up(app_driver):
    """
    在应用程序中执行上滑操作。

    参数:
    - app_driver: 应用程序的驱动对象,用于与设备交互。

    返回值:

    """
    # 获取屏幕尺寸
    size = app_driver.get_window_size()
    # 计算滑动的起始和结束坐标
    start_x = size['width'] // 2
    start_y = int(size['height'] * 0.2)  # 起始y坐标,屏幕高度的20%
    end_x = start_x
    end_y = int(size['height'] * 0.8)  # 结束y坐标,屏幕高度的80%

    # 执行滑动操作
    app_driver.swipe(start_x, start_y, end_x, end_y, duration=500)

# 图片亮度对比函数
# 请使用“pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple”安装PIL库
from PIL import Image
import numpy as np
import os
import logging
def compare_brightness(light_down_path, light_on_path, threshold=1):
    """
    对比两张图片的亮度,返回亮度是否增加的布尔值。
    light_on_path:传入暗色的图片
    light_down_path:传入亮色的图片
    threshold:亮度变化的阈值,默认为1
    """
    try:
        # 打开图片并转换为灰度图像,以便后续处理
        image1 = Image.open(light_down_path).convert('L')  # 转换为灰度图像
        image2 = Image.open(light_on_path).convert('L')  # 转换为灰度图像

        # 将图像转换为numpy数组,便于计算
        array1 = np.array(image1)
        array2 = np.array(image2)

        # 计算两张图片的平均亮度
        avg_brightness1 = np.mean(array1)
        avg_brightness2 = np.mean(array2)

        # 记录日志,输出两张图片的平均亮度
        logging.info(f"关闭灯光时的平均亮度: {avg_brightness1}")
        logging.info(f"打开灯光时的平均亮度: {avg_brightness2}")

        # 计算亮度变化量
        brightness_increase = avg_brightness2 - avg_brightness1
        # 记录日志,输出亮度变化量
        logging.info(f"亮度变化量: {brightness_increase}")

        # 判断亮度变化量是否超过阈值
        return brightness_increase > threshold
    except Exception as e:
        # 异常处理,记录错误日志
        logging.error(f"对比亮度时发生错误: {e}", exc_info=True)
        return False

# 调用示例
# if __name__ == '__main__':
#     logging.info("开始对比亮度")
#
#     image1_path = r'D:\GithubData\自动化\ubains-module-test\预定系统\Base\captured_frame2.jpg'
#     image2_path = r'D:\GithubData\自动化\ubains-module-test\预定系统\Base\captured_frame.jpg'
#
#     # 检查图片路径是否存在
#     if not os.path.exists(image1_path):
#         logging.error(f"图片 {image1_path} 不存在")
#         exit(1)
#     if not os.path.exists(image2_path):
#         logging.error(f"图片 {image2_path} 不存在")
#         exit(1)
#
#     # 对比两张截图的亮度
#     result = compare_brightness(image1_path, image2_path)
#     logging.info(f"亮度比较结果: {result}")
#
#     if result:
#         logging.info("灯光已成功打开")
#     else:
#         logging.error("灯光未成功打开")

# 提取特征点并比较图片函数
# 请使用“pip install opencv-python”安装cv2库
import cv2
import logging
from PIL import Image
import numpy as np
import os
def compare_images_feature_matching(image1_path, image2_path):
    """
    比较两张图片是否相同

    使用特征匹配的方法比较两张图片是否相同。首先验证图片路径和格式,然后根据图片尺寸进行调整,
    最后转换为numpy数组进行比较。

    参数:
    image1_path (str): 第一张图片的路径
    image2_path (str): 第二张图片的路径

    返回:
    dict: 包含比较结果和可能的错误信息
    """
    try:
        # 验证图片路径是否存在且为有效图片文件
        if not os.path.isfile(image1_path) or not os.path.isfile(image2_path):
            logging.error("图片路径无效")
            return {"result": False, "error": "图片路径无效"}

        # 打开两张图片
        img1 = Image.open(image1_path)
        img2 = Image.open(image2_path)

        # 验证图片是否为有效格式
        if img1.format not in ['JPEG', 'PNG'] or img2.format not in ['JPEG', 'PNG']:
            logging.error("图片格式无效")
            return {"result": False, "error": "图片格式无效"}

        # 如果尺寸不同,先调整大小
        if img1.size != img2.size:
            logging.info("图片尺寸不同,调整为相同尺寸进行比较")
            img2 = img2.resize(img1.size, Image.ANTIALIAS)  # 保持纵横比

        # 将图片转换为相同的模式
        img1 = img1.convert("RGB")
        img2 = img2.convert("RGB")

        # 转换为 numpy 数组进行比较
        img1_array = np.array(img1)
        img2_array = np.array(img2)

        # 输出numpy数组信息
        logging.info(f"图片1数组: {img1_array},图片2数组: {img2_array}")

        # 比较两个数组是否相同
        return {"result": np.array_equal(img1_array, img2_array), "error": None}

    except FileNotFoundError as e:
        logging.error(f"文件未找到: {e}")
        return {"result": False, "error": f"文件未找到: {e}"}
    except OSError as e:
        logging.error(f"图片处理错误: {e}")
        return {"result": False, "error": f"图片处理错误: {e}"}
    except MemoryError as e:
        logging.error(f"内存不足: {e}")
        return {"result": False, "error": f"内存不足: {e}"}
    except Exception as e:
        logging.error(f"未知错误: {e}")
        return {"result": False, "error": f"未知错误: {e}"}

# 示例调用
# if __name__ == '__main__':
#     logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
#     logging.info("开始对比图片")
#
#     image1_path = r'D:\GithubData\自动化\ubains-module-test\预定系统\reports\imgs\Exhibit_Inspect\No_PaperLess\DeviceA-ShareScreen.png'
#     image2_path = r'D:\GithubData\自动化\ubains-module-test\预定系统\reports\imgs\Exhibit_Inspect\No_PaperLess\同屏前-无纸化设备B界面截屏.png'
#
#     if not os.path.exists(image1_path):
#         logging.error(f"图片 {image1_path} 不存在")
#         exit(1)
#     if not os.path.exists(image2_path):
#         logging.error(f"图片 {image2_path} 不存在")
#         exit(1)
#
#     # 对比两张截图的相似度
#     if compare_images_feature_matching(image1_path, image2_path):
#         logging.info("图片相同")
#     else:
#         logging.error("图片不同")

# 计算直方图相似度函数
import cv2
import numpy
from PIL import Image
import logging
import os
def calculate(image1, image2):
    """
    计算两张图片的直方图重合度

    通过将图片转换为BGR格式,并计算每张图片的蓝色通道直方图,然后比较这两个直方图的重合度来评估图片的相似度

    参数:
    image1: 第一张图片,应为RGB格式
    image2: 第二张图片,应为RGB格式

    返回值:
    返回两张图片直方图的重合度,范围在0到1之间,1表示完全重合,即图片高度相似
    """
    image1 = cv2.cvtColor(numpy.asarray(image1), cv2.COLOR_RGB2BGR)
    image2 = cv2.cvtColor(numpy.asarray(image2), cv2.COLOR_RGB2BGR)
    hist1 = cv2.calcHist([image1], [0], None, [256], [0.0, 255.0])
    hist2 = cv2.calcHist([image2], [0], None, [256], [0.0, 255.0])
    # 计算直方图的重合度
    degree = 0
    for i in range(len(hist1)):
        if hist1[i] != hist2[i]:
            degree = degree + (1 - abs(hist1[i] - hist2[i]) / max(hist1[i], hist2[i]))
        else:
            degree = degree + 1
    degree = degree / len(hist1)
    return degree

# 图片相似性对比函数
def classify_hist_with_split(image1, image2, size=(256, 256)):
    """
    根据两张图片的RGB直方图比较它们的相似性。

    参数:
    image1: 第一张图片的路径。
    image2: 第二张图片的路径。
    size: 将图片调整到的统一尺寸,默认为(256, 256)。

    返回:
    两张图片的相似度,值越小表示两张图片越相似。
    """
    # 打开图片文件
    image1 = Image.open(image1)
    image2 = Image.open(image2)
    # 将PIL图像转换为OpenCV格式(BGR)
    image1 = cv2.cvtColor(numpy.asarray(image1), cv2.COLOR_RGB2BGR)
    image2 = cv2.cvtColor(numpy.asarray(image2), cv2.COLOR_RGB2BGR)
    # 调整图片尺寸,以确保比较是在相同尺寸下进行
    image1 = cv2.resize(image1, size)
    image2 = cv2.resize(image2, size)
    # 分离图片的RGB通道
    sub_image1 = cv2.split(image1)
    sub_image2 = cv2.split(image2)
    sub_data = 0
    # 遍历每个通道,计算并累加相似度
    for im1, im2 in zip(sub_image1, sub_image2):
        sub_data += calculate(im1, im2)
    # 计算平均相似度
    sub_data = sub_data / 3
    # 返回最终的相似度结果
    return sub_data

# 示例调用
# if __name__ == '__main__':
#     logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
#     logging.info("开始对比图片")
#
#     image1_path = r'D:\GithubData\自动化\ubains-module-test\预定系统\reports\imgs\Exhibit_Inspect\No_PaperLess\同屏后-无纸化设备A界面截屏.png'
#     image2_path = r'D:\GithubData\自动化\ubains-module-test\预定系统\reports\imgs\Exhibit_Inspect\No_PaperLess\同屏后-无纸化设备B界面截屏.png'
#
#     if not os.path.exists(image1_path):
#         logging.error(f"图片 {image1_path} 不存在")
#         exit(1)
#     if not os.path.exists(image2_path):
#         logging.error(f"图片 {image2_path} 不存在")
#         exit(1)
#
#     # 对比两张截图的相似度
#     result1 = classify_hist_with_split(image1_path, image2_path)
#
#     # 确保 result1 是一个标量值
#     if isinstance(result1, numpy.ndarray):
#         result1 = result1.item()
#
#     print("相似度为:" + "%.2f%%" % (result1 * 100))

# 检查输出路径是否有效函数
import cv2
import logging
import os
import shutil  # 导入 shutil 模块以检查磁盘空间
def check_output_path(output_path):
    """
    检查输出路径是否有效

    如果输出目录不存在,则尝试创建它,并检查是否有写权限

    参数:
    output_path (str): 输出文件的路径

    返回:
    bool: 如果输出路径有效且可写,则返回True,否则返回False
    """
    # 获取输出文件的目录部分
    output_dir = os.path.dirname(output_path)
    # 检查输出目录是否存在
    if not os.path.exists(output_dir):
        try:
            # 尝试创建输出目录
            os.makedirs(output_dir)
            logging.info(f"创建目录: {output_dir}")
        except Exception as e:
            # 如果创建目录失败,记录错误信息并返回False
            logging.error(f"无法创建目录 {output_dir}: {e}")
            return False
    # 检查文件权限
    if not os.access(output_dir, os.W_OK):
        # 如果没有写权限,记录错误信息并返回False
        logging.error(f"没有写权限: {output_dir}")
        return False
    # 如果一切正常,返回True
    return True

# 捕获RTSP流并保存为图像文件函数
def capture_frame_from_rtsp(rtsp_url, file_name, output_path=None):
    """
    从RTSP流中捕获一帧并保存为图像文件。

    参数:
    - rtsp_url: RTSP流的URL。
    - file_name: 保存图像文件的名称。
    - output_path: 保存图像文件的路径,默认为None,如果未提供则使用默认路径。

    返回:
    - 成功捕获并保存帧时返回True,否则返回False。
    """
    try:
        # 验证输入参数
        if not rtsp_url:
            logging.error("RTSP URL 为空")
            return False

        # 获取当前脚本所在的根目录
        script_dir = os.path.dirname(os.path.abspath(__file__))
        root_dir = os.path.dirname(script_dir)

        # 构建默认输出路径
        if output_path is None:
            output_path = os.path.join(root_dir, "reports", "imgs", "Exhibit_Inspect", "Control_Manage", file_name)

        # 检查并创建输出目录
        if not check_output_path(output_path):
            return False

        # 打开RTSP流
        cap = cv2.VideoCapture(rtsp_url)
        if not cap.isOpened():
            logging.error("无法打开RTSP流")
            return False

        # 尝试多次读取帧以确保获取有效帧
        for _ in range(5):  # 尝试读取5次
            ret, frame = cap.read()
            if ret and frame is not None:
                break
        else:
            logging.error("无法从RTSP流中读取有效帧")
            cap.release()
            return False

        # 确认帧不为空
        if frame is None or frame.size == 0:
            logging.error("捕获到的帧为空")
            cap.release()
            return False

        # 检查帧的形状和类型
        logging.info(f"捕获到的帧尺寸: {frame.shape}, 数据类型: {frame.dtype}")

        # 尝试保存帧为图像文件
        success = False
        try:
            # 使用 cv2.imencode 保存图像到内存中,再写入文件
            _, img_encoded = cv2.imencode('.png', frame)
            with open(output_path, 'wb') as f:
                f.write(img_encoded.tobytes())
            success = True
        except Exception as e:
            logging.error(f"无法保存帧到 {output_path}: {e}")
            logging.error(f"检查路径是否存在: {os.path.exists(os.path.dirname(output_path))}")
            logging.error(f"检查路径是否可写: {os.access(os.path.dirname(output_path), os.W_OK)}")

            # 使用 shutil.disk_usage 检查磁盘空间
            try:
                total, used, free = shutil.disk_usage(os.path.dirname(output_path))
                logging.error(f"检查磁盘空间: {free // (2 ** 20)} MB available")
            except Exception as e:
                logging.error(f"无法检查磁盘空间: {e}")

        if success:
            logging.info(f"帧已保存到 {output_path}")
        else:
            logging.error(f"帧保存失败")

        # 释放资源
        cap.release()
        return success
    except Exception as e:
        logging.error(f"捕获帧时发生错误: {e}", exc_info=True)
        return False

# 中控屏灯光控制函数
def light_control(app_drive):
    """
    控制灯光的函数。

    该函数通过Appium驱动定位并点击应用中的灯光控制按钮,以开启不同区域的灯光。

    参数:
    - app_drive: Appium驱动实例,用于与移动应用交互。
    """
    # 开启所有区域灯光
    # 定位【接待区】灯光
    light_reception_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                     "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.ImageView[1]")
    sleep(2)
    logging.info("尝试定位【接待区】按钮元素,并点击按钮")
    click_with_retry(light_reception_button)
    sleep(2)

    # 定位【指挥中心】灯光
    light_command_center_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                          "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button[3]")
    sleep(2)
    logging.info("尝试定位【指挥中心】按钮元素,并点击按钮")
    click_with_retry(light_command_center_button)
    sleep(2)

    # 定位【影音室】灯光
    light_audio_room_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                      "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button[2]")
    sleep(2)
    logging.info("尝试定位【影音室】按钮元素,并点击按钮")
    click_with_retry(light_audio_room_button)
    sleep(2)

    # 定位【会议室】灯光
    light_meeting_room_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                        "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button[5]")
    sleep(2)
    logging.info("尝试定位【会议室】按钮元素,并点击按钮")
    click_with_retry(light_meeting_room_button)
    sleep(2)

    # 定位【会商区】灯光
    light_meeting_area_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                       "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button[4]")
    sleep(2)
    logging.info("尝试定位【会商区】按钮元素,并点击按钮")
    click_with_retry(light_meeting_area_button)
    sleep(2)

    # 定位【培训室】灯光
    light_training_room_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                        "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button[6]")
    sleep(2)
    logging.info("尝试定位【培训室】按钮元素,并点击按钮")
    click_with_retry(light_training_room_button)
    sleep(2)

# 中控屏窗帘控制函数
def curtain_control(app_drive, wd):
    """
    控制窗帘的上升和下降,并捕获相应状态的截图。

    参数:
    app_drive: Appium驱动对象,用于操作App。
    wd: WebDriver对象,用于捕获屏幕截图。

    此函数无返回值。
    """
    # 所有窗帘全部上升
    logging.info("尝试定位所有【窗帘上升】按钮元素,并点击按钮")
    # 上升按钮的定位
    curtain_up_locator = ['3', '4', '5', '1', '13']
    for i in curtain_up_locator:
        curtain_up_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                    f"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button[{i}]")
        click_with_retry(curtain_up_button)
        sleep(2)
    INFO("请检查窗帘上升状态是否正常")
    # 截图获取当前中控屏软件窗帘上升的界面
    get_screenshot_with_retry(wd, app_drive, "Exhibit_Inspect", "Control_Manage", "curtain_up")

    sleep(30)
    # # 测试报告中补充窗帘上升的截图
    SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "curtain_rtsp_up")
    # 通过rtsp流获取当前窗帘的上升效果图
    curtain_rtsp_url = "rtsp://admin:huawei@123@192.168.4.18/LiveMedia/ch1/Media2"
    logging.info("开始捕获RTSP流中的帧")
    if capture_frame_from_rtsp(curtain_rtsp_url, "curtain_rtsp_up.png"):
        logging.info("帧捕获成功")
    else:
        logging.error("帧捕获失败")

    # 所有窗帘全部下降
    logging.info("尝试定位所有【窗帘下降】按钮元素,并点击按钮")
    # 下降按钮的定位
    curtain_down_locator = ['10', '11', '12', '6', '15']
    for i in curtain_down_locator:
        curtain_down_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                      f"/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button[{i}]")
        click_with_retry(curtain_down_button)
        sleep(2)
    sleep(30)
    INFO("请检查窗帘下降状态是否正常")
    get_screenshot_with_retry(wd, app_drive, "Exhibit_Inspect", "Control_Manage", "curtain_down")
    # 截图获取当前中控屏软件窗帘上升的界面
    # 测试报告中补充窗帘下降的截图
    SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "curtain_rtsp_down")
    logging.info("开始捕获RTSP流中的帧")
    if capture_frame_from_rtsp(curtain_rtsp_url, "curtain_rtsp_down.png"):
        logging.info("帧捕获成功")
    else:
        logging.error("帧捕获失败")

# 中控屏空调控制函数
def air_condition_control(app_drive, wd):
    """
    控制空调的打开与关闭,并检查其状态显示。

    参数:
    - app_drive: Appium驱动对象,用于操作移动端应用。
    - wd: WebDriver对象,用于捕获屏幕截图。

    此函数不返回任何值。
    """
    # 点击【打开空调】按钮
    logging.info("尝试定位【打开空调】按钮元素,并点击按钮")
    open_air_conditioner_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                          "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button[14]")
    click_with_retry(open_air_conditioner_button)
    sleep(20)

    # 这是空调开启的状态显示
    INFO("请检查空调开启的状态是否正常")
    get_screenshot_with_retry(wd, app_drive, "Exhibit_Inspect", "Control_Manage", "air_condition_on")
    sleep(2)

    # 点击【关闭空调】按钮
    logging.info("尝试定位【关闭空调】按钮元素,并点击按钮")
    close_air_conditioner_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                           "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button[14]")
    click_with_retry(close_air_conditioner_button)
    sleep(20)

    # 这是空调关闭的状态显示
    INFO("请检查空调关闭的状态是否正常")
    get_screenshot_with_retry(wd, app_drive, "Exhibit_Inspect", "Control_Manage", "air_condition_off")
    sleep(2)

# 中控屏信息发布控制函数
def information_control(app_drive, wd):
    """
    控制信息展示和捕获RTSP流中的帧。

    参数:
    - app_drive: Appium驱动实例,用于操作移动应用。
    - wd: WebDriver实例,用于操作网页。

    此函数依次选择不同的内容进行播放,捕获RTSP流中的帧,并记录屏幕状态。
    """

    # 选择生日快乐内容播放
    brithday_information_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                         "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button[1]")
    click_with_retry(brithday_information_button)
    logging.info("选择生日快乐内容播放")
    sleep(5)
    # 这是生日快乐主题内容发布
    INFO("请检查中控屏软件信息发布界面是否正常")
    SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "information_brithday_on")

    information_rtsp_url = "rtsp://admin:huawei@123@192.168.4.19/LiveMedia/ch1/Media2"  # 替换为你的RTSP流地址
    logging.info("开始捕获RTSP流中的帧")
    if capture_frame_from_rtsp(information_rtsp_url, "information_brithday_on.png"):
        logging.info("帧捕获成功")
    else:
        logging.error("帧捕获失败")

    # 选择欢迎领导发布内容播放
    meeting_information_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                        "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button[2]")
    click_with_retry(meeting_information_button)
    logging.info("选择会议发布内容播放")
    sleep(5)
    # 这是会议欢迎主题内容发布
    INFO("请检查中控屏软件信息发布界面是否正常")
    SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "information_meeting_on")

    logging.info("开始捕获RTSP流中的帧")
    if capture_frame_from_rtsp(information_rtsp_url, "information_meeting_on.png"):
        logging.info("帧捕获成功")
    else:
        logging.error("帧捕获失败")

    # 选择展厅空间结构内容播放
    information_space_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                      "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button[3]")
    click_with_retry(information_space_button)
    logging.info("选择展厅空间结构内容播放")
    sleep(5)
    # 这是展厅空间结构内容发布
    INFO("请检查中控屏软件信息发布界面是否正常显示")
    SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "information_space_on")

    if capture_frame_from_rtsp(information_rtsp_url, "information_space_on.png"):
        logging.info("帧捕获成功")
    else:
        logging.error("帧捕获失败")

    # 点击信息发布关闭按钮
    INFO("点击信息发布关闭按钮")
    information_close_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button[6]")
    click_with_retry(information_close_button)
    # 这是信发屏已关闭的界面
    INFO("请检查中控屏软件信息发布界面是否正常显示为关闭")
    SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "information_off")

    if capture_frame_from_rtsp(information_rtsp_url, "information_off.png"):
        logging.info("帧捕获成功")
    else:
        logging.error("帧捕获失败")

# 中控屏音乐控制函数
def music_control(app_drive, wd):
    """
    控制音乐播放的函数,包括播放和停止音乐。

    :param app_drive: Appium驱动对象,用于操作App。
    :param wd: WebDriver对象,用于浏览器自动化操作。
    """
    # 点击【播放音乐】
    logging.info("尝试定位【播放音乐】按钮元素,并点击按钮")
    play_music_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                               "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button[8]")
    click_with_retry(play_music_button)
    sleep(2)

    sleep(5)
    # 这是音乐开启播放后的界面显示
    INFO("请检查中控屏软件打开音乐播放后的界面状态显示")
    get_screenshot_with_retry(wd, app_drive, "Exhibit_Inspect", "Control_Manage", "music_on")

    # 点击【关闭播放音乐】
    logging.info("尝试定位【关闭播放音乐】按钮元素,并点击按钮")
    close_play_music_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                     "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button[8]")
    click_with_retry(close_play_music_button)
    sleep(5)
    # 这是音乐关闭播放后的界面显示
    INFO("请检查中控屏软件关闭音乐播放后的界面状态显示")
    get_screenshot_with_retry(wd, app_drive, "Exhibit_Inspect", "Control_Manage", "music_off")

# 中控屏控制函数
def command_centre_control(rtsp_url, app_drive, wd):
    """
    控制指挥中心大屏的开启和关闭,并 capture RTSP 流的一帧作为日志。

    参数:
    - rtsp_url: RTSP 流的 URL。
    - app_drive: Appium 驱动对象,用于操作移动应用。
    - wd: WebDriver 对象,用于执行 Selenium 相关操作。

    此函数会尝试打开指挥中心大屏,capture 并记录大屏开启和关闭时的监控视频帧。
    """
    # 打开指挥中心大屏
    open_center_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button")
    click_with_retry(open_center_button)
    sleep(10)
    # 这是指挥大屏开启的监控视频显示
    INFO("请检查指挥大屏开启的监控视频状态是否正常")
    SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "command_screen_on")
    # 从rtsp流中截取一帧保存为图片
    logging.info("开始捕获RTSP流中的帧")
    if capture_frame_from_rtsp(rtsp_url, "command_screen_on.png"):
        logging.info("帧捕获成功")
    else:
        logging.error("帧捕获失败")

    # 这是指挥大屏关闭的监控视频显示
    INFO("请检查指挥大屏关闭的监控视频状态是否正常")
    SELENIUM_LOG_SCREEN(wd, "50%", "Exhibit_Inspect", "Control_Manage", "command_screen_down")
    # 关闭指挥中心大屏幕
    logging.info("尝试定位【关闭指挥中心控制】按钮元素,并点击按钮")
    close_center_button = find_element_with_retry(app_drive, AppiumBy.XPATH,
                                                 "/hierarchy/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.LinearLayout/android.widget.FrameLayout/android.widget.RelativeLayout/android.widget.RelativeLayout/android.widget.RelativeLayout[2]/android.widget.Button")
    click_with_retry(close_center_button)
    sleep(10)
    # 从rtsp流中截取一帧保存为图片
    logging.info("开始捕获RTSP流中的帧")
    if capture_frame_from_rtsp(rtsp_url,
                               "command_screen_down.png"):
        logging.info("帧捕获成功")
    else:
        logging.error("帧捕获失败")

# app设备初始化adb连接函数
import subprocess
from venv import logger
def app_init(device_ip, port=5555):
    """
    初始化浏览器设置和实例。

    此函数旨在初始化程序与app设备之间的adb连接,判断adb连接状态是否可控
    """
    # 标记初始化过程的开始
    INFO("'----------' 正在初始化ADB连接 '----------'")
    """
        通过 ADB 连接设备并检查设备状态
        :param device_ip: 设备的 IP 地址
        :param port: 端口号,默认为 5555
        """
    try:
        # 构建设备地址
        device_address = f"{device_ip}:{port}"
        # 连接设备
        subprocess.run(['adb', 'connect', device_address], check=True)
        INFO(f"尝试连接到设备: {device_address}")

        # 检查设备状态
        result = subprocess.run(['adb', 'devices'], capture_output=True, text=True, check=True)
        devices = result.stdout.strip().split('\n')[1:]  # 去掉标题行
        for device in devices:
            ip, status = device.split()
            if ip == device_address and status == 'device':
                INFO(f"设备 {device_address} 已连接并可用")
                return True
            elif ip == device_address and status == 'offline':
                INFO(f"设备 {device_address} 处于 offline 状态,当前设备不可控制,请检查设备网络状态")
                return False
            elif ip == device_address and status == 'unauthorized':
                logger.error(f"设备 {device_address} 未授权调试")
                return False
        INFO(f"设备 {device_address} 未找到,请检查设备IP是否正确!")
        return False
    except subprocess.CalledProcessError as e:
        INFO(f"连接设备失败: {e}")
        return False

# app设备退出adb连接函数
def app_quit(device_ip,port=5555):
    """
    退出浏览器并释放资源。

    该函数从全局存储中获取设备adb连接状态
    """
    # 断开特定 IP 和端口的 ADB 连接
    device_address = f"{device_ip}:{port}"
    subprocess.run(['adb', 'disconnect', device_address])
    INFO(f"ADB 连接已断开: {device_address}")

# app截屏函数
def get_screenshot_with_retry(wd,app_drive, module_name, function_name, step_name, max_retries=3, retry_delay=5):
    """
    使用重试机制获取并保存截图。

    参数:
    app_drive: 实现了get_screenshot_as_file方法的对象,用于获取截图。
    module_name: 用于构造保存截图的目录名称。
    function_name: 用于构造截图的目录名称。
    setp_name:用于构造截图文件的名称
    max_retries: 最大重试次数,默认为3次。
    retry_delay: 重试间隔时间,默认为5秒。

    返回值:
    无。如果多次尝试截图失败,则抛出异常。
    """
    # 获取当前文件的绝对路径
    current_file_path = os.path.abspath(__file__)
    # 获取当前文件的父级目录
    parent_dir = os.path.dirname(current_file_path)
    # 构造目标目录路径
    target_dir = os.path.join(parent_dir, '..', 'reports', 'imgs', module_name, function_name)
    # 确保目标目录存在,如果不存在则创建
    os.makedirs(target_dir, exist_ok=True)
    # 构造文件路径
    file_path = os.path.join(target_dir, f"{step_name}.png")

    #截屏
    SELENIUM_LOG_SCREEN(wd, "75%", module_name, function_name, f"{step_name}")

    # 使用循环实现重试机制
    for _ in range(max_retries):
        try:
            # 尝试保存截图
            app_drive.get_screenshot_as_file(file_path)
            # 如果成功,记录日志并退出函数
            logging.info(f"截图保存成功: {file_path}")
            return
        except Exception as e:
            # 如果失败,记录日志并等待重试
            logging.warning(f"截图失败,重试中... ({e})")
            sleep(retry_delay)
    # 如果多次尝试均失败,则抛出异常
    raise Exception(f"多次尝试截图失败: {file_path}")

# app查找元素函数
def find_element_with_retry(app_driver, by, value, max_retries=3, retry_delay=5):
    """
    使用重试机制查找元素。

    在WebDriver(driver)中通过给定的查找方式(by)和值(value)来查找页面元素。
    如果在指定的最大重试次数(max_retries)内仍然找不到元素,则抛出异常。
    每次重试之间会有指定的延迟时间(retry_delay)。

    参数:
    - driver: WebDriver实例,用于执行查找操作。
    - by: 查找元素的方式,如XPath、ID等。
    - value: 元素的值,根据'by'参数指定的查找方式对应的具体值。
    - max_retries: 最大重试次数,默认为3次。
    - retry_delay: 每次重试之间的延迟时间,默认为5秒。

    返回:
    - 返回找到的元素。

    异常:
    - 如果超过最大重试次数仍未找到元素,则抛出异常。
    """
    for _ in range(max_retries):
        try:
            # 尝试查找元素,如果成功则立即返回元素
            return app_driver.find_element(by, value)
        except Exception as e:
            # 如果查找元素失败,记录日志并等待一段时间后重试
            logging.warning(f"查找元素失败,重试中... ({e})")
            sleep(retry_delay)
    # 如果达到最大重试次数仍未找到元素,则抛出异常
    raise Exception(f"多次尝试查找元素失败: {by}={value}")

# app点击事件函数
def click_with_retry(element, max_retries=3, retry_delay=5):
    """
    点击元素的函数,带有重试机制。

    参数:
    element (obj): 要点击的元素对象。
    max_retries (int): 最大重试次数,默认为3次。
    retry_delay (int): 每次重试之间的延迟时间,默认为5秒。

    异常:
    如果超过最大重试次数仍未成功点击元素,则抛出异常。
    """
    # 尝试点击元素,直到达到最大重试次数
    for _ in range(max_retries):
        try:
            # 尝试点击元素
            element.click()
            # 如果点击成功,记录日志并退出函数
            logging.info(f"点击元素成功: {element}")
            return
        except Exception as e:
            # 如果点击失败,记录日志并等待下一次重试
            logging.warning(f"点击元素失败,重试中... ({e})")
            sleep(retry_delay)
    # 如果所有重试都失败,抛出异常
    raise Exception(f"多次尝试点击元素失败: {element}")