N706A H111 T1108 版本AT指令上网固件,python验证测试脚本提交。https/http下载,tcp 连接已经验证完毕。证书已经生成。 2733/1 2733/head 2793/target/1
author孙春阳 <snickersun@163.com>
Wed, 12 Nov 2025 10:19:37 +0000 (18:19 +0800)
committer孙春阳 <snickersun@163.com>
Thu, 13 Nov 2025 01:07:58 +0000 (09:07 +0800)
37 files changed:
cat1modem/leiniao/N706A-H111-STD-BZ_CN1X-001_T1108.fwpkg [new file with mode: 0644]
cat1modem/src/origImg/N706A-H111-STD-BZ_CN1X-001_T1108.tar.gz [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/at_test_connect_tcp.py [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/at_test_download_file.py [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/at_test_get_device_token.py [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/atcmd.txt [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/certificate_storage_and_usage.md [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/certificate_usage_guide.md [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/certs/.gitignore [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/certs/ca_cert_usage_explanation.md [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/certs/ca_chain.pem [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/certs/cert_chain.pem [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/certs/cert_chain_ca.pem [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/certs/check_certificate.py [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/certs/download_server_ca.py [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/certs/extract_ca_from_chain.py [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/certs/generate_test_certs.sh [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/certs/intermediate_cas.pem [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/certs/root_ca.pem [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/certs/verify_ca_for_https.py [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/download_emoji_mp4.py [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/download_mp4_at.py [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/get_device_token_at.py [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/n706a_at_handler.py [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/n706a_http_client.py [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/n706a_tcp_client.py [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/performance_analysis.md [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/performance_optimization_summary.md [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/request_headers_comparison.md [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/single_auth_usage.md [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/tcp_test_server.py [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/verify_signatures.py [new file with mode: 0644]
code/sdk/zhihui/leiniao/leiniao_python_test/websocket_feasibility_analysis.md [new file with mode: 0644]
code/sdk/zhihui/ppp/doc/Neoway_功能模块_SSL_TLS_应用指南_V1.0.md [new file with mode: 0644]
code/sdk/zhihui/ppp/doc/Neoway_功能模块_SSL_TLS_应用指南_V1.0.pdf [new file with mode: 0644]
code/sdk/zhihui/ppp/doc/pdf_to_md.py [new file with mode: 0644]
code/sdk/zhihui/ppp/doc/pdf_watermark_remover.py [new file with mode: 0644]

diff --git a/cat1modem/leiniao/N706A-H111-STD-BZ_CN1X-001_T1108.fwpkg b/cat1modem/leiniao/N706A-H111-STD-BZ_CN1X-001_T1108.fwpkg
new file mode 100644 (file)
index 0000000..162f336
Binary files /dev/null and b/cat1modem/leiniao/N706A-H111-STD-BZ_CN1X-001_T1108.fwpkg differ
diff --git a/cat1modem/src/origImg/N706A-H111-STD-BZ_CN1X-001_T1108.tar.gz b/cat1modem/src/origImg/N706A-H111-STD-BZ_CN1X-001_T1108.tar.gz
new file mode 100644 (file)
index 0000000..192051a
Binary files /dev/null and b/cat1modem/src/origImg/N706A-H111-STD-BZ_CN1X-001_T1108.tar.gz differ
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_connect_tcp.py b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_connect_tcp.py
new file mode 100644 (file)
index 0000000..d68a08e
--- /dev/null
@@ -0,0 +1,217 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+通过TCP客户端连接个人测试服务器
+使用重构后的公共TCP客户端模块实现
+通过被动接收的方式接收服务器的消息
+"""
+
+import time
+from n706a_at_handler import ATCommandHandler
+from n706a_tcp_client import N706ATCPClient
+
+# -------------------------- 1. 配置参数 --------------------------
+# 服务器配置
+SERVER_HOST = "snicker.com.cn"
+SERVER_PORT = 8910
+
+# AT模块串口配置(根据实际硬件配置修改)
+SERIAL_PORT = "COM19"  # 修改为实际的串口设备
+SERIAL_BAUDRATE = 1000000
+SERIAL_TIMEOUT = 10
+
+# Socket ID
+SOCKET_ID = 0
+
+# 是否使用SSL/TLS加密
+USE_SSL = False  # 设置为True使用TCP+SSL连接
+
+# SSL配置(仅USE_SSL=True时需要)
+SSL_AUTH_MODE = 0  # 0=不认证, 1=单向认证, 2=双向认证
+SSL_CA_CERT = None  # CA证书文件名(单向/双向认证需要)
+SSL_CLIENT_CERT = None  # 客户端证书文件名(双向认证需要)
+SSL_CLIENT_KEY = None  # 客户端密钥文件名(双向认证需要)
+
+# 接收超时时间(秒)
+RECEIVE_TIMEOUT = 30
+
+# -------------------------- 2. 连接测试服务器函数 --------------------------
+def connect_test_server():
+    """连接测试服务器并被动接收消息"""
+    
+    # 初始化AT指令处理器
+    at_handler = ATCommandHandler(SERIAL_PORT, SERIAL_BAUDRATE, SERIAL_TIMEOUT)
+    
+    try:
+        # 连接串口
+        if not at_handler.connect():
+            print("❌ 无法连接串口,请检查串口配置")
+            return False
+        
+        # 测试AT连接
+        success, response = at_handler.send_at_command("AT", timeout=3)
+        if not success:
+            print("❌ AT模块无响应,请检查连接")
+            return False
+        print("✅ AT模块连接正常")
+        
+        # 检查PPP拨号状态
+        print("\n" + "=" * 50)
+        print("【步骤1: 检查PPP拨号状态】")
+        print("=" * 50)
+        is_ppp_connected, ppp_ip = at_handler.check_ppp_status()
+        
+        if is_ppp_connected:
+            print(f"✅ PPP已连接,IP地址: {ppp_ip}")
+        else:
+            print(f"❌ PPP未连接,IP地址: {ppp_ip or '未知'}")
+            print("\n尝试建立PPP连接...")
+            
+            apn_name = None
+            success, ip = at_handler.establish_ppp_connection(apn_name=apn_name)
+            
+            if not success:
+                print(f"❌ PPP连接建立失败,无法继续TCP连接")
+                return False
+            
+            ppp_ip = ip
+        
+        # 配置网络省电模式
+        print("\n" + "=" * 50)
+        print("【步骤2: 配置网络省电模式】")
+        print("=" * 50)
+        success, response = at_handler.send_at_command("AT+NWENPWRSAVE=1", timeout=5)
+        if not success:
+            print(f"⚠️ 网络省电模式配置失败: {response}")
+            print("继续执行...")
+        else:
+            print("✅ 网络省电模式已启用 (AT+NWENPWRSAVE=1)")
+        
+        # 初始化TCP客户端
+        print("\n" + "=" * 50)
+        print("【步骤3: 初始化TCP客户端】")
+        print("=" * 50)
+        tcp_client = N706ATCPClient(at_handler, socket_id=SOCKET_ID)
+        
+        # 配置SSL(如果需要)
+        if USE_SSL:
+            print("\n" + "=" * 50)
+            print("【步骤4: 配置SSL参数】")
+            print("=" * 50)
+            if not tcp_client.configure_ssl(
+                ssl_version=3,  # TLS1.2
+                auth_mode=SSL_AUTH_MODE,
+                ca_cert=SSL_CA_CERT,
+                client_cert=SSL_CLIENT_CERT,
+                client_key=SSL_CLIENT_KEY
+            ):
+                print("❌ SSL配置失败,终止流程")
+                return False
+        
+        # 建立TCP连接
+        protocol_name = "TCP+SSL" if USE_SSL else "TCP"
+        print("\n" + "=" * 50)
+        print(f"【步骤{4 if not USE_SSL else 5}: 建立{protocol_name}连接】")
+        print("=" * 50)
+        print(f"连接服务器: {SERVER_HOST}:{SERVER_PORT}")
+        
+        if not tcp_client.connect(
+            server_ip=SERVER_HOST,
+            server_port=SERVER_PORT,
+            use_ssl=USE_SSL,
+            transparent_mode=False  # 非透传模式,使用被动接收
+        ):
+            print(f"❌ {protocol_name}连接失败")
+            return False
+        
+        print(f"✅ {protocol_name}连接成功")
+        
+        # 被动接收服务器消息
+        print("\n" + "=" * 50)
+        print("【步骤6: 被动接收服务器消息】")
+        print("=" * 50)
+        print(f"等待服务器发送消息(超时时间: {RECEIVE_TIMEOUT}秒)...")
+        print("(按Ctrl+C中断)")
+        print("=" * 50)
+        
+        message_count = 0
+        start_time = time.time()
+        
+        try:
+            while True:
+                # 等待接收数据
+                data = tcp_client.wait_for_data(timeout=RECEIVE_TIMEOUT)
+                
+                if data:
+                    message_count += 1
+                    elapsed_time = time.time() - start_time
+                    
+                    print(f"\n【消息 #{message_count}】 (连接后 {elapsed_time:.1f}秒)")
+                    print(f"数据长度: {len(data)} 字节")
+                    
+                    # 尝试解码为文本
+                    try:
+                        text_data = data.decode('utf-8')
+                        print(f"文本内容: {text_data}")
+                    except:
+                        print(f"二进制数据(hex): {data[:100].hex()}...")  # 只显示前100字节
+                    
+                    print("-" * 50)
+                else:
+                    print(f"\n⚠️ 接收超时({RECEIVE_TIMEOUT}秒),继续等待...")
+                    print("-" * 50)
+        
+        except KeyboardInterrupt:
+            print("\n\n用户中断接收")
+        
+        # 关闭连接
+        print("\n" + "=" * 50)
+        print(f"【步骤7: 关闭{protocol_name}连接】")
+        print("=" * 50)
+        tcp_client.close()
+        
+        # 统计信息
+        total_time = time.time() - start_time
+        print("\n" + "=" * 50)
+        print("【连接统计】")
+        print("=" * 50)
+        print(f"连接持续时间: {total_time:.1f} 秒")
+        print(f"接收消息数量: {message_count}")
+        if message_count > 0:
+            print(f"平均消息间隔: {total_time/message_count:.1f} 秒")
+        print("=" * 50)
+        
+        return True
+        
+    except Exception as e:
+        print("\n" + "=" * 50)
+        print("【错误处理】")
+        print("=" * 50)
+        print(f"❌ 连接异常: {str(e)}")
+        import traceback
+        traceback.print_exc()
+        return False
+    finally:
+        # 断开串口连接
+        print("\n断开串口连接...")
+        at_handler.disconnect()
+
+# -------------------------- 3. 主程序 --------------------------
+if __name__ == "__main__":
+    print("=" * 50)
+    print("TCP测试服务器连接工具(AT指令版本)")
+    print("=" * 50)
+    print(f"服务器地址: {SERVER_HOST}:{SERVER_PORT}")
+    print(f"连接类型: {'TCP+SSL' if USE_SSL else 'TCP'}")
+    print(f"Socket ID: {SOCKET_ID}")
+    print("=" * 50)
+    
+    # 执行连接
+    success = connect_test_server()
+    
+    if success:
+        print("\n✅ 连接测试完成!")
+    else:
+        print("\n❌ 连接测试失败!")
+        exit(1)
+
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_download_file.py b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_download_file.py
new file mode 100644 (file)
index 0000000..edbec90
--- /dev/null
@@ -0,0 +1,257 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+使用AT指令通过HTTP/HTTPS下载文件
+使用重构后的公共模块实现
+"""
+
+import os
+import time
+from n706a_at_handler import ATCommandHandler
+from n706a_http_client import N706AHTTPClient, load_certificate_file
+
+# -------------------------- 1. 配置参数 --------------------------
+# 目标文件URL
+FILE_URL = "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/RoundEyes/Blue/Angry.mp4"
+# FILE_URL = "http://snicker.com.cn:8089/LEINIAO/blue/angry.zip"
+
+# 保存路径
+OUTPUT_DIR = "downloads"
+OUTPUT_FILENAME = "downloaded_file"
+
+# AT模块串口配置(根据实际硬件配置修改)
+SERIAL_PORT = "COM19"  # 修改为实际的串口设备
+SERIAL_BAUDRATE = 1000000
+SERIAL_TIMEOUT = 10
+
+# 认证模式配置
+AUTH_MODE = 1  # 0=无需认证, 1=单向认证, 2=双向认证
+# CA_CERT_PATH = None  # 例如: "certs/ca.crt"
+# CA_CERT_PATH = "certs/ca_chain.pem"
+CA_CERT_PATH = "certs/intermediate_cas.pem"
+
+# -------------------------- 2. 下载文件函数 --------------------------
+def download_file(url, output_path, auth_mode=0, ca_cert_path=None):
+    """
+    使用AT指令通过HTTP/HTTPS下载文件
+    
+    Args:
+        url: 文件的HTTP/HTTPS URL
+        output_path: 保存路径
+        auth_mode: 认证模式,0=无需认证, 1=单向认证, 2=双向认证(仅HTTPS需要)
+        ca_cert_path: CA证书路径(HTTPS单向认证时需要)
+    """
+    # 初始化AT指令处理器
+    at_handler = ATCommandHandler(SERIAL_PORT, SERIAL_BAUDRATE, SERIAL_TIMEOUT)
+    
+    try:
+        # 连接串口
+        if not at_handler.connect():
+            print("❌ 无法连接串口,请检查串口配置")
+            return False
+        
+        # 测试AT连接
+        success, response = at_handler.send_at_command("AT", timeout=3)
+        if not success:
+            print("❌ AT模块无响应,请检查连接")
+            return False
+        print("✅ AT模块连接正常")
+        
+        # 检查PPP拨号状态
+        print("\n" + "=" * 50)
+        print("【步骤1: 检查PPP拨号状态】")
+        print("=" * 50)
+        is_ppp_connected, ppp_ip = at_handler.check_ppp_status()
+        
+        if is_ppp_connected:
+            print(f"✅ PPP已连接,IP地址: {ppp_ip}")
+        else:
+            print(f"❌ PPP未连接,IP地址: {ppp_ip or '未知'}")
+            print("\n尝试建立PPP连接...")
+            
+            apn_name = None
+            success, ip = at_handler.establish_ppp_connection(apn_name=apn_name)
+            
+            if not success:
+                print(f"❌ PPP连接建立失败,无法继续HTTP/HTTPS访问")
+                return False
+            
+            ppp_ip = ip
+        
+        # 初始化HTTP/HTTPS客户端(自动判断协议类型)
+        http_client = N706AHTTPClient(at_handler)
+        
+        # 先设置URL以判断协议类型
+        http_client.set_url(url)
+        
+        # 处理证书(如果需要)
+        ca_cert_name = None
+        if http_client.is_https and auth_mode == 1 and ca_cert_path:
+            print("\n" + "=" * 50)
+            print("【步骤2: 处理证书】")
+            print("=" * 50)
+            
+            ca_cert_name = os.path.basename(ca_cert_path)
+            
+            # 检查证书是否已存在
+            if http_client.certificate_exists(ca_cert_name):
+                print(f"✅ 证书 {ca_cert_name} 已存在于模组中,跳过添加")
+            else:
+                # 证书不存在,需要添加
+                print(f"证书 {ca_cert_name} 不存在,开始添加...")
+                
+                # 加载证书文件
+                print(f"加载证书文件: {ca_cert_path}")
+                ca_cert_data = load_certificate_file(ca_cert_path)
+                if not ca_cert_data:
+                    print("❌ 无法加载CA证书,终止流程")
+                    return False
+                
+                # 添加证书到模组
+                print(f"添加证书到模组: {ca_cert_name}")
+                if not http_client.add_certificate(ca_cert_name, ca_cert_data):
+                    print("❌ CA证书添加失败,终止流程")
+                    return False
+                
+                # 确认证书添加成功
+                if not http_client.check_certificate(ca_cert_name):
+                    print("❌ CA证书确认失败,终止流程")
+                    return False
+                
+                print(f"✅ 证书 {ca_cert_name} 已成功添加到模组")
+        
+        # ========== 开始计时:HTTP/HTTPS访问耗时 ==========
+        access_start_time = time.time()
+        
+        # 配置HTTPS参数(如果需要)
+        # 重要:这里会明确指定使用哪个证书,确保不会混用其他证书
+        if http_client.is_https:
+            print("\n" + "=" * 50)
+            print("【步骤3: 配置HTTPS参数】")
+            print("=" * 50)
+            if ca_cert_name:
+                print(f"明确指定使用证书: {ca_cert_name}")
+            if not http_client.configure_https(
+                ssl_version=3,  # TLS1.2
+                auth_mode=auth_mode,
+                ca_cert=ca_cert_name  # 明确指定:本次HTTPS连接使用此证书
+            ):
+                print("❌ HTTPS配置失败,终止流程")
+                return False
+            
+            # 确认HTTPS配置
+            print("\n确认HTTPS配置...")
+            config = http_client.verify_https_config()
+            if config:
+                print("✅ HTTPS配置确认成功")
+            else:
+                print("⚠️ HTTPS配置确认失败,但继续执行")
+        
+        # URL已在前面设置,这里只显示信息
+        protocol_name = "HTTPS" if http_client.is_https else "HTTP"
+        print("\n" + "=" * 50)
+        print(f"【步骤{3 if not http_client.is_https else 4}: {protocol_name}请求参数已设置】")
+        print("=" * 50)
+        print(f"请求URL: {url}")
+        
+        # 建立连接
+        print("\n" + "=" * 50)
+        print(f"【步骤{4 if not http_client.is_https else 5}: 建立{protocol_name}连接】")
+        print("=" * 50)
+        http_client.setup_connection()
+        print(f"✅ {protocol_name}连接建立成功")
+        
+        # 发送GET请求
+        print("\n" + "=" * 50)
+        print(f"【步骤{5 if not http_client.is_https else 6}: 发送{protocol_name} GET请求】")
+        print("=" * 50)
+        
+        # 使用标准GET模式(不传headers参数)
+        file_data = http_client.send_get_request(headers=None, timeout=600)
+        
+        # 保存文件
+        print("\n" + "=" * 50)
+        print("【步骤7: 保存文件】")
+        print("=" * 50)
+        
+        # 创建输出目录
+        os.makedirs(os.path.dirname(output_path), exist_ok=True)
+        
+        # 写入文件
+        with open(output_path, 'wb') as f:
+            f.write(file_data)
+        
+        file_size = len(file_data)
+        print(f"✅ 文件保存成功: {output_path}")
+        print(f"   文件大小: {file_size} 字节 ({file_size/1024/1024:.2f} MB)")
+        
+        # 关闭连接
+        print("\n" + "=" * 50)
+        print(f"【步骤8: 关闭{protocol_name}连接】")
+        print("=" * 50)
+        try:
+            http_client.close_connection()
+            print(f"✅ {protocol_name}连接已关闭")
+        except Exception as e:
+            print(f"警告: 关闭{protocol_name}连接时出错: {str(e)}")
+        
+        # ========== 结束计时:HTTP/HTTPS访问耗时 ==========
+        access_end_time = time.time()
+        access_duration = access_end_time - access_start_time
+        
+        print("\n" + "=" * 50)
+        print(f"【{protocol_name}访问耗时统计】")
+        print("=" * 50)
+        print(f"{protocol_name}访问总耗时: {access_duration:.3f} 秒 ({access_duration*1000:.1f} 毫秒)")
+        print(f"下载速度: {file_size/1024/1024/access_duration:.2f} MB/s")
+        print("\n耗时包含以下步骤:")
+        if http_client.is_https:
+            print("  - HTTPS配置和验证")
+        print("  - URL和端口设置")
+        print(f"  - {protocol_name}连接建立")
+        print("  - HTTP请求发送")
+        print("  - 文件数据接收")
+        print("  - 连接关闭")
+        print("=" * 50)
+        
+        return True
+        
+    except Exception as e:
+        print("\n" + "=" * 50)
+        print("【错误处理】")
+        print("=" * 50)
+        print(f"❌ 下载异常: {str(e)}")
+        import traceback
+        traceback.print_exc()
+        return False
+    finally:
+        # 断开串口连接
+        print("\n断开串口连接...")
+        at_handler.disconnect()
+
+# -------------------------- 3. 主程序 --------------------------
+if __name__ == "__main__":
+    print("=" * 50)
+    print("HTTP/HTTPS文件下载工具(AT指令版本)")
+    print("=" * 50)
+    print(f"目标URL: {FILE_URL}")
+    
+    # 构建输出路径
+    output_path = os.path.join(OUTPUT_DIR, OUTPUT_FILENAME)
+    print(f"保存路径: {output_path}")
+    print("=" * 50)
+    
+    # 执行下载
+    success = download_file(
+        url=FILE_URL,
+        output_path=output_path,
+        auth_mode=AUTH_MODE,
+        ca_cert_path=CA_CERT_PATH
+    )
+    
+    if success:
+        print("\n✅ 文件下载完成!")
+    else:
+        print("\n❌ 文件下载失败!")
+        exit(1)
+
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_get_device_token.py b/code/sdk/zhihui/leiniao/leiniao_python_test/at_test_get_device_token.py
new file mode 100644 (file)
index 0000000..9600d56
--- /dev/null
@@ -0,0 +1,381 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+使用AT指令通过HTTPS获取设备Token
+使用重构后的公共模块实现
+"""
+
+import hashlib
+import hmac
+import random
+import string
+import json
+import time
+import os
+from n706a_at_handler import ATCommandHandler
+from n706a_http_client import N706AHTTPClient, load_certificate_file
+
+# -------------------------- 1. 接口基础配置 --------------------------
+BASE_URL = "https://ambybox-api.test.leiniao.com/ambybox-device-api"
+INTERFACE_PATH = "/v1/auth/getDeviceToken"
+FULL_URL = BASE_URL + INTERFACE_PATH
+SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus="
+
+# AT模块串口配置(根据实际硬件配置修改)
+SERIAL_PORT = "COM19"  # 修改为实际的串口设备
+SERIAL_BAUDRATE = 1000000
+SERIAL_TIMEOUT = 10
+
+# -------------------------- 2. 生成合法请求参数 --------------------------
+def generate_request_params():
+    device_id = "DEVICE_PC_TEST_001"
+    device_iccid = "86123456789012345678"
+    
+    # 1. 时间戳格式修正:确保是字符串类型的毫秒级时间戳
+    timestamp = str(int(time.time() * 1000))
+    
+    # 2. 随机数生成:确保长度在8-16位之间(当前12位符合要求)
+    nonce_length = 12
+    nonce = ''.join(random.choices(string.ascii_letters + string.digits, k=nonce_length))
+    
+    # 3. 签名生成(保持不变,符合协议要求)
+    sign_data = f"{device_id}:{device_iccid}:{timestamp}:{nonce}"
+    hmac_obj = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data.encode("utf-8"), hashlib.sha256)
+    signature = hmac_obj.hexdigest().lower()
+    
+    # 4. 返回参数(键名严格匹配文档,保持不变)
+    return {
+        "deviceId": device_id,
+        "deviceIccid": device_iccid,
+        "timestamp": timestamp,
+        "nonce": nonce,
+        "signature": signature,
+        "deviceModel": "AB101",
+        "deviceVersion": "1.0.0"
+    }
+
+# -------------------------- 3. 发送请求并解析响应 --------------------------
+def send_get_device_token_request(auth_mode=0, ca_cert_path=None, client_cert_path=None, client_key_path=None):
+    """使用AT指令发送HTTPS GET请求"""
+    
+    # 初始化AT指令处理器
+    at_handler = ATCommandHandler(SERIAL_PORT, SERIAL_BAUDRATE, SERIAL_TIMEOUT)
+    
+    try:
+        # 连接串口
+        if not at_handler.connect():
+            print("❌ 无法连接串口,请检查串口配置")
+            return
+        
+        # 测试AT连接
+        success, response = at_handler.send_at_command("AT", timeout=3)
+        if not success:
+            print("❌ AT模块无响应,请检查连接")
+            return
+        print("✅ AT模块连接正常")
+        
+        # 查询版本号
+        print("\n查询模块版本号...")
+        success, response = at_handler.send_at_command("AT+GMR", timeout=5)
+        if success:
+            print("✅ 版本信息:")
+            for line in response.split('\n'):
+                line = line.strip()
+                if line and line not in ['OK', 'AT+GMR']:
+                    print(f"  {line}")
+        else:
+            print(f"⚠️  版本查询失败: {response}")
+        
+        # 检查PPP拨号状态
+        print("\n" + "=" * 50)
+        print("【步骤1: 检查PPP拨号状态】")
+        print("=" * 50)
+        is_ppp_connected, ppp_ip = at_handler.check_ppp_status()
+        
+        if is_ppp_connected:
+            print(f"✅ PPP已连接,IP地址: {ppp_ip}")
+        else:
+            print(f"❌ PPP未连接,IP地址: {ppp_ip or '未知'}")
+            print("\n尝试建立PPP连接...")
+            
+            apn_name = None
+            success, ip = at_handler.establish_ppp_connection(apn_name=apn_name)
+            
+            if not success:
+                print("❌ PPP连接建立失败,无法继续HTTPS访问")
+                return
+            
+            ppp_ip = ip
+        
+        # 初始化HTTP/HTTPS客户端
+        http_client = N706AHTTPClient(at_handler)
+        
+        # 查询设备中已有的证书列表(可选,用于调试)
+        if auth_mode in [1, 2]:
+            print("\n" + "=" * 50)
+            print("【步骤2: 查询设备证书列表】")
+            print("=" * 50)
+            cert_list = http_client.list_certificates()
+            if cert_list:
+                print(f"设备中已有 {len(cert_list)} 个证书:")
+                for cert in cert_list:
+                    print(f"  - {cert}")
+            else:
+                print("设备中暂无证书")
+        
+        # 配置HTTPS(根据认证模式)
+        print("\n" + "=" * 50)
+        print("【步骤3: 配置HTTPS参数】")
+        print("=" * 50)
+        
+        # 根据认证模式处理证书
+        ca_cert_name = None
+        client_cert_name = None
+        client_key_name = None
+        
+        if auth_mode == 1:  # 单向认证
+            print("配置模式: 单向认证")
+            if not ca_cert_path:
+                print("❌ 错误: 单向认证需要提供CA证书路径")
+                return
+            
+            ca_cert_name = os.path.basename(ca_cert_path)
+            
+            print(f"\n检查证书是否已存在: {ca_cert_name}")
+            if http_client.certificate_exists(ca_cert_name):
+                print(f"✅ 证书 {ca_cert_name} 已存在于设备中,跳过添加步骤")
+            else:
+                print(f"证书 {ca_cert_name} 不存在,开始添加...")
+                
+                ca_cert_data = load_certificate_file(ca_cert_path)
+                if not ca_cert_data:
+                    print("❌ 无法加载CA证书,终止流程")
+                    return
+                
+                print(f"\n添加CA证书: {ca_cert_name}")
+                if not http_client.add_certificate(ca_cert_name, ca_cert_data):
+                    print("❌ CA证书添加失败,终止流程")
+                    return
+                
+                if not http_client.check_certificate(ca_cert_name):
+                    print("❌ CA证书确认失败,终止流程")
+                    return
+        
+        elif auth_mode == 2:  # 双向认证
+            print("配置模式: 双向认证")
+            if not ca_cert_path or not client_cert_path or not client_key_path:
+                print("❌ 错误: 双向认证需要提供CA证书、客户端证书和密钥路径")
+                return
+            
+            ca_cert_name = os.path.basename(ca_cert_path)
+            client_cert_name = os.path.basename(client_cert_path)
+            client_key_name = os.path.basename(client_key_path)
+            
+            # 检查并添加CA证书
+            print(f"\n检查CA证书: {ca_cert_name}")
+            if http_client.certificate_exists(ca_cert_name):
+                print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加")
+            else:
+                ca_cert_data = load_certificate_file(ca_cert_path)
+                if not ca_cert_data:
+                    return
+                if not http_client.add_certificate(ca_cert_name, ca_cert_data):
+                    return
+                if not http_client.check_certificate(ca_cert_name):
+                    return
+            
+            # 检查并添加客户端证书
+            print(f"\n检查客户端证书: {client_cert_name}")
+            if http_client.certificate_exists(client_cert_name):
+                print(f"✅ 证书 {client_cert_name} 已存在,跳过添加")
+            else:
+                client_cert_data = load_certificate_file(client_cert_path)
+                if not client_cert_data:
+                    return
+                if not http_client.add_certificate(client_cert_name, client_cert_data):
+                    return
+                if not http_client.check_certificate(client_cert_name):
+                    return
+            
+            # 检查并添加客户端密钥
+            print(f"\n检查客户端密钥: {client_key_name}")
+            if http_client.certificate_exists(client_key_name):
+                print(f"✅ 密钥 {client_key_name} 已存在,跳过添加")
+            else:
+                client_key_data = load_certificate_file(client_key_path)
+                if not client_key_data:
+                    return
+                if not http_client.add_certificate(client_key_name, client_key_data):
+                    return
+                if not http_client.check_certificate(client_key_name):
+                    return
+        else:
+            print("配置模式: 无需认证")
+        
+        # ========== 开始计时:HTTPS访问耗时 ==========
+        https_start_time = time.time()
+        
+        # 配置HTTPS参数
+        print("\n" + "=" * 50)
+        print("【步骤4: 配置HTTPS参数】")
+        print("=" * 50)
+        if not http_client.configure_https(
+            ssl_version=3,  # TLS1.2
+            auth_mode=auth_mode,
+            ca_cert=ca_cert_name,
+            client_cert=client_cert_name,
+            client_key=client_key_name
+        ):
+            print("❌ HTTPS配置失败,终止流程")
+            return
+        
+        # 确认HTTPS配置
+        print("\n确认HTTPS配置...")
+        config = http_client.verify_https_config()
+        if config:
+            print("✅ HTTPS配置确认成功")
+        else:
+            print("⚠️ HTTPS配置确认失败,但继续执行")
+        
+        # 设置URL
+        print("\n" + "=" * 50)
+        print("【步骤5: 设置HTTPS请求参数】")
+        print("=" * 50)
+        print(f"设置请求URL: {FULL_URL}")
+        http_client.set_url(FULL_URL)
+        
+        # 建立HTTPS连接
+        print("\n" + "=" * 50)
+        print("【步骤6: 建立HTTPS连接】")
+        print("=" * 50)
+        http_client.setup_connection()
+        print("✅ HTTPS连接建立成功")
+        
+        # 生成请求参数
+        params = generate_request_params()
+        
+        # 构建HTTP头
+        headers = {}
+        for key, value in params.items():
+            headers[key] = str(value)
+        headers["Content-Type"] = "application/json"
+        
+        print("\n" + "=" * 50)
+        print("【步骤7: 发送HTTPS GET请求】")
+        print("=" * 50)
+        print("【请求信息】")
+        print(f"请求 URL: {FULL_URL}")
+        print(f"请求头: {headers}")
+        print("=" * 50)
+        
+        # 发送请求(获取完整响应,包括响应头和响应体)
+        try:
+            full_response, header_text, body_data = http_client._send_https_get_request(headers=headers)
+        except Exception as e:
+            print(f"❌ HTTPS请求发送失败: {str(e)}")
+            raise
+        
+        # 解析响应(文本模式)
+        print("\n" + "=" * 50)
+        print("【步骤8: 解析HTTPS响应】")
+        print("=" * 50)
+        try:
+            # 使用响应头和响应体解析
+            parsed_response = http_client.parse_http_response(None, header_text=header_text, body_data=body_data)
+        except Exception as e:
+            print(f"❌ HTTP响应解析失败: {str(e)}")
+            print(f"响应体数据: {body_data[:500] if body_data else 'None'}...")
+            raise
+        
+        print(f"HTTP状态码: {parsed_response['status_code']}")
+        print(f"响应头: {parsed_response['headers']}")
+        print(f"响应体: {parsed_response['body']}")
+        
+        # 解析JSON响应
+        print("\n" + "=" * 50)
+        print("【步骤9: 解析JSON响应】")
+        print("=" * 50)
+        try:
+            response_data = json.loads(parsed_response['body'])
+            print("\n【JSON响应】")
+            print(json.dumps(response_data, ensure_ascii=False, indent=2))
+            
+            if response_data.get("errorCode") is None:
+                print("\n✅ 请求成功!")
+                if "data" in response_data:
+                    print(f"设备 Token: {response_data['data'].get('token')}")
+                    print(f"Token 过期时间: {response_data['data'].get('expireTime')} 秒({response_data['data'].get('expireTime', 0)//86400} 天)")
+            else:
+                print(f"\n❌ 请求失败!错误码: {response_data['errorCode']}, 原因: {response_data.get('message', '未知错误')}")
+        except json.JSONDecodeError as e:
+            print(f"\n❌ JSON解析失败: {e}")
+            print(f"原始响应体: {parsed_response['body']}")
+            raise
+        
+        # 关闭连接
+        print("\n" + "=" * 50)
+        print("【步骤10: 关闭HTTPS连接】")
+        print("=" * 50)
+        try:
+            http_client.close_connection()
+            print("✅ HTTPS连接已关闭")
+        except Exception as e:
+            print(f"警告: 关闭HTTPS连接时出错: {str(e)}")
+        
+        # ========== 结束计时:HTTPS访问耗时 ==========
+        https_end_time = time.time()
+        https_duration = https_end_time - https_start_time
+        
+        print("\n" + "=" * 50)
+        print("【HTTPS访问耗时统计】")
+        print("=" * 50)
+        print(f"HTTPS访问总耗时: {https_duration:.3f} 秒 ({https_duration*1000:.1f} 毫秒)")
+        print("\n耗时包含以下步骤:")
+        print("  - HTTPS配置和验证")
+        print("  - URL和端口设置")
+        print("  - HTTPS连接建立")
+        print("  - HTTP请求发送")
+        print("  - HTTP响应接收")
+        print("  - 响应解析")
+        print("  - 连接关闭")
+        print("=" * 50)
+        
+    except Exception as e:
+        print("\n" + "=" * 50)
+        print("【错误处理】")
+        print("=" * 50)
+        print(f"❌ 请求异常: {str(e)}")
+        import traceback
+        traceback.print_exc()
+    finally:
+        # 断开串口连接
+        print("\n断开串口连接...")
+        at_handler.disconnect()
+
+# -------------------------- 4. 执行请求 --------------------------
+if __name__ == "__main__":
+    # 认证模式配置
+    # auth_mode: 0=无需认证, 1=单向认证, 2=双向认证
+    # AUTH_MODE = 0  # 默认无需认证
+    
+    # 证书文件路径(单向/双向认证时需要)
+    # CA_CERT_PATH = None  # 例如: "certs/ca.crt"
+    CLIENT_CERT_PATH = None  # 例如: "certs/client.crt"
+    CLIENT_KEY_PATH = None  # 例如: "certs/client.key"
+    
+    # 如果使用单向认证,取消下面的注释并设置证书路径
+    AUTH_MODE = 1
+    CA_CERT_PATH = "certs/ca_chain.pem"
+    
+    # 如果使用双向认证,取消下面的注释并设置所有证书路径
+    # AUTH_MODE = 2
+    # CA_CERT_PATH = "certs/ca.crt"
+    # CLIENT_CERT_PATH = "certs/client.crt"
+    # CLIENT_KEY_PATH = "certs/client.key"
+    
+    send_get_device_token_request(
+        auth_mode=AUTH_MODE,
+        ca_cert_path=CA_CERT_PATH,
+        client_cert_path=CLIENT_CERT_PATH,
+        client_key_path=CLIENT_KEY_PATH
+    )
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/atcmd.txt b/code/sdk/zhihui/leiniao/leiniao_python_test/atcmd.txt
new file mode 100644 (file)
index 0000000..3d7c534
--- /dev/null
@@ -0,0 +1,586 @@
+at+clac\r
+\r
+AT+CFUN\r
+\r
+AT+CSQ\r
+\r
+AT+COPS\r
+\r
+AT+CGATT\r
+\r
+AT+CTZR\r
+\r
+AT+CCLK\r
+\r
+AT+CEREG\r
+\r
+AT+CPLS\r
+\r
+AT+CPOL\r
+\r
+AT+COPN\r
+\r
+AT+NPSMR\r
+\r
+AT+NPSMSET\r
+\r
+AT+NEARFCN\r
+\r
+AT+CPSMS\r
+\r
+AT+NBAND\r
+\r
+AT+CNEC\r
+\r
+AT+CTZU\r
+\r
+AT+NDRX\r
+\r
+AT+CSCON\r
+\r
+AT+NCSEARFCN\r
+\r
+AT+NUESTATS\r
+\r
+AT+NPOWD\r
+\r
+AT+CREG\r
+\r
+AT+CGREG\r
+\r
+AT+NSQ\r
+\r
+AT+NNMPSD\r
+\r
+AT+NGSBLACKCELL\r
+\r
+AT+NCSBLACKCELL\r
+\r
+AT+NBACKOFF\r
+\r
+AT+NROAMING\r
+\r
+AT+NRESELCFG\r
+\r
+AT+NSIMAC\r
+\r
+AT+NDRXS\r
+\r
+AT+CGTFT\r
+\r
+AT+CGPADDR\r
+\r
+AT+CGDCONT\r
+\r
+AT+CGACT\r
+\r
+AT+CGCMOD\r
+\r
+AT+CGDSCONT\r
+\r
+AT+CGTFTRDP\r
+\r
+AT+CGEQOSRDP\r
+\r
+AT+CGAUTH\r
+\r
+AT+CGEQOS\r
+\r
+AT+CEER\r
+\r
+AT+CGCONTRDP\r
+\r
+AT+NCONFIG\r
+\r
+AT+CIMI\r
+\r
+AT+CGLA\r
+\r
+AT+CRSM\r
+\r
+AT+CSIM\r
+\r
+AT+CCHC\r
+\r
+AT+CCHO\r
+\r
+AT+USIMHOTPLUG\r
+\r
+AT+NUICC\r
+\r
+AT+ICCID\r
+\r
+AT+PHONENUM\r
+\r
+AT+NSIMSRS\r
+\r
+AT+CLEARFPLMN\r
+\r
+AT+USIMRETRYTIMES\r
+\r
+AT+CPIN\r
+\r
+AT+CLCK\r
+\r
+AT+CPWD\r
+\r
+AT+NPIN\r
+\r
+AT+SETHWLOCK\r
+\r
+AT+CARDLOCK\r
+\r
+AT+SIMLOCK\r
+\r
+AT+CPINR\r
+\r
+AT+CMEE\r
+\r
+AT+CGMI\r
+\r
+AT+CGSN\r
+\r
+AT+CLAC\r
+\r
+AT+RESET\r
+\r
+AT+NLOGLEVEL\r
+\r
+AT+NATSPEED\r
+\r
+AT+NFWUPD\r
+\r
+AT+NDSDELAY\r
+\r
+AT+NCRASHINFO\r
+\r
+AT+NTSETID\r
+\r
+AT+NTPERMID\r
+\r
+AT+NCHIPTYPE\r
+\r
+AT+NQSTATE\r
+\r
+AT+USIMSEL\r
+\r
+AT+NVETOSUPERDS\r
+\r
+AT+NBANKD\r
+\r
+AT+NVER\r
+\r
+AT+MODEMTEMP\r
+\r
+AT+NUSBPSM\r
+\r
+AT+NRATEMODE\r
+\r
+AT+NFLIST\r
+\r
+AT+RECONNDEL\r
+\r
+AT+NRCUPD\r
+\r
+AT+NLOGMODFT\r
+\r
+AT+NRDCTRL\r
+\r
+AT+NRDSET\r
+\r
+AT+NRDEXEC\r
+\r
+AT+NGOLD\r
+\r
+AT+CSCA\r
+\r
+AT+CNMI\r
+\r
+AT+CMGS\r
+\r
+AT+CMMS\r
+\r
+AT+CMGF\r
+\r
+AT+CSMP\r
+\r
+AT+CSDH\r
+\r
+AT+CMGR\r
+\r
+AT+CMGL\r
+\r
+AT+CMGD\r
+\r
+AT+CMGW\r
+\r
+AT+CPMS\r
+\r
+AT+CMSS\r
+\r
+AT+CSCS\r
+\r
+AT+NDISDUP\r
+\r
+AT+NPING\r
+\r
+AT+NDNS\r
+\r
+AT+NSOCR\r
+\r
+AT+NSOST\r
+\r
+AT+NSOCO\r
+\r
+AT+NSOSD\r
+\r
+AT+NQSOS\r
+\r
+AT+NSORF\r
+\r
+AT+NSOCL\r
+\r
+AT+NSOSTATUS\r
+\r
+AT+NCIDSTATUS\r
+\r
+AT+NDATATRAN\r
+\r
+AT+NIPINFO\r
+\r
+AT+NKEEPALIVE\r
+\r
+AT+NISEND\r
+\r
+AT+NDATARATE\r
+\r
+AT+NFCMEMBYTE\r
+\r
+AT+TTSBATLV\r
+\r
+AT+TTSAMOUNT\r
+\r
+AT+TTSPRESET\r
+\r
+AT+NRDTEST\r
+\r
+AT+NWIFISCAN\r
+\r
+AT+NWIFISCANSTOP\r
+\r
+AT+NTXPOWER\r
+\r
+AT+DEMO\r
+\r
+AT+IVSN\r
+\r
+AT+GMR\r
+\r
+AT+FTCAL\r
+\r
+AT+FTRF\r
+\r
+AT+IFC\r
+\r
+AT+ICF\r
+\r
+AT+IPR\r
+\r
+AT+NWIDCFG\r
+\r
+AT+DUMP\r
+\r
+AT+CERTADD\r
+\r
+AT+CERTCHECK\r
+\r
+AT+CERTDEL\r
+\r
+AT+IPINFO\r
+\r
+AT+IPSTATUS\r
+\r
+AT+NCUSTSWITCH\r
+\r
+AT+PDPKEEPALIVE\r
+\r
+AT+PDPSTATUS\r
+\r
+AT+RECVMODE\r
+\r
+AT+TCPACK\r
+\r
+AT+TCPCLOSE\r
+\r
+AT+TCPKEEPALIVE\r
+\r
+AT+TCPREAD\r
+\r
+AT+TCPSEND\r
+\r
+AT+TCPSETUP\r
+\r
+AT+TCPTRANS\r
+\r
+AT+TRANSCLOSE\r
+\r
+AT+UDPCLOSE\r
+\r
+AT+UDPREAD\r
+\r
+AT+UDPSEND\r
+\r
+AT+UDPSETUP\r
+\r
+AT+UDPTRANS\r
+\r
+AT+CESQ\r
+\r
+AT+CPSERVICE\r
+\r
+AT+ENPWRSAVE\r
+\r
+AT+IMEI\r
+\r
+AT+NFTCHIPTYPE\r
+\r
+AT+NFTFBACKUP\r
+\r
+AT+NFTIMEI\r
+\r
+AT+NFTRCAL\r
+\r
+AT+NFTRCREG\r
+\r
+AT+NFTRCSQ\r
+\r
+AT+NFTRFBAK\r
+\r
+AT+NFTRFIT\r
+\r
+AT+NWENPWRSAVE\r
+\r
+AT+NWPWROFF\r
+\r
+AT+QCNVERSION\r
+\r
+AT+READADC\r
+\r
+AT+CCID\r
+\r
+AT+CNUM\r
+\r
+AT+NWCCID\r
+\r
+AT+NWSIMCFG\r
+\r
+AT+NWSIMDET\r
+\r
+AT+NWSIMWRCNT\r
+\r
+AT+NWSIMWRCNTCTL\r
+\r
+AT+SIMCOUNTCTL\r
+\r
+AT+SIMCOUNTREAD\r
+\r
+AT+SIMCROSS\r
+\r
+AT+SIMHOTSWAP\r
+\r
+AT+NETSHAREACT\r
+\r
+AT+XIIC\r
+\r
+AT+CGMM\r
+\r
+AT+CGMR\r
+\r
+AT+GMM\r
+\r
+AT+GSN\r
+\r
+AT+HEAPINFO\r
+\r
+AT+NETSHAREMODE\r
+\r
+AT+RSPACK\r
+\r
+AT+SIGNAL\r
+\r
+AT+BANDLOCK\r
+\r
+AT+MYCELLINFO\r
+\r
+AT+MYCGED\r
+\r
+AT+MYLACID\r
+\r
+AT+MYNETINFO\r
+\r
+AT+MYSYSINFO\r
+\r
+AT+MYSYSINFOURC\r
+\r
+AT+MYURCSYSINFO\r
+\r
+AT+NBANDLOCK\r
+\r
+AT+NETDMSG\r
+\r
+AT+NETMSG\r
+\r
+AT+NFREQLOCK\r
+\r
+AT+NRSP\r
+\r
+AT+NSTGETRSSI\r
+\r
+AT+NWENG\r
+\r
+AT+NWRFTEST\r
+\r
+AT+NWRRCRELEASE\r
+\r
+AT+NWUEDRX\r
+\r
+AT+MQTTCONN\r
+\r
+AT+MQTTCONNPARAM\r
+\r
+AT+MQTTDISCONN\r
+\r
+AT+MQTTMUX\r
+\r
+AT+MQTTPUB\r
+\r
+AT+MQTTPUBS\r
+\r
+AT+MQTTSTATE\r
+\r
+AT+MQTTSUB\r
+\r
+AT+MQTTTLS\r
+\r
+AT+MQTTUNSUB\r
+\r
+AT+MQTTWILLMSG\r
+\r
+AT+MQTTWILLPARAM\r
+\r
+AT+SSLCIPHERSET\r
+\r
+AT+SSLTCPCFG\r
+\r
+AT+SSLTCPCFGA\r
+\r
+AT+SSLTCPCLOSE\r
+\r
+AT+SSLTCPREAD\r
+\r
+AT+SSLTCPSEND\r
+\r
+AT+SSLTCPSETUP\r
+\r
+AT+FILEHTTPACTION\r
+\r
+AT+FILEHTTPSACTION\r
+\r
+AT+HTTPACTION\r
+\r
+AT+HTTPCLOSE\r
+\r
+AT+HTTPGET\r
+\r
+AT+HTTPGETSTAT\r
+\r
+AT+HTTPPARA\r
+\r
+AT+HTTPSACTION\r
+\r
+AT+HTTPSCFG\r
+\r
+AT+HTTPSCLOSE\r
+\r
+AT+HTTPSETUP\r
+\r
+AT+HTTPSGET\r
+\r
+AT+HTTPSGETSTAT\r
+\r
+AT+HTTPSPARA\r
+\r
+AT+HTTPSSETUP\r
+\r
+AT+FSDF\r
+\r
+AT+FSFAT\r
+\r
+AT+FSFS\r
+\r
+AT+FSLIST\r
+\r
+AT+FSLS\r
+\r
+AT+FSRF\r
+\r
+AT+FSRFEX\r
+\r
+AT+FSRN\r
+\r
+AT+FSWF\r
+\r
+AT+NWATFOTA\r
+\r
+AT+NWFOTA\r
+\r
+AT+NWFOTASCFG\r
+\r
+AT+NWFTPFOTA\r
+\r
+AT+FILEFTPGET\r
+\r
+AT+FTPGET\r
+\r
+AT+FTPLOGI\r
+N\r
+\r
+AT+FTPLOGOUT\r
+\r
+AT+FTPPUT\r
+\r
+AT+FTPSIZE\r
+\r
+AT+FTPSTATUS\r
+\r
+AT+DNSSERVER\r
+\r
+AT+NWDNS\r
+\r
+AT+PING\r
+\r
+AT+UPDATETIME\r
+\r
+AT+CIPGSMLOC\r
+\r
+AT+NWURCCFG\r
+\r
+AT&F\r
+\r
+AT&W\r
+\r
+AT&V\r
+\r
+ATD\r
+\r
+ATH\r
+\r
+ATS0\r
+\r
+ATE\r
+\r
+ATI\r
+\r
+ATO\r
+\r
+OK
\ No newline at end of file
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certificate_storage_and_usage.md b/code/sdk/zhihui/leiniao/leiniao_python_test/certificate_storage_and_usage.md
new file mode 100644 (file)
index 0000000..12e7350
--- /dev/null
@@ -0,0 +1,150 @@
+# 证书存储和使用机制说明\r
+\r
+## 证书存储方式\r
+\r
+### 1. 存储机制\r
+- **存储方式**:通过 `AT+CERTADD=<file_name>,<length>` 添加证书\r
+- **存储位置**:证书存储在模组的非易失性存储器中\r
+- **存储格式**:以**文件名**作为唯一标识符存储证书内容\r
+- **持久化**:证书会持久保存,即使模组重启也不会丢失\r
+\r
+### 2. 证书数量\r
+- 模组可以存储**多个证书**(具体数量限制取决于模组型号和存储空间)\r
+- 每个证书通过**唯一的文件名**区分\r
+- 可以通过 `AT+CERTCHECK?` 查询所有已存储的证书列表\r
+\r
+### 3. 证书管理\r
+- **添加证书**:`AT+CERTADD=<file_name>,<length>`\r
+- **检查证书**:`AT+CERTCHECK=<file_name>` 或 `AT+CERTCHECK?`\r
+- **删除证书**:`AT+CERTDEL=<file_name>`\r
+\r
+## HTTPS下载时证书的指定方式\r
+\r
+### 关键机制:每次连接前明确指定证书\r
+\r
+**重要**:HTTPS下载时,**不会混用证书**,因为每次连接前都会**明确指定**使用哪个证书。\r
+\r
+### 工作流程\r
+\r
+1. **添加证书到模组**(如果不存在)\r
+   ```python\r
+   http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+   ```\r
+   - 执行:`AT+CERTADD=<cert_name>,<length>`\r
+   - 作用:将证书存储到模组中\r
+\r
+2. **配置HTTPS使用指定证书**(每次连接前)\r
+   ```python\r
+   http_client.configure_https(\r
+       ssl_version=3,\r
+       auth_mode=1,  # 单向认证\r
+       ca_cert=ca_cert_name  # 明确指定证书名称\r
+   )\r
+   ```\r
+   - 执行:`AT+HTTPSCFG="cacert",<cert_name>`\r
+   - 作用:**明确指定**本次HTTPS连接使用哪个证书\r
+\r
+3. **建立HTTPS连接**\r
+   - 模组会使用步骤2中配置的证书来验证服务器证书\r
+   - **不会使用其他证书**\r
+\r
+### 证书配置的作用范围\r
+\r
+- **会话级别**:`AT+HTTPSCFG="cacert"` 的配置是**会话级别的**\r
+- **每次连接都需要重新配置**:每次建立新的HTTPS连接前,都需要重新调用 `configure_https()`\r
+- **不会影响其他证书**:配置只影响当前HTTPS会话,不会影响模组中存储的其他证书\r
+\r
+## 为什么不会混用证书?\r
+\r
+### 原因1:明确指定机制\r
+```python\r
+# 每次HTTPS连接前都会执行\r
+AT+HTTPSCFG="cacert",<cert_name>  # 明确指定使用哪个证书\r
+```\r
+\r
+### 原因2:会话隔离\r
+- 每个HTTPS连接都是独立的会话\r
+- 每个会话都会明确指定使用的证书\r
+- 不同会话之间不会互相影响\r
+\r
+### 原因3:配置覆盖\r
+- 如果之前配置过其他证书,新的配置会**覆盖**之前的配置\r
+- 但只影响**当前会话**,不会影响模组中存储的证书\r
+\r
+## 实际代码流程\r
+\r
+```python\r
+# 1. 添加证书(如果不存在)\r
+if not http_client.certificate_exists(ca_cert_name):\r
+    http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+\r
+# 2. 配置HTTPS使用指定证书(每次连接前都会执行)\r
+http_client.configure_https(\r
+    ssl_version=3,\r
+    auth_mode=1,\r
+    ca_cert=ca_cert_name  # 明确指定:使用这个证书\r
+)\r
+\r
+# 3. 建立HTTPS连接\r
+# 模组会使用步骤2中指定的证书来验证服务器\r
+http_client.setup_connection()\r
+http_client.send_get_request()\r
+```\r
+\r
+## 关于删除证书的必要性\r
+\r
+### 如果证书名称不同\r
+- **不需要删除**:因为每个证书都有唯一的名称\r
+- **不会混用**:因为每次都会明确指定使用哪个证书\r
+\r
+### 如果证书名称相同\r
+- **理论上不需要删除**:如果保证同一个名称的证书内容相同\r
+- **但建议删除**:如果证书内容可能不同,建议先删除再添加,确保使用最新证书\r
+\r
+### 当前代码的处理方式\r
+\r
+```python\r
+# 当前代码:如果证书已存在,先删除再添加\r
+if http_client.certificate_exists(ca_cert_name):\r
+    http_client.delete_certificate(ca_cert_name)\r
+\r
+http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+```\r
+\r
+**对于您的场景**(每个证书名称都不同,且保证同名证书内容相同):\r
+- **可以优化**:如果证书已存在且内容相同,可以跳过添加步骤\r
+- **不会混用**:因为每次都会通过 `configure_https(ca_cert=ca_cert_name)` 明确指定\r
+\r
+## 建议的优化方案\r
+\r
+### 方案1:检查证书是否存在,存在则跳过(推荐)\r
+\r
+```python\r
+# 如果证书已存在,跳过添加(假设同名证书内容相同)\r
+if http_client.certificate_exists(ca_cert_name):\r
+    print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加")\r
+else:\r
+    # 只有不存在时才添加\r
+    http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+    http_client.check_certificate(ca_cert_name)\r
+\r
+# 无论证书是否已存在,都需要配置HTTPS使用该证书\r
+http_client.configure_https(\r
+    ssl_version=3,\r
+    auth_mode=1,\r
+    ca_cert=ca_cert_name  # 明确指定使用这个证书\r
+)\r
+```\r
+\r
+### 方案2:验证证书内容(如果模组支持)\r
+\r
+如果模组支持验证证书内容,可以比较证书内容是否相同,但通常不需要这么复杂。\r
+\r
+## 总结\r
+\r
+1. **证书存储**:模组可以存储多个证书,每个证书通过文件名唯一标识\r
+2. **证书指定**:每次HTTPS连接前,通过 `AT+HTTPSCFG="cacert",<cert_name>` 明确指定使用哪个证书\r
+3. **不会混用**:因为每次都会明确指定,不会使用其他证书\r
+4. **删除必要性**:如果证书名称不同,不需要删除;如果名称相同但内容可能不同,建议先删除再添加\r
+5. **优化建议**:如果保证同名证书内容相同,可以检查存在后跳过添加,但**必须**每次都调用 `configure_https()` 明确指定证书\r
+\r
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certificate_usage_guide.md b/code/sdk/zhihui/leiniao/leiniao_python_test/certificate_usage_guide.md
new file mode 100644 (file)
index 0000000..56a8026
--- /dev/null
@@ -0,0 +1,176 @@
+# AT指令HTTP/HTTPS证书使用指南\r
+\r
+## 证书管理AT指令\r
+\r
+### 1. 添加证书:`AT+CERTADD`\r
+```\r
+AT+CERTADD=<file_name>,<length>\r
+```\r
+- 发送后会返回 `CONNECT`\r
+- 然后发送证书数据(二进制)\r
+- 最后返回 `+CERTADD: <length>,OK` 或 `+CERTADD: ERROR`\r
+\r
+### 2. 检查证书:`AT+CERTCHECK`\r
+```\r
+AT+CERTCHECK=<file_name>     # 检查指定证书是否存在\r
+AT+CERTCHECK?                # 列出所有证书\r
+```\r
+\r
+### 3. 删除证书:`AT+CERTDEL`\r
+```\r
+AT+CERTDEL=<file_name>       # 删除指定证书\r
+```\r
+\r
+## 证书使用流程\r
+\r
+### 当前实现方式\r
+\r
+在 `at_test_download_file.py` 中,证书添加流程如下:\r
+\r
+1. **检查证书是否存在**(可选)\r
+   ```python\r
+   if http_client.certificate_exists(ca_cert_name):\r
+       print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加")\r
+   ```\r
+\r
+2. **添加证书**\r
+   ```python\r
+   http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+   ```\r
+\r
+3. **确认证书**\r
+   ```python\r
+   http_client.check_certificate(ca_cert_name)\r
+   ```\r
+\r
+4. **配置HTTPS使用证书**\r
+   ```python\r
+   http_client.configure_https(\r
+       ssl_version=3,\r
+       auth_mode=1,  # 单向认证\r
+       ca_cert=ca_cert_name\r
+   )\r
+   ```\r
+\r
+## 关于证书覆盖的问题\r
+\r
+### 问题分析\r
+\r
+**如果使用相同的证书名称添加证书:**\r
+- 模组可能会**覆盖**旧证书\r
+- 但为了**确保验证的是当前证书**,建议先删除旧证书\r
+\r
+### 为什么需要删除旧证书?\r
+\r
+1. **确保证书一致性**:删除旧证书可以确保模组中只有最新的证书\r
+2. **避免证书冲突**:如果证书内容不同但名称相同,可能导致验证失败\r
+3. **清理不需要的证书**:删除不再使用的证书,释放存储空间\r
+\r
+## 推荐的最佳实践\r
+\r
+### 方案1:先删除再添加(推荐)\r
+\r
+```python\r
+# 1. 检查证书是否存在\r
+if http_client.certificate_exists(ca_cert_name):\r
+    print(f"证书 {ca_cert_name} 已存在,先删除旧证书...")\r
+    http_client.delete_certificate(ca_cert_name)\r
+\r
+# 2. 添加新证书\r
+http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+\r
+# 3. 确认证书\r
+http_client.check_certificate(ca_cert_name)\r
+```\r
+\r
+### 方案2:使用唯一证书名称\r
+\r
+```python\r
+# 使用带时间戳或版本的证书名称\r
+import time\r
+ca_cert_name = f"ca_{int(time.time())}.crt"\r
+# 或\r
+ca_cert_name = f"ca_v1.crt"\r
+```\r
+\r
+### 方案3:列出所有证书并清理\r
+\r
+```python\r
+# 列出所有证书\r
+cert_list = http_client.list_certificates()\r
+print(f"当前证书列表: {cert_list}")\r
+\r
+# 删除不需要的证书\r
+for old_cert in old_cert_list:\r
+    http_client.delete_certificate(old_cert)\r
+\r
+# 添加新证书\r
+http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+```\r
+\r
+## 证书验证机制\r
+\r
+### HTTPS单向认证流程\r
+\r
+1. **客户端(模组)** 使用 `AT+HTTPSCFG="cacert",<cert_name>` 配置CA证书\r
+2. **建立HTTPS连接** 时,模组会:\r
+   - 接收服务器的证书\r
+   - 使用配置的CA证书验证服务器证书\r
+   - 如果验证通过,建立连接\r
+   - 如果验证失败,返回错误\r
+\r
+### 证书选择\r
+\r
+- 模组会使用 `AT+HTTPSCFG="cacert"` 中指定的证书名称\r
+- 如果该证书不存在,HTTPS连接会失败\r
+- **如果模组中有多个证书,只有配置的证书会被使用**\r
+\r
+## 注意事项\r
+\r
+1. **证书名称唯一性**:如果使用相同的证书名称,新证书会覆盖旧证书\r
+2. **证书存储限制**:模组可能有证书存储数量限制\r
+3. **证书大小限制**:单个证书可能有大小限制\r
+4. **证书格式**:必须是PEM格式的证书\r
+\r
+## 建议的代码改进\r
+\r
+在 `n706a_http_client.py` 中添加删除证书的方法:\r
+\r
+```python\r
+def delete_certificate(self, cert_name):\r
+    """\r
+    删除SSL证书\r
+    \r
+    Args:\r
+        cert_name: 证书文件名\r
+        \r
+    Returns:\r
+        bool: 是否删除成功\r
+    """\r
+    success, response = self.at.send_at_command(\r
+        f'AT+CERTDEL={cert_name}',\r
+        timeout=5\r
+    )\r
+    if not success:\r
+        return False\r
+    \r
+    if "OK" in response:\r
+        print(f"✅ 证书 {cert_name} 已删除")\r
+        return True\r
+    else:\r
+        print(f"❌ 证书删除失败: {response}")\r
+        return False\r
+```\r
+\r
+然后在添加证书前先删除旧证书:\r
+\r
+```python\r
+# 如果证书已存在,先删除\r
+if http_client.certificate_exists(ca_cert_name):\r
+    print(f"证书 {ca_cert_name} 已存在,先删除...")\r
+    http_client.delete_certificate(ca_cert_name)\r
+\r
+# 添加新证书\r
+http_client.add_certificate(ca_cert_name, ca_cert_data)\r
+```\r
+\r
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/.gitignore b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/.gitignore
new file mode 100644 (file)
index 0000000..d7d4290
--- /dev/null
@@ -0,0 +1,18 @@
+# 证书文件(包含敏感信息,不应提交到版本控制)\r
+*.crt\r
+*.key\r
+# *.pem\r
+*.der\r
+*.csr\r
+*.srl\r
+\r
+# 但保留示例和说明文件\r
+!README.md\r
+!certificate_preparation_guide.md\r
+!generate_test_certs.sh\r
+!download_server_ca.py\r
+!check_certificate.py\r
+!extract_ca_from_chain.py\r
+!verify_ca_for_https.py\r
+!ca_cert_usage_explanation.md\r
+\r
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/ca_cert_usage_explanation.md b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/ca_cert_usage_explanation.md
new file mode 100644 (file)
index 0000000..6bcbf32
--- /dev/null
@@ -0,0 +1,157 @@
+# CA证书使用说明:服务器证书 vs CA证书\r
+\r
+## 问题:直接拿服务器的ca.crt可以用到client上吗?\r
+\r
+**简短回答**:**可以,但需要确认证书类型**。\r
+\r
+## 详细说明\r
+\r
+### 1. 证书类型区分\r
+\r
+#### CA证书(Certificate Authority Certificate)\r
+- **用途**:用于验证其他证书的有效性\r
+- **特点**:是根证书或中间证书,用于验证服务器证书是否由可信CA签发\r
+- **客户端使用**:✅ **可以直接使用**,用于单向认证验证服务器身份\r
+\r
+#### 服务器证书(Server Certificate)\r
+- **用途**:服务器自己的身份证书\r
+- **特点**:由CA签发的终端实体证书\r
+- **客户端使用**:❌ **不能直接使用**,但可以从中提取CA证书链\r
+\r
+### 2. 如何判断证书类型\r
+\r
+#### 方法1:查看证书主题(Subject)\r
+\r
+```bash\r
+openssl x509 -in ca.crt -noout -subject -issuer\r
+```\r
+\r
+**CA证书特征**:\r
+- Subject 和 Issuer **相同**(自签名)\r
+- 通常包含 "CA"、"Certificate Authority" 等字样\r
+- CN(Common Name)通常是 "Root CA"、"Intermediate CA" 等\r
+\r
+**服务器证书特征**:\r
+- Subject 和 Issuer **不同**\r
+- Subject 通常是服务器域名(如 `ambybox-api.test.leiniao.com`)\r
+- Issuer 是CA的名称\r
+\r
+#### 方法2:查看证书扩展\r
+\r
+```bash\r
+openssl x509 -in ca.crt -noout -text | grep -A 5 "X509v3 extensions"\r
+```\r
+\r
+**CA证书**:通常包含 `Basic Constraints: CA:TRUE`\r
+**服务器证书**:通常包含 `Basic Constraints: CA:FALSE`\r
+\r
+### 3. 使用场景\r
+\r
+#### 场景A:服务器提供的是CA证书 ✅\r
+\r
+如果服务器提供的 `ca.crt` 是真正的CA证书(根证书或中间证书),那么:\r
+\r
+```python\r
+# 可以直接使用\r
+AUTH_MODE = 1\r
+CA_CERT_PATH = "certs/ca.crt"  # 直接使用\r
+```\r
+\r
+**这是最常见的情况**,因为:\r
+- 客户端需要CA证书来验证服务器证书\r
+- 服务器通常会提供其证书链中的CA证书\r
+\r
+#### 场景B:服务器提供的是服务器证书 ⚠️\r
+\r
+如果服务器提供的是服务器自己的证书(不是CA证书),需要:\r
+\r
+**选项1:提取CA证书链**\r
+\r
+```bash\r
+# 使用OpenSSL获取完整证书链(包括CA证书)\r
+openssl s_client -showcerts -connect ambybox-api.test.leiniao.com:443 </dev/null 2>/dev/null | \\r
+  openssl x509 -outform PEM > server_chain.pem\r
+\r
+# 从证书链中提取CA证书(通常是第二个或最后一个证书)\r
+openssl x509 -in server_chain.pem -outform PEM -out ca.crt\r
+```\r
+\r
+**选项2:从服务器管理员获取CA证书**\r
+\r
+联系服务器管理员,获取用于验证服务器证书的CA证书。\r
+\r
+### 4. 实际使用建议\r
+\r
+#### 推荐方法:从服务器下载完整证书链\r
+\r
+使用提供的脚本:\r
+\r
+```bash\r
+cd leiniao/leiniao_python_test/certs\r
+python3 download_server_ca.py https://ambybox-api.test.leiniao.com -o ca.crt\r
+```\r
+\r
+这个脚本会:\r
+1. 连接到服务器\r
+2. 获取服务器证书\r
+3. 提取CA证书链\r
+4. 保存为 `ca.crt`\r
+\r
+#### 验证证书是否可用\r
+\r
+```bash\r
+# 检查证书类型\r
+openssl x509 -in ca.crt -noout -subject -issuer\r
+\r
+# 验证证书格式\r
+python3 check_certificate.py ca.crt\r
+```\r
+\r
+### 5. 常见问题\r
+\r
+#### Q1: 服务器给的ca.crt是服务器证书怎么办?\r
+\r
+**A**: 需要提取CA证书链,或联系服务器管理员获取真正的CA证书。\r
+\r
+#### Q2: 如何确认证书是否可用?\r
+\r
+**A**: \r
+1. 检查证书的 Subject 和 Issuer\r
+2. 如果 Subject == Issuer,通常是CA证书,可以直接使用\r
+3. 如果 Subject != Issuer,可能是服务器证书,需要提取CA证书\r
+\r
+#### Q3: 证书链中有多个证书怎么办?\r
+\r
+**A**: \r
+- 对于单向认证,通常只需要**根CA证书**(最后一个证书)\r
+- 某些情况下可能需要**中间CA证书**\r
+- 可以尝试使用根CA证书,如果验证失败,再添加中间CA证书\r
+\r
+### 6. 代码中的使用\r
+\r
+```python\r
+# 在 get_device_token_at.py 中\r
+if __name__ == "__main__":\r
+    # 单向认证配置\r
+    AUTH_MODE = 1\r
+    CA_CERT_PATH = "certs/ca.crt"  # 使用从服务器获取的CA证书\r
+    \r
+    send_get_device_token_request(\r
+        auth_mode=AUTH_MODE,\r
+        ca_cert_path=CA_CERT_PATH\r
+    )\r
+```\r
+\r
+### 7. 总结\r
+\r
+| 证书类型 | 能否直接用于客户端 | 说明 |\r
+|---------|------------------|------|\r
+| CA证书(根证书/中间证书) | ✅ **可以** | 直接用于单向认证验证服务器 |\r
+| 服务器证书 | ❌ **不能** | 需要提取CA证书链 |\r
+| 证书链文件 | ⚠️ **需要提取** | 从中提取CA证书部分 |\r
+\r
+**最佳实践**:\r
+1. 使用 `download_server_ca.py` 脚本从服务器下载CA证书\r
+2. 验证证书类型(Subject == Issuer)\r
+3. 如果验证失败,联系服务器管理员获取正确的CA证书\r
+\r
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/ca_chain.pem b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/ca_chain.pem
new file mode 100644 (file)
index 0000000..abb5e1e
--- /dev/null
@@ -0,0 +1,91 @@
+-----BEGIN CERTIFICATE-----
+MIIF4jCCA8qgAwIBAgIRANVuJGyU7WOrsUbvwZa2T7AwDQYJKoZIhvcNAQEMBQAw
+gYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtK
+ZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYD
+VQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTIw
+MDEwODAwMDAwMFoXDTMwMDEwNzIzNTk1OVowXDELMAkGA1UEBhMCQ04xGjAYBgNV
+BAoTEVdvVHJ1cyBDQSBMaW1pdGVkMTEwLwYDVQQDDChXb1RydXMgRFYgU2VydmVy
+IENBICBbUnVuIGJ5IHRoZSBJc3N1ZXJdMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEA7IE0rmVTVdRdNOzK1jsR/VppyukZ/XbQgakJHOhg6XGDsiHe/l5B
+3PxyXw18jEdN+7YxP0qsGz+HlQbsQh6XlwIyjpz/2gFMiqa7y1v+dHOgj6xNOF5a
+oaPm/Qhb0N+JYQidgaC+1Zp6W+YeC736rzCMr9vL1Usa3QLzRoQEo0DzbG4sPeP1
+US0Ia/i8o6szArH+DAcvrzCZ2kkpTScQ9QfOsvkMBP1W2otICdKUyZHaBc+ztTAd
+ovSlOR+GPf29dYfGQkZAp0tffIRw/na3WB86WGZPpNFfo2QxxsHYoL3oSWKfSWTY
+FgW22J8eA03TFHYowm/NqYuJ7GW553HppQIDAQABo4IBcDCCAWwwHwYDVR0jBBgw
+FoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFJmbLfaL8KPbidSe++V0
+L2jSkE/kMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1Ud
+JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAiBgNVHSAEGzAZMA0GCysGAQQBsjEB
+AgIWMAgGBmeBDAECATBQBgNVHR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0
+cnVzdC5jb20vVVNFUlRydXN0UlNBQ2VydGlmaWNhdGlvbkF1dGhvcml0eS5jcmww
+cQYIKwYBBQUHAQEEZTBjMDoGCCsGAQUFBzAChi5odHRwOi8vY3J0LnVzZXJ0cnVz
+dC5jb20vVVNFUlRydXN0UlNBQUFBQ0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8v
+b2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQB5t8v3uYzHa4EL
+0rOb9g/YAmptUbILcBMKk1x188ucsGVPaG1DG9bpVamxbmCtFA1MlrA7iUC8SGop
+KBnuWFsNKiC7jCbRoahT1/FSwFsSuDlDmOjr1MqDXE+or08UkXsJB57XxXxdVOPl
+DcZHII4qHi1XKK4iurMqb+kbdpAWadyfidRRCGPopYCVYLLYhRJgpFGtfr6Gk8N0
+j81jq/7QbN0dRSDzMNdadKTc7c3+i9fIrXj79lV5Wvva+OL7nh8MxQhG1Ekek7Rv
+en++jSZvaEhCrMsSedFTA/aIy7oJg85tfglF2ybK61HsobjYzdDNICKJlIm4chlA
+XIDDqw2mw0Kz2snrkp9dpvMBqahF/Uy1kHzPcrq1/w5OqZWAuDKxZ68PuZ/ME2hI
+YbIDG9dWT6Y7eqtjQ2TmAQbOqdAG2LeikPMl2DMrPEa4lcKJzsFbHfHAW3hVgPSQ
+hRfS4TtbNnxijbsp8GguMHxP2R7dpAAYybwfZdXP7WYAnwEr1mzIf0Y3J0m7GDyX
+JhaflN3G2wIm2HzRd39NvnDRmFEraqui/YYO9ym0pwq1d0S+bGG6876QCto0u3Cg
+ItFh3Za2ZeIY+g5mWrejSaDs9LT7eu44iCyebfgekdMRqFeCuGAsJdsun3LOHHJo
+tCVPRjyFg9NDeJeMa4Z8QuXAXLd9cw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7
+MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
+VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE
+AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4
+MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5
+MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO
+ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0
+aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI
+s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG
+vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ
+Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb
+IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0
+tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E
+xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV
+icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5
+D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ
+WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ
+5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG
+KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg
+EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID
+ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG
+BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t
+L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr
+BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA
+A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+
+rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+
+/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA
+CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F
+zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA
+vGp4z7h/jnZymQyd/teRCBaho1+V
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb
+MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
+GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj
+YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL
+MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
+BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM
+GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua
+BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe
+3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4
+YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR
+rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm
+ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU
+oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
+MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v
+QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t
+b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF
+AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q
+GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz
+Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2
+G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi
+l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3
+smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
+-----END CERTIFICATE-----
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/cert_chain.pem b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/cert_chain.pem
new file mode 100644 (file)
index 0000000..7312de2
--- /dev/null
@@ -0,0 +1,127 @@
+-----BEGIN CERTIFICATE-----
+MIIGLzCCBRegAwIBAgIQDoeEtRS9PUI03KGd2JIoazANBgkqhkiG9w0BAQsFADBc
+MQswCQYDVQQGEwJDTjEaMBgGA1UEChMRV29UcnVzIENBIExpbWl0ZWQxMTAvBgNV
+BAMMKFdvVHJ1cyBEViBTZXJ2ZXIgQ0EgIFtSdW4gYnkgdGhlIElzc3Vlcl0wHhcN
+MjUwODA1MDAwMDAwWhcNMjYwODIxMjM1OTU5WjAdMRswGQYDVQQDDBIqLnRlc3Qu
+bGVpbmlhby5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDAYTnC
+rJxPLTWj7w3k75qmqOrcue6+0zEv2OG2DcV7ZANB0+j3dCXLeAUmWHAyoIYX92Nb
+ApJ67vCAoz8EjwxWDmy0xrvm8J6y9+CaXggfpdkJmBGB2fjlDGvjWEY9QVl0YIOJ
+eeGHWKuPIf0JJ1m1aeZdlP04NHB1ppBw4JC9pekfDWiJj0X4sGy44zdjx0voAjiM
++nSAeO3IDWTeG7nlWgnrQryCA74l/wBkggz2Nlnjzwm857pDFZAn8fx4GY2IAAvY
+3WISfGOkeixzStZmTk+CxGEy2SxTRGTLXSICDF41gkfgTcK7gqK2J1xL2hPzRjLE
+EvF8EVLvQj1LBCd/AgMBAAGjggMqMIIDJjAfBgNVHSMEGDAWgBSZmy32i/Cj24nU
+nvvldC9o0pBP5DAdBgNVHQ4EFgQUHboPw04c8wuw9nagnR/WA25F0V8wDgYDVR0P
+AQH/BAQDAgWgMAwGA1UdEwEB/wQCMAAwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsG
+AQUFBwMCMEkGA1UdIARCMEAwNAYLKwYBBAGyMQECAhYwJTAjBggrBgEFBQcCARYX
+aHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQIBMD0GA1UdHwQ2MDQwMqAw
+oC6GLGh0dHA6Ly9jcmwuY3Jsb2NzcC5jbi9Xb1RydXNEVlNlcnZlckNBXzIuY3Js
+MGwGCCsGAQUFBwEBBGAwXjA4BggrBgEFBQcwAoYsaHR0cDovL2FpYS5jcmxvY3Nw
+LmNuL1dvVHJ1c0RWU2VydmVyQ0FfMi5jcnQwIgYIKwYBBQUHMAGGFmh0dHA6Ly9v
+Y3NwLmNybG9jc3AuY24wLwYDVR0RBCgwJoISKi50ZXN0LmxlaW5pYW8uY29tghB0
+ZXN0LmxlaW5pYW8uY29tMIIBfAYKKwYBBAHWeQIEAgSCAWwEggFoAWYAdQDYCVU7
+lE96/8gWGW+UT4WrsPj8XodVJg8V0S5yu0VLFAAAAZh5caiPAAAEAwBGMEQCIEDI
+9cjdBO2+Syn6MGoIkuElcBaLDqcZx90pcRLVDC0vAiAh7jVQwA7L1O/kPQHiejUY
+mD76It9dmNO8yapW4rt2+gB2AKyrMHBs6+yEMfQT0vSRXxEeQiRDsfKmjE88Kzun
+HgLDAAABmHlxqFwAAAQDAEcwRQIgcxpTxyrEt4HGRBIZrJAjcQHCebXx67Xc3m5R
+LoJ1QRYCIQCK3gzJOfVtI7HDit/DF0MoABE9S4mpEvJBJlXGgEA7bgB1ANdtfRDR
+p/V3wsfpX9cAv/mCyTNaZeHQswFzF8DIxWl3AAABmHlxqEwAAAQDAEYwRAIgFzKE
+BggU2u8IELOYhUhsHUf0mj16mTIiCRrM7N3czpACIAfIat8Wg/QrGG7CfVq3dwak
+2nZCOj8lRgWAHr332biXMA0GCSqGSIb3DQEBCwUAA4IBAQAb8cZRhW51AHv/qqrh
+yvZ7Hj2MMbZhLgzmWU5VHIfIpM9FKK1K7jOT8Bj+CVm4uQs9VmPqmUGcFLksX3H+
+oYlXZ7P3rAhe1Scve/nF/10Gb5I2A842KMXg1us9FNazPC7L54rALB2CfjdkvBF1
+xIz8eSq1I8jwsEb7MDeYELfu1tks08ijVBHyRsHyKrUmWPVsc43MSC4/YS6PdppK
+tBpkI4m9nISNo/TkWv+cBk3MlMGVI+n6p//hhQ8ws6Mb7isGv9+nYxk1bMRXvW4s
+1alSdOG31gUUg+ewEFcMB7rYCJsvZ6ymSFgKCKoJmDJPajG6Ay03YGPc1CtZnfQF
+2bvf
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIF4jCCA8qgAwIBAgIRANVuJGyU7WOrsUbvwZa2T7AwDQYJKoZIhvcNAQEMBQAw
+gYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtK
+ZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYD
+VQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTIw
+MDEwODAwMDAwMFoXDTMwMDEwNzIzNTk1OVowXDELMAkGA1UEBhMCQ04xGjAYBgNV
+BAoTEVdvVHJ1cyBDQSBMaW1pdGVkMTEwLwYDVQQDDChXb1RydXMgRFYgU2VydmVy
+IENBICBbUnVuIGJ5IHRoZSBJc3N1ZXJdMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEA7IE0rmVTVdRdNOzK1jsR/VppyukZ/XbQgakJHOhg6XGDsiHe/l5B
+3PxyXw18jEdN+7YxP0qsGz+HlQbsQh6XlwIyjpz/2gFMiqa7y1v+dHOgj6xNOF5a
+oaPm/Qhb0N+JYQidgaC+1Zp6W+YeC736rzCMr9vL1Usa3QLzRoQEo0DzbG4sPeP1
+US0Ia/i8o6szArH+DAcvrzCZ2kkpTScQ9QfOsvkMBP1W2otICdKUyZHaBc+ztTAd
+ovSlOR+GPf29dYfGQkZAp0tffIRw/na3WB86WGZPpNFfo2QxxsHYoL3oSWKfSWTY
+FgW22J8eA03TFHYowm/NqYuJ7GW553HppQIDAQABo4IBcDCCAWwwHwYDVR0jBBgw
+FoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFJmbLfaL8KPbidSe++V0
+L2jSkE/kMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1Ud
+JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAiBgNVHSAEGzAZMA0GCysGAQQBsjEB
+AgIWMAgGBmeBDAECATBQBgNVHR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0
+cnVzdC5jb20vVVNFUlRydXN0UlNBQ2VydGlmaWNhdGlvbkF1dGhvcml0eS5jcmww
+cQYIKwYBBQUHAQEEZTBjMDoGCCsGAQUFBzAChi5odHRwOi8vY3J0LnVzZXJ0cnVz
+dC5jb20vVVNFUlRydXN0UlNBQUFBQ0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8v
+b2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQB5t8v3uYzHa4EL
+0rOb9g/YAmptUbILcBMKk1x188ucsGVPaG1DG9bpVamxbmCtFA1MlrA7iUC8SGop
+KBnuWFsNKiC7jCbRoahT1/FSwFsSuDlDmOjr1MqDXE+or08UkXsJB57XxXxdVOPl
+DcZHII4qHi1XKK4iurMqb+kbdpAWadyfidRRCGPopYCVYLLYhRJgpFGtfr6Gk8N0
+j81jq/7QbN0dRSDzMNdadKTc7c3+i9fIrXj79lV5Wvva+OL7nh8MxQhG1Ekek7Rv
+en++jSZvaEhCrMsSedFTA/aIy7oJg85tfglF2ybK61HsobjYzdDNICKJlIm4chlA
+XIDDqw2mw0Kz2snrkp9dpvMBqahF/Uy1kHzPcrq1/w5OqZWAuDKxZ68PuZ/ME2hI
+YbIDG9dWT6Y7eqtjQ2TmAQbOqdAG2LeikPMl2DMrPEa4lcKJzsFbHfHAW3hVgPSQ
+hRfS4TtbNnxijbsp8GguMHxP2R7dpAAYybwfZdXP7WYAnwEr1mzIf0Y3J0m7GDyX
+JhaflN3G2wIm2HzRd39NvnDRmFEraqui/YYO9ym0pwq1d0S+bGG6876QCto0u3Cg
+ItFh3Za2ZeIY+g5mWrejSaDs9LT7eu44iCyebfgekdMRqFeCuGAsJdsun3LOHHJo
+tCVPRjyFg9NDeJeMa4Z8QuXAXLd9cw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7
+MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
+VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE
+AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4
+MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5
+MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO
+ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0
+aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI
+s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG
+vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ
+Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb
+IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0
+tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E
+xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV
+icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5
+D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ
+WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ
+5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG
+KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg
+EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID
+ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG
+BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t
+L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr
+BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA
+A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+
+rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+
+/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA
+CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F
+zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA
+vGp4z7h/jnZymQyd/teRCBaho1+V
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb
+MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
+GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj
+YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL
+MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
+BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM
+GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua
+BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe
+3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4
+YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR
+rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm
+ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU
+oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
+MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v
+QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t
+b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF
+AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q
+GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz
+Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2
+G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi
+l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3
+smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
+-----END CERTIFICATE-----
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/cert_chain_ca.pem b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/cert_chain_ca.pem
new file mode 100644 (file)
index 0000000..33c71ba
--- /dev/null
@@ -0,0 +1,25 @@
+-----BEGIN CERTIFICATE-----
+MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb
+MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
+GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj
+YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL
+MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
+BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM
+GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua
+BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe
+3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4
+YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR
+rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm
+ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU
+oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
+MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v
+QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t
+b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF
+AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q
+GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz
+Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2
+G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi
+l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3
+smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
+-----END CERTIFICATE-----
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/check_certificate.py b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/check_certificate.py
new file mode 100644 (file)
index 0000000..8274c2c
--- /dev/null
@@ -0,0 +1,131 @@
+#!/usr/bin/env python3\r
+# -*- coding: utf-8 -*-\r
+"""\r
+检查证书文件信息\r
+用于验证证书文件是否正确\r
+"""\r
+\r
+import sys\r
+import os\r
+import ssl\r
+import socket\r
+from datetime import datetime\r
+\r
+def check_certificate_file(cert_path):\r
+    """\r
+    检查证书文件\r
+    \r
+    Args:\r
+        cert_path: 证书文件路径\r
+    """\r
+    if not os.path.exists(cert_path):\r
+        print(f"❌ 证书文件不存在: {cert_path}")\r
+        return False\r
+    \r
+    # 读取文件大小\r
+    file_size = os.path.getsize(cert_path)\r
+    print(f"文件: {cert_path}")\r
+    print(f"大小: {file_size} 字节")\r
+    print()\r
+    \r
+    try:\r
+        # 读取证书内容\r
+        with open(cert_path, 'rb') as f:\r
+            cert_data = f.read()\r
+        \r
+        # 尝试解析证书\r
+        try:\r
+            cert = ssl.PEM_cert_to_DER_cert(cert_data.decode('utf-8'))\r
+            cert_info = ssl.DER_cert_to_PEM_cert(cert)\r
+            \r
+            # 使用OpenSSL解析(如果可用)\r
+            import subprocess\r
+            result = subprocess.run(\r
+                ['openssl', 'x509', '-in', cert_path, '-noout', '-text'],\r
+                capture_output=True,\r
+                text=True,\r
+                timeout=5\r
+            )\r
+            \r
+            if result.returncode == 0:\r
+                print("✅ 证书格式正确")\r
+                print()\r
+                print("证书详细信息:")\r
+                print("-" * 50)\r
+                print(result.stdout)\r
+                print("-" * 50)\r
+                \r
+                # 提取关键信息\r
+                subject_result = subprocess.run(\r
+                    ['openssl', 'x509', '-in', cert_path, '-noout', '-subject'],\r
+                    capture_output=True,\r
+                    text=True,\r
+                    timeout=5\r
+                )\r
+                issuer_result = subprocess.run(\r
+                    ['openssl', 'x509', '-in', cert_path, '-noout', '-issuer'],\r
+                    capture_output=True,\r
+                    text=True,\r
+                    timeout=5\r
+                )\r
+                dates_result = subprocess.run(\r
+                    ['openssl', 'x509', '-in', cert_path, '-noout', '-dates'],\r
+                    capture_output=True,\r
+                    text=True,\r
+                    timeout=5\r
+                )\r
+                \r
+                if subject_result.returncode == 0:\r
+                    print(subject_result.stdout.strip())\r
+                if issuer_result.returncode == 0:\r
+                    print(issuer_result.stdout.strip())\r
+                if dates_result.returncode == 0:\r
+                    print(dates_result.stdout.strip())\r
+                \r
+                return True\r
+            else:\r
+                print("⚠️ 无法使用OpenSSL解析证书,但文件格式可能正确")\r
+                print(f"错误: {result.stderr}")\r
+                return False\r
+                \r
+        except Exception as e:\r
+            print(f"❌ 证书解析失败: {e}")\r
+            return False\r
+            \r
+    except Exception as e:\r
+        print(f"❌ 读取证书文件失败: {e}")\r
+        return False\r
+\r
+def main():\r
+    if len(sys.argv) < 2:\r
+        print("用法: python3 check_certificate.py <证书文件路径>")\r
+        print("示例: python3 check_certificate.py ca.crt")\r
+        sys.exit(1)\r
+    \r
+    cert_path = sys.argv[1]\r
+    \r
+    print("=" * 50)\r
+    print("证书文件检查")\r
+    print("=" * 50)\r
+    print()\r
+    \r
+    success = check_certificate_file(cert_path)\r
+    \r
+    if success:\r
+        print()\r
+        print("=" * 50)\r
+        print("✅ 证书检查完成")\r
+        print("=" * 50)\r
+        print()\r
+        print("在 get_device_token_at.py 中使用:")\r
+        print(f"  CA_CERT_PATH = \"{cert_path}\"")\r
+    else:\r
+        print()\r
+        print("=" * 50)\r
+        print("❌ 证书检查失败")\r
+        print("=" * 50)\r
+        sys.exit(1)\r
+\r
+if __name__ == "__main__":\r
+    main()\r
+\r
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/download_server_ca.py b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/download_server_ca.py
new file mode 100644 (file)
index 0000000..a41b2ef
--- /dev/null
@@ -0,0 +1,135 @@
+#!/usr/bin/env python3\r
+# -*- coding: utf-8 -*-\r
+"""\r
+从HTTPS服务器下载CA证书\r
+用于单向认证场景\r
+"""\r
+\r
+import ssl\r
+import socket\r
+import sys\r
+import argparse\r
+from urllib.parse import urlparse\r
+\r
+def download_server_certificate(hostname, port=443, output_file="ca.crt", extract_ca=True):\r
+    """\r
+    从HTTPS服务器下载证书\r
+    \r
+    Args:\r
+        hostname: 服务器主机名或IP\r
+        port: 端口号(默认443)\r
+        output_file: 输出文件名\r
+        extract_ca: 是否提取CA证书(True=提取CA证书,False=保存服务器证书)\r
+    """\r
+    try:\r
+        # 创建SSL上下文\r
+        context = ssl.create_default_context()\r
+        \r
+        # 连接到服务器\r
+        print(f"正在连接到 {hostname}:{port}...")\r
+        with socket.create_connection((hostname, port), timeout=10) as sock:\r
+            with context.wrap_socket(sock, server_hostname=hostname) as ssock:\r
+                # 获取证书链(Python的getpeercert只返回服务器证书,需要从SSL连接获取完整链)\r
+                cert_der = ssock.getpeercert(binary_form=True)\r
+                cert_pem = ssl.DER_cert_to_PEM_cert(cert_der)\r
+                \r
+                # 获取证书信息\r
+                cert_info = ssock.getpeercert()\r
+                \r
+                print(f"\n服务器证书信息:")\r
+                print(f"   主题: {cert_info.get('subject', 'N/A')}")\r
+                print(f"   颁发者: {cert_info.get('issuer', 'N/A')}")\r
+                \r
+                # 判断是服务器证书还是CA证书\r
+                subject = str(cert_info.get('subject', ''))\r
+                issuer = str(cert_info.get('issuer', ''))\r
+                is_ca_cert = (subject == issuer)\r
+                \r
+                if extract_ca and not is_ca_cert:\r
+                    # 如果提取CA证书,但当前是服务器证书,提示用户\r
+                    print(f"\n⚠️  注意: 下载的是服务器证书,不是CA证书")\r
+                    print(f"   建议使用OpenSSL获取完整证书链:")\r
+                    print(f"   openssl s_client -showcerts -connect {hostname}:{port} </dev/null 2>/dev/null | \\")\r
+                    print(f"     openssl x509 -outform PEM > {output_file}")\r
+                    print(f"\n   或者联系服务器管理员获取CA证书")\r
+                    print(f"\n   当前保存服务器证书到: {output_file}")\r
+                    print(f"   (可以尝试使用,但可能无法验证服务器身份)")\r
+                elif extract_ca and is_ca_cert:\r
+                    print(f"\n✅ 这是CA证书(Subject == Issuer),可以直接使用")\r
+                else:\r
+                    print(f"\n✅ 保存服务器证书到: {output_file}")\r
+                \r
+                # 保存证书\r
+                with open(output_file, 'w') as f:\r
+                    f.write(cert_pem)\r
+                \r
+                print(f"   文件大小: {len(cert_pem)} 字节")\r
+                print(f"   有效期: {cert_info.get('notBefore', 'N/A')} 至 {cert_info.get('notAfter', 'N/A')}")\r
+                \r
+                return True\r
+                \r
+    except socket.gaierror as e:\r
+        print(f"❌ DNS解析失败: {e}")\r
+        return False\r
+    except socket.timeout:\r
+        print(f"❌ 连接超时")\r
+        return False\r
+    except ssl.SSLError as e:\r
+        print(f"❌ SSL错误: {e}")\r
+        return False\r
+    except Exception as e:\r
+        print(f"❌ 错误: {e}")\r
+        import traceback\r
+        traceback.print_exc()\r
+        return False\r
+\r
+def main():\r
+    parser = argparse.ArgumentParser(description='从HTTPS服务器下载证书')\r
+    parser.add_argument('url', help='HTTPS URL (例如: https://example.com)')\r
+    parser.add_argument('-o', '--output', default='ca.crt', help='输出文件名 (默认: ca.crt)')\r
+    parser.add_argument('-p', '--port', type=int, help='端口号 (默认从URL解析或443)')\r
+    parser.add_argument('--server-cert', action='store_true', help='下载服务器证书而不是CA证书')\r
+    \r
+    args = parser.parse_args()\r
+    \r
+    # 解析URL\r
+    parsed = urlparse(args.url)\r
+    hostname = parsed.hostname\r
+    port = args.port or parsed.port or 443\r
+    \r
+    if not hostname:\r
+        print("❌ 错误: 无法从URL解析主机名")\r
+        sys.exit(1)\r
+    \r
+    print("=" * 50)\r
+    if args.server_cert:\r
+        print("从HTTPS服务器下载服务器证书")\r
+    else:\r
+        print("从HTTPS服务器下载证书(尝试提取CA证书)")\r
+    print("=" * 50)\r
+    print(f"服务器: {hostname}:{port}")\r
+    print(f"输出文件: {args.output}")\r
+    print("=" * 50)\r
+    \r
+    success = download_server_certificate(hostname, port, args.output, extract_ca=not args.server_cert)\r
+    \r
+    if success:\r
+        print("\n" + "=" * 50)\r
+        print("✅ 证书下载成功!")\r
+        print("=" * 50)\r
+        print(f"\n使用方法:")\r
+        print(f"  在 get_device_token_at.py 中设置:")\r
+        print(f"    AUTH_MODE = 1")\r
+        print(f"    CA_CERT_PATH = \"{args.output}\"")\r
+        print(f"\n注意:")\r
+        print(f"  - 如果下载的是服务器证书,可能需要提取CA证书链")\r
+        print(f"  - 使用以下命令获取完整证书链:")\r
+        print(f"    openssl s_client -showcerts -connect {hostname}:{port} </dev/null 2>/dev/null | \\")\r
+        print(f"      openssl x509 -outform PEM > {args.output}")\r
+    else:\r
+        print("\n❌ 证书下载失败!")\r
+        sys.exit(1)\r
+\r
+if __name__ == "__main__":\r
+    main()\r
+\r
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/extract_ca_from_chain.py b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/extract_ca_from_chain.py
new file mode 100644 (file)
index 0000000..f5d83d1
--- /dev/null
@@ -0,0 +1,266 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+从证书链中提取CA证书
+用于从cert_chain.pem中提取可用于设备端验证的CA证书
+"""
+
+import sys
+import os
+import re
+import subprocess
+import tempfile
+
+def extract_certificates(chain_file):
+    """
+    从证书链文件中提取所有证书
+    
+    Returns:
+        list: 证书列表,每个元素是证书的PEM格式字符串
+    """
+    with open(chain_file, 'r') as f:
+        content = f.read()
+    
+    # 使用正则表达式提取所有证书
+    pattern = r'-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----'
+    cert_matches = re.findall(pattern, content, re.DOTALL)
+    
+    certificates = []
+    for match in cert_matches:
+        cert_text = f'-----BEGIN CERTIFICATE-----{match}-----END CERTIFICATE-----\n'
+        certificates.append(cert_text)
+    
+    return certificates
+
+def get_cert_info(cert_pem):
+    """
+    获取证书信息
+    
+    Returns:
+        dict: 包含subject, issuer, is_ca等信息
+    """
+    # 写入临时文件
+    with tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) as tmp:
+        tmp.write(cert_pem)
+        tmp_path = tmp.name
+    
+    try:
+        # 获取subject和issuer
+        subject_result = subprocess.run(
+            ['openssl', 'x509', '-in', tmp_path, '-noout', '-subject'],
+            capture_output=True, text=True, timeout=5
+        )
+        issuer_result = subprocess.run(
+            ['openssl', 'x509', '-in', tmp_path, '-noout', '-issuer'],
+            capture_output=True, text=True, timeout=5
+        )
+        
+        # 检查是否是CA证书
+        text_result = subprocess.run(
+            ['openssl', 'x509', '-in', tmp_path, '-noout', '-text'],
+            capture_output=True, text=True, timeout=5
+        )
+        
+        subject = subject_result.stdout.strip() if subject_result.returncode == 0 else None
+        issuer = issuer_result.stdout.strip() if issuer_result.returncode == 0 else None
+        
+        # 提取subject和issuer的实际值(去除前缀)
+        subject_value = subject.replace('subject=', '').strip() if subject else None
+        issuer_value = issuer.replace('issuer=', '').strip() if issuer else None
+        
+        is_ca = 'CA:TRUE' in text_result.stdout if text_result.returncode == 0 else False
+        is_self_signed = (subject_value == issuer_value) if subject_value and issuer_value else False
+        
+        return {
+            'subject': subject,
+            'issuer': issuer,
+            'is_ca': is_ca,
+            'is_self_signed': is_self_signed,
+            'pem': cert_pem
+        }
+    finally:
+        os.unlink(tmp_path)
+
+def extract_ca_certificates(chain_file, output_file=None):
+    """
+    从证书链中提取CA证书
+    
+    Args:
+        chain_file: 证书链文件路径
+        output_file: 输出文件路径(如果为None,则自动生成)
+    
+    Returns:
+        str: 输出文件路径
+    """
+    print("=" * 60)
+    print("从证书链中提取CA证书")
+    print("=" * 60)
+    print(f"输入文件: {chain_file}\n")
+    
+    # 提取所有证书
+    certificates = extract_certificates(chain_file)
+    print(f"证书链包含 {len(certificates)} 个证书\n")
+    
+    # 分析每个证书
+    cert_infos = []
+    for i, cert in enumerate(certificates):
+        print(f"分析证书 {i+1}/{len(certificates)}...")
+        info = get_cert_info(cert)
+        cert_infos.append(info)
+        
+        print(f"  Subject: {info['subject']}")
+        print(f"  Issuer:  {info['issuer']}")
+        print(f"  CA证书:  {'是' if info['is_ca'] else '否'}")
+        print(f"  自签名:  {'是' if info['is_self_signed'] else '否'}")
+        print()
+    
+    # 查找CA证书(优先选择自签名的根CA)
+    ca_certs = []
+    root_ca = None
+    
+    for info in cert_infos:
+        if info['is_ca']:
+            ca_certs.append(info)
+            if info['is_self_signed']:
+                root_ca = info
+    
+    # 选择要使用的CA证书
+    if root_ca:
+        selected_ca = root_ca
+        print(f"✅ 找到根CA证书(自签名):")
+    elif ca_certs:
+        # 如果没有根CA,使用最后一个CA证书(通常是中间CA)
+        selected_ca = ca_certs[-1]
+        print(f"⚠️  未找到根CA证书,使用中间CA证书:")
+    else:
+        print("❌ 未找到CA证书!")
+        print("   证书链中可能只包含服务器证书")
+        print("   建议:")
+        print("   1. 使用authmode=0(不验证证书)")
+        print("   2. 或联系服务器管理员获取CA根证书")
+        return None
+    
+    print(f"   Subject: {selected_ca['subject']}")
+    print(f"   Issuer:  {selected_ca['issuer']}")
+    print()
+    
+    # 确定输出文件名
+    if output_file is None:
+        base_name = os.path.splitext(chain_file)[0]
+        output_file = f"{base_name}_ca.pem"
+    
+    # 构建完整CA链(包含所有中间CA和根CA)
+    # 通常需要中间CA证书才能验证服务器证书
+    ca_chain_pem = ""
+    intermediate_cas = [info for info in ca_certs if not info['is_self_signed']]
+    
+    if intermediate_cas:
+        print(f"找到 {len(intermediate_cas)} 个中间CA证书")
+        for info in intermediate_cas:
+            ca_chain_pem += info['pem']
+    
+    # 添加根CA
+    if root_ca:
+        ca_chain_pem += root_ca['pem']
+    
+    # 保存完整CA链(推荐)
+    base_name = os.path.splitext(output_file)[0]
+    ca_chain_file = f"{base_name}_chain.pem"
+    with open(ca_chain_file, 'w') as f:
+        f.write(ca_chain_pem)
+    
+    # 也保存单独的根CA(某些设备可能只需要根CA)
+    with open(output_file, 'w') as f:
+        f.write(selected_ca['pem'])
+    
+    file_size = os.path.getsize(output_file)
+    chain_size = os.path.getsize(ca_chain_file)
+    
+    print(f"✅ 根CA证书已保存到: {output_file}")
+    print(f"   文件大小: {file_size} 字节")
+    print()
+    print(f"✅ 完整CA链已保存到: {ca_chain_file}")
+    print(f"   文件大小: {chain_size} 字节")
+    print(f"   (包含 {len(intermediate_cas)} 个中间CA + 1 个根CA)")
+    print()
+    
+    # 验证证书是否能验证服务器证书
+    print("验证证书链...")
+    server_cert = cert_infos[0] if cert_infos else None
+    if server_cert:
+        # 将服务器证书保存到临时文件
+        with tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) as tmp_server:
+            tmp_server.write(server_cert['pem'])
+            tmp_server_path = tmp_server.name
+        
+        try:
+            # 先尝试使用完整CA链验证
+            verify_result = subprocess.run(
+                ['openssl', 'verify', '-CAfile', ca_chain_file, tmp_server_path],
+                capture_output=True, text=True, timeout=5
+            )
+            
+            if verify_result.returncode == 0:
+                print(f"✅ 使用完整CA链验证成功!")
+                print(f"   推荐使用: {ca_chain_file}")
+            else:
+                # 尝试只用根CA验证
+                verify_result2 = subprocess.run(
+                    ['openssl', 'verify', '-CAfile', output_file, tmp_server_path],
+                    capture_output=True, text=True, timeout=5
+                )
+                
+                if verify_result2.returncode == 0:
+                    print(f"✅ 使用根CA验证成功!")
+                    print(f"   可以使用: {output_file}")
+                else:
+                    print(f"⚠️  证书验证失败")
+                    print(f"   根CA验证: {verify_result2.stderr.strip()}")
+                    print(f"   完整链验证: {verify_result.stderr.strip()}")
+                    print(f"   建议:尝试使用完整证书链文件 {ca_chain_file}")
+        finally:
+            os.unlink(tmp_server_path)
+    
+    print()
+    print("=" * 60)
+    print("使用说明")
+    print("=" * 60)
+    print(f"在 get_device_token_at.py 中设置:")
+    print(f"  AUTH_MODE = 1")
+    print()
+    print(f"推荐使用完整CA链(包含中间CA和根CA):")
+    print(f"  CA_CERT_PATH = \"{ca_chain_file}\"")
+    print()
+    print(f"或者只使用根CA(如果设备支持):")
+    print(f"  CA_CERT_PATH = \"{output_file}\"")
+    print()
+    print("注意:")
+    print("  - 某些设备可能需要完整的CA链才能验证服务器证书")
+    print("  - 如果验证失败,尝试使用完整CA链文件")
+    print()
+    
+    return ca_chain_file  # 返回完整CA链文件路径
+
+def main():
+    parser = argparse.ArgumentParser(description='从证书链中提取CA证书')
+    parser.add_argument('chain_file', help='证书链文件路径 (例如: cert_chain.pem)')
+    parser.add_argument('-o', '--output', help='输出CA证书文件路径 (默认: <chain_file>_ca.pem)')
+    
+    args = parser.parse_args()
+    
+    if not os.path.exists(args.chain_file):
+        print(f"❌ 错误: 证书链文件不存在: {args.chain_file}")
+        sys.exit(1)
+    
+    output_file = extract_ca_certificates(args.chain_file, args.output)
+    
+    if output_file:
+        print("✅ 处理完成!")
+    else:
+        print("❌ 处理失败!")
+        sys.exit(1)
+
+if __name__ == "__main__":
+    import argparse
+    main()
+
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/generate_test_certs.sh b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/generate_test_certs.sh
new file mode 100644 (file)
index 0000000..85f4c67
--- /dev/null
@@ -0,0 +1,71 @@
+#!/bin/bash
+# 生成测试用自签名证书脚本
+# 注意:这些证书仅用于测试,生产环境请使用正式的CA签发的证书
+
+set -e
+
+CERT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+cd "$CERT_DIR"
+
+echo "=========================================="
+echo "生成测试用SSL/TLS证书"
+echo "=========================================="
+echo ""
+
+# 创建临时目录
+TMP_DIR=$(mktemp -d)
+trap "rm -rf $TMP_DIR" EXIT
+
+# 1. 生成CA私钥
+echo "1. 生成CA私钥..."
+openssl genrsa -out "$TMP_DIR/ca.key" 2048
+
+# 2. 生成CA证书
+echo "2. 生成CA证书..."
+openssl req -new -x509 -days 3650 -key "$TMP_DIR/ca.key" -out "$TMP_DIR/ca.crt" \
+    -subj "/C=CN/ST=Guangdong/L=Shenzhen/O=Test CA/CN=Test CA"
+
+# 3. 生成客户端私钥
+echo "3. 生成客户端私钥..."
+openssl genrsa -out "$TMP_DIR/client.key" 2048
+
+# 4. 生成客户端证书请求
+echo "4. 生成客户端证书请求..."
+openssl req -new -key "$TMP_DIR/client.key" -out "$TMP_DIR/client.csr" \
+    -subj "/C=CN/ST=Guangdong/L=Shenzhen/O=Test Client/CN=test-client"
+
+# 5. 使用CA签名客户端证书
+echo "5. 使用CA签名客户端证书..."
+openssl x509 -req -days 3650 -in "$TMP_DIR/client.csr" -CA "$TMP_DIR/ca.crt" \
+    -CAkey "$TMP_DIR/ca.key" -CAcreateserial -out "$TMP_DIR/client.crt"
+
+# 6. 复制证书到当前目录
+echo "6. 复制证书文件..."
+cp "$TMP_DIR/ca.crt" "$CERT_DIR/ca.crt"
+cp "$TMP_DIR/client.crt" "$CERT_DIR/client.crt"
+cp "$TMP_DIR/client.key" "$CERT_DIR/client.key"
+
+# 7. 显示证书信息
+echo ""
+echo "=========================================="
+echo "证书生成完成!"
+echo "=========================================="
+echo ""
+echo "证书文件:"
+echo "  - ca.crt      ($(wc -c < "$CERT_DIR/ca.crt") 字节)"
+echo "  - client.crt  ($(wc -c < "$CERT_DIR/client.crt") 字节)"
+echo "  - client.key  ($(wc -c < "$CERT_DIR/client.key") 字节)"
+echo ""
+echo "证书信息:"
+echo ""
+echo "CA证书:"
+openssl x509 -in "$CERT_DIR/ca.crt" -noout -subject -issuer -dates
+echo ""
+echo "客户端证书:"
+openssl x509 -in "$CERT_DIR/client.crt" -noout -subject -issuer -dates
+echo ""
+echo "=========================================="
+echo "注意:这些是测试证书,仅用于开发测试!"
+echo "生产环境请使用正式的CA签发的证书。"
+echo "=========================================="
+
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/intermediate_cas.pem b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/intermediate_cas.pem
new file mode 100644 (file)
index 0000000..7f9e387
--- /dev/null
@@ -0,0 +1,66 @@
+-----BEGIN CERTIFICATE-----
+MIIF4jCCA8qgAwIBAgIRANVuJGyU7WOrsUbvwZa2T7AwDQYJKoZIhvcNAQEMBQAw
+gYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5MRQwEgYDVQQHEwtK
+ZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMS4wLAYD
+VQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTIw
+MDEwODAwMDAwMFoXDTMwMDEwNzIzNTk1OVowXDELMAkGA1UEBhMCQ04xGjAYBgNV
+BAoTEVdvVHJ1cyBDQSBMaW1pdGVkMTEwLwYDVQQDDChXb1RydXMgRFYgU2VydmVy
+IENBICBbUnVuIGJ5IHRoZSBJc3N1ZXJdMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEA7IE0rmVTVdRdNOzK1jsR/VppyukZ/XbQgakJHOhg6XGDsiHe/l5B
+3PxyXw18jEdN+7YxP0qsGz+HlQbsQh6XlwIyjpz/2gFMiqa7y1v+dHOgj6xNOF5a
+oaPm/Qhb0N+JYQidgaC+1Zp6W+YeC736rzCMr9vL1Usa3QLzRoQEo0DzbG4sPeP1
+US0Ia/i8o6szArH+DAcvrzCZ2kkpTScQ9QfOsvkMBP1W2otICdKUyZHaBc+ztTAd
+ovSlOR+GPf29dYfGQkZAp0tffIRw/na3WB86WGZPpNFfo2QxxsHYoL3oSWKfSWTY
+FgW22J8eA03TFHYowm/NqYuJ7GW553HppQIDAQABo4IBcDCCAWwwHwYDVR0jBBgw
+FoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFJmbLfaL8KPbidSe++V0
+L2jSkE/kMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1Ud
+JQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAiBgNVHSAEGzAZMA0GCysGAQQBsjEB
+AgIWMAgGBmeBDAECATBQBgNVHR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0
+cnVzdC5jb20vVVNFUlRydXN0UlNBQ2VydGlmaWNhdGlvbkF1dGhvcml0eS5jcmww
+cQYIKwYBBQUHAQEEZTBjMDoGCCsGAQUFBzAChi5odHRwOi8vY3J0LnVzZXJ0cnVz
+dC5jb20vVVNFUlRydXN0UlNBQUFBQ0EuY3J0MCUGCCsGAQUFBzABhhlodHRwOi8v
+b2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQB5t8v3uYzHa4EL
+0rOb9g/YAmptUbILcBMKk1x188ucsGVPaG1DG9bpVamxbmCtFA1MlrA7iUC8SGop
+KBnuWFsNKiC7jCbRoahT1/FSwFsSuDlDmOjr1MqDXE+or08UkXsJB57XxXxdVOPl
+DcZHII4qHi1XKK4iurMqb+kbdpAWadyfidRRCGPopYCVYLLYhRJgpFGtfr6Gk8N0
+j81jq/7QbN0dRSDzMNdadKTc7c3+i9fIrXj79lV5Wvva+OL7nh8MxQhG1Ekek7Rv
+en++jSZvaEhCrMsSedFTA/aIy7oJg85tfglF2ybK61HsobjYzdDNICKJlIm4chlA
+XIDDqw2mw0Kz2snrkp9dpvMBqahF/Uy1kHzPcrq1/w5OqZWAuDKxZ68PuZ/ME2hI
+YbIDG9dWT6Y7eqtjQ2TmAQbOqdAG2LeikPMl2DMrPEa4lcKJzsFbHfHAW3hVgPSQ
+hRfS4TtbNnxijbsp8GguMHxP2R7dpAAYybwfZdXP7WYAnwEr1mzIf0Y3J0m7GDyX
+JhaflN3G2wIm2HzRd39NvnDRmFEraqui/YYO9ym0pwq1d0S+bGG6876QCto0u3Cg
+ItFh3Za2ZeIY+g5mWrejSaDs9LT7eu44iCyebfgekdMRqFeCuGAsJdsun3LOHHJo
+tCVPRjyFg9NDeJeMa4Z8QuXAXLd9cw==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7
+MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
+VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE
+AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4
+MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5
+MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO
+ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0
+aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI
+s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG
+vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ
+Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb
+IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0
+tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E
+xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV
+icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5
+D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ
+WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ
+5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG
+KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg
+EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID
+ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG
+BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t
+L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr
+BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA
+A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+
+rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+
+/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA
+CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F
+zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA
+vGp4z7h/jnZymQyd/teRCBaho1+V
+-----END CERTIFICATE-----
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/root_ca.pem b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/root_ca.pem
new file mode 100644 (file)
index 0000000..33c71ba
--- /dev/null
@@ -0,0 +1,25 @@
+-----BEGIN CERTIFICATE-----
+MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb
+MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow
+GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj
+YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL
+MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE
+BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM
+GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP
+ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua
+BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe
+3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4
+YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR
+rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm
+ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU
+oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF
+MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v
+QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t
+b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF
+AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q
+GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz
+Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2
+G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi
+l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3
+smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg==
+-----END CERTIFICATE-----
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/certs/verify_ca_for_https.py b/code/sdk/zhihui/leiniao/leiniao_python_test/certs/verify_ca_for_https.py
new file mode 100644 (file)
index 0000000..fc555f7
--- /dev/null
@@ -0,0 +1,561 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+验证CA证书是否可以用来做HTTPS下载的服务器认证(单向认证)
+
+功能:
+1. 检查证书类型(CA证书、服务器证书、客户端证书)
+2. 验证CA证书是否可以验证目标服务器的证书
+3. 测试HTTPS连接(使用CA证书进行服务器认证)
+"""
+
+import ssl
+import socket
+import sys
+import os
+import subprocess
+from urllib.parse import urlparse
+
+# 可选依赖:用于HTTPS下载测试
+try:
+    import requests
+    from requests.adapters import HTTPAdapter
+    from urllib3.util.ssl_ import create_urllib3_context
+    from urllib3.poolmanager import PoolManager
+    REQUESTS_AVAILABLE = True
+except ImportError:
+    REQUESTS_AVAILABLE = False
+
+
+def extract_certificates_from_file(cert_path):
+    """
+    从证书文件中提取所有证书(支持证书链文件)
+    
+    Returns:
+        list: 证书PEM字符串列表
+    """
+    import re
+    
+    with open(cert_path, 'r') as f:
+        content = f.read()
+    
+    # 使用正则表达式提取所有证书
+    pattern = r'-----BEGIN CERTIFICATE-----(.*?)-----END CERTIFICATE-----'
+    cert_matches = re.findall(pattern, content, re.DOTALL)
+    
+    certificates = []
+    for match in cert_matches:
+        cert_text = f'-----BEGIN CERTIFICATE-----{match}-----END CERTIFICATE-----\n'
+        certificates.append(cert_text)
+    
+    return certificates
+
+
+def check_single_certificate(cert_pem):
+    """
+    检查单个证书的类型
+    
+    Args:
+        cert_pem: 证书的PEM格式字符串
+    
+    Returns:
+        dict: 包含证书类型信息的字典
+    """
+    import tempfile
+    
+    # 写入临时文件
+    with tempfile.NamedTemporaryFile(mode='w', suffix='.pem', delete=False) as tmp:
+        tmp.write(cert_pem)
+        tmp_path = tmp.name
+    
+    try:
+        result = subprocess.run(
+            ['openssl', 'x509', '-in', tmp_path, '-noout', '-text'],
+            capture_output=True,
+            text=True,
+            check=True
+        )
+        cert_text = result.stdout
+        
+        cert_info = {
+            'is_ca': False,
+            'is_server': False,
+            'is_client': False,
+            'basic_constraints': None,
+            'key_usage': None,
+            'extended_key_usage': None,
+            'subject': None,
+            'issuer': None,
+            'is_self_signed': False
+        }
+        
+        # 检查Basic Constraints
+        if 'CA:TRUE' in cert_text or 'CA:true' in cert_text:
+            cert_info['is_ca'] = True
+            cert_info['basic_constraints'] = 'CA:TRUE'
+        elif 'CA:FALSE' in cert_text or 'CA:false' in cert_text:
+            cert_info['basic_constraints'] = 'CA:FALSE'
+        
+        # 检查Key Usage
+        if 'Key Usage' in cert_text:
+            key_usage_parts = []
+            if 'Certificate Sign' in cert_text:
+                key_usage_parts.append('Certificate Sign')
+            if 'Digital Signature' in cert_text:
+                key_usage_parts.append('Digital Signature')
+            if key_usage_parts:
+                cert_info['key_usage'] = ', '.join(key_usage_parts)
+        
+        # 检查Extended Key Usage
+        extended_key_usage_parts = []
+        if 'TLS Web Server Authentication' in cert_text:
+            cert_info['is_server'] = True
+            extended_key_usage_parts.append('TLS Web Server Authentication')
+        if 'TLS Web Client Authentication' in cert_text:
+            cert_info['is_client'] = True
+            extended_key_usage_parts.append('TLS Web Client Authentication')
+        if extended_key_usage_parts:
+            cert_info['extended_key_usage'] = ', '.join(extended_key_usage_parts)
+        
+        # 提取Subject和Issuer
+        subject_result = subprocess.run(
+            ['openssl', 'x509', '-in', tmp_path, '-noout', '-subject'],
+            capture_output=True,
+            text=True,
+            check=True
+        )
+        cert_info['subject'] = subject_result.stdout.strip()
+        
+        issuer_result = subprocess.run(
+            ['openssl', 'x509', '-in', tmp_path, '-noout', '-issuer'],
+            capture_output=True,
+            text=True,
+            check=True
+        )
+        cert_info['issuer'] = issuer_result.stdout.strip()
+        
+        # 检查是否自签名
+        subject_value = cert_info['subject'].replace('subject=', '').strip()
+        issuer_value = cert_info['issuer'].replace('issuer=', '').strip()
+        cert_info['is_self_signed'] = (subject_value == issuer_value)
+        
+        return cert_info
+        
+    finally:
+        os.unlink(tmp_path)
+
+
+def check_certificate_type(cert_path):
+    """
+    检查证书类型(支持证书链文件)
+    
+    Returns:
+        dict: 包含证书类型信息的字典
+    """
+    try:
+        # 提取所有证书
+        certificates = extract_certificates_from_file(cert_path)
+        
+        if not certificates:
+            print(f"❌ 证书文件中未找到任何证书")
+            return None
+        
+        print(f"证书文件包含 {len(certificates)} 个证书")
+        
+        # 检查每个证书
+        cert_infos = []
+        ca_certs = []
+        root_ca = None
+        
+        for i, cert_pem in enumerate(certificates):
+            cert_info = check_single_certificate(cert_pem)
+            if cert_info:
+                cert_infos.append(cert_info)
+                
+                # 收集CA证书
+                if cert_info['is_ca']:
+                    ca_certs.append(cert_info)
+                    if cert_info['is_self_signed']:
+                        root_ca = cert_info
+        
+        # 如果只有一个证书,返回该证书的信息
+        if len(cert_infos) == 1:
+            return cert_infos[0]
+        
+        # 如果有多个证书,返回汇总信息
+        summary = {
+            'is_certificate_chain': True,
+            'cert_count': len(cert_infos),
+            'ca_count': len(ca_certs),
+            'has_root_ca': root_ca is not None,
+            'certificates': cert_infos,
+            'ca_certificates': ca_certs,
+            'root_ca': root_ca
+        }
+        
+        # 判断是否可以作为CA证书使用
+        # 如果包含CA证书(特别是根CA),可以用于服务器认证
+        if ca_certs:
+            summary['can_use_for_server_auth'] = True
+            if root_ca:
+                summary['recommended_cert'] = root_ca
+            else:
+                summary['recommended_cert'] = ca_certs[-1]  # 使用最后一个CA证书
+        else:
+            summary['can_use_for_server_auth'] = False
+        
+        return summary
+        
+    except Exception as e:
+        print(f"❌ 检查证书类型失败: {e}")
+        import traceback
+        traceback.print_exc()
+        return None
+
+
+def verify_ca_with_server(ca_cert_path, server_host, server_port=443):
+    """
+    验证CA证书是否可以验证服务器的证书
+    
+    Args:
+        ca_cert_path: CA证书路径
+        server_host: 服务器主机名
+        server_port: 服务器端口,默认443
+    
+    Returns:
+        bool: 是否可以验证
+    """
+    try:
+        # 创建SSL上下文
+        context = ssl.create_default_context()
+        context.check_hostname = False  # 先不检查主机名
+        context.verify_mode = ssl.CERT_REQUIRED
+        
+        # 加载CA证书
+        context.load_verify_locations(ca_cert_path)
+        
+        # 建立连接
+        with socket.create_connection((server_host, server_port), timeout=10) as sock:
+            with context.wrap_socket(sock, server_hostname=server_host) as ssock:
+                server_cert = ssock.getpeercert(binary_form=False)
+                server_cert_binary = ssock.getpeercert(binary_form=True)
+                
+                print(f"✅ SSL连接成功")
+                print(f"   服务器证书主题: {server_cert.get('subject', 'N/A')}")
+                print(f"   服务器证书颁发者: {server_cert.get('issuer', 'N/A')}")
+                
+                # 验证证书链
+                try:
+                    # 使用openssl验证
+                    result = subprocess.run(
+                        ['openssl', 'verify', '-CAfile', ca_cert_path, '-'],
+                        input=server_cert_binary,
+                        capture_output=True,
+                        check=False
+                    )
+                    
+                    if result.returncode == 0:
+                        print(f"✅ CA证书可以验证服务器证书")
+                        return True
+                    else:
+                        print(f"❌ CA证书无法验证服务器证书")
+                        print(f"   错误: {result.stderr.decode('utf-8', errors='ignore')}")
+                        return False
+                except Exception as e:
+                    print(f"⚠️  无法使用openssl验证,但SSL连接成功: {e}")
+                    return True  # SSL连接成功说明证书验证通过
+                    
+    except ssl.SSLError as e:
+        print(f"❌ SSL验证失败: {e}")
+        return False
+    except socket.timeout:
+        print(f"❌ 连接超时")
+        return False
+    except Exception as e:
+        print(f"❌ 连接失败: {e}")
+        return False
+
+
+def test_https_download_with_ca(ca_cert_path, test_url):
+    """
+    测试使用CA证书进行HTTPS下载
+    
+    Args:
+        ca_cert_path: CA证书路径
+        test_url: 测试URL
+    
+    Returns:
+        bool: 是否成功
+    """
+    if not REQUESTS_AVAILABLE:
+        print("⚠️  requests库未安装,跳过HTTPS下载测试")
+        print("   安装方法: pip install requests urllib3")
+        return None
+    
+    try:
+        parsed = urlparse(test_url)
+        if parsed.scheme != 'https':
+            print(f"❌ URL必须是HTTPS协议: {test_url}")
+            return False
+        
+        print(f"\n测试HTTPS下载: {test_url}")
+        print(f"使用CA证书: {ca_cert_path}")
+        
+        # 创建自定义的HTTPAdapter,使用CA证书
+        class CAAdapter(HTTPAdapter):
+            def init_poolmanager(self, *args, **kwargs):
+                context = create_urllib3_context()
+                context.load_verify_locations(ca_cert_path)
+                kwargs['ssl_context'] = context
+                return super().init_poolmanager(*args, **kwargs)
+        
+        # 创建Session
+        session = requests.Session()
+        session.mount('https://', CAAdapter())
+        
+        # 发送HEAD请求(只获取响应头,不下载内容)
+        print("发送HTTPS HEAD请求...")
+        response = session.head(test_url, timeout=10, allow_redirects=True)
+        
+        if response.status_code == 200:
+            print(f"✅ HTTPS请求成功")
+            print(f"   状态码: {response.status_code}")
+            print(f"   内容类型: {response.headers.get('Content-Type', 'N/A')}")
+            print(f"   内容长度: {response.headers.get('Content-Length', 'N/A')} 字节")
+            return True
+        else:
+            print(f"⚠️  HTTPS请求返回状态码: {response.status_code}")
+            return False
+            
+    except requests.exceptions.SSLError as e:
+        print(f"❌ SSL验证失败: {e}")
+        return False
+    except requests.exceptions.RequestException as e:
+        print(f"❌ 请求失败: {e}")
+        return False
+    except Exception as e:
+        print(f"❌ 测试失败: {e}")
+        import traceback
+        traceback.print_exc()
+        return False
+
+
+def main():
+    """主函数"""
+    import argparse
+    
+    parser = argparse.ArgumentParser(
+        description='验证CA证书是否可以用来做HTTPS下载的服务器认证',
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+示例:
+  # 验证CA证书类型
+  python verify_ca_for_https.py ca_chain.pem
+  
+  # 验证CA证书是否可以验证特定服务器
+  python verify_ca_for_https.py ca_chain.pem --server ambybox-api.test.leiniao.com
+  
+  # 测试HTTPS下载
+  python verify_ca_for_https.py ca_chain.pem --test-url https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/auth/getDeviceToken
+        """
+    )
+    
+    parser.add_argument('ca_cert', help='CA证书文件路径')
+    parser.add_argument('--server', help='服务器主机名(用于验证CA证书是否可以验证服务器证书)')
+    parser.add_argument('--port', type=int, default=443, help='服务器端口,默认443')
+    parser.add_argument('--test-url', help='测试HTTPS下载的URL')
+    
+    args = parser.parse_args()
+    
+    # 检查证书文件是否存在
+    if not os.path.exists(args.ca_cert):
+        print(f"❌ 证书文件不存在: {args.ca_cert}")
+        sys.exit(1)
+    
+    print("=" * 70)
+    print("CA证书HTTPS验证工具")
+    print("=" * 70)
+    print(f"证书文件: {args.ca_cert}")
+    print()
+    
+    # 1. 检查证书类型
+    print("【步骤1: 检查证书类型】")
+    print("-" * 70)
+    cert_info = check_certificate_type(args.ca_cert)
+    
+    if not cert_info:
+        print("❌ 无法检查证书类型")
+        sys.exit(1)
+    
+    # 处理证书链文件
+    if cert_info.get('is_certificate_chain'):
+        print(f"✅ 这是一个证书链文件,包含 {cert_info['cert_count']} 个证书")
+        print(f"   其中包含 {cert_info['ca_count']} 个CA证书")
+        
+        if cert_info['has_root_ca']:
+            print("   ✅ 包含根CA证书(自签名)")
+            root_ca = cert_info['root_ca']
+            print(f"   根CA主题: {root_ca['subject']}")
+            print(f"   根CA颁发者: {root_ca['issuer']}")
+        else:
+            print("   ⚠️  未找到根CA证书(自签名),但包含中间CA证书")
+            if cert_info['ca_certificates']:
+                last_ca = cert_info['ca_certificates'][-1]
+                print(f"   最后一个CA证书主题: {last_ca['subject']}")
+        
+        print()
+        print("证书链中的证书:")
+        for i, cert in enumerate(cert_info['certificates'], 1):
+            cert_type = []
+            if cert['is_ca']:
+                cert_type.append("CA证书")
+                if cert['is_self_signed']:
+                    cert_type.append("(根CA)")
+            if cert['is_server']:
+                cert_type.append("服务器证书")
+            if cert['is_client']:
+                cert_type.append("客户端证书")
+            
+            print(f"  {i}. {', '.join(cert_type) if cert_type else '未知类型'}")
+            print(f"     Subject: {cert['subject']}")
+            print(f"     Issuer:  {cert['issuer']}")
+        
+        print()
+        
+        if cert_info.get('can_use_for_server_auth'):
+            print("✅ 证书链可以用于HTTPS服务器认证(单向认证)")
+            if cert_info.get('recommended_cert'):
+                print(f"   推荐使用: 根CA证书或完整证书链")
+        else:
+            print("⚠️  证书链中未找到CA证书,可能无法用于服务器认证")
+    else:
+        # 单个证书
+        print(f"证书主题: {cert_info['subject']}")
+        print(f"证书颁发者: {cert_info['issuer']}")
+        print()
+        
+        # 判断证书类型
+        if cert_info['is_ca']:
+            print("✅ 这是一个CA证书(证书颁发机构证书)")
+            if cert_info['is_self_signed']:
+                print("   ✅ 这是根CA证书(自签名)")
+            print(f"   Basic Constraints: {cert_info['basic_constraints']}")
+            if cert_info['key_usage']:
+                print(f"   Key Usage: {cert_info['key_usage']}")
+        else:
+            print("⚠️  这不是一个CA证书")
+            if cert_info['is_server']:
+                print("   这是一个服务器证书")
+            elif cert_info['is_client']:
+                print("   这是一个客户端证书")
+            else:
+                print("   证书类型未知")
+        
+        if cert_info['extended_key_usage']:
+            print(f"   Extended Key Usage: {cert_info['extended_key_usage']}")
+    
+    print()
+    
+    # 2. 如果指定了服务器,验证CA证书是否可以验证服务器证书
+    if args.server:
+        print("【步骤2: 验证CA证书是否可以验证服务器证书】")
+        print("-" * 70)
+        print(f"服务器: {args.server}:{args.port}")
+        print()
+        
+        can_verify = verify_ca_with_server(args.ca_cert, args.server, args.port)
+        print()
+        
+        if not can_verify:
+            print("❌ CA证书无法验证服务器证书,不能用于HTTPS下载")
+            sys.exit(1)
+    else:
+        print("【步骤2: 跳过服务器验证(未指定--server参数)】")
+        print()
+    
+    # 3. 如果指定了测试URL,测试HTTPS下载
+    if args.test_url:
+        print("【步骤3: 测试HTTPS下载】")
+        print("-" * 70)
+        
+        success = test_https_download_with_ca(args.ca_cert, args.test_url)
+        print()
+        
+        if success:
+            print("✅ CA证书可以用于HTTPS下载")
+        else:
+            print("❌ CA证书无法用于HTTPS下载")
+            sys.exit(1)
+    else:
+        print("【步骤3: 跳过HTTPS下载测试(未指定--test-url参数)】")
+        print()
+        print("提示: 使用 --test-url 参数可以测试实际的HTTPS下载")
+    
+    # 总结
+    print("=" * 70)
+    print("【验证总结】")
+    print("=" * 70)
+    
+    # 判断是否可以用于服务器认证
+    can_use = False
+    if cert_info.get('is_certificate_chain'):
+        can_use = cert_info.get('can_use_for_server_auth', False)
+        if can_use:
+            print("✅ 证书类型: 证书链文件(包含CA证书,可用于服务器认证)")
+        else:
+            print("⚠️  证书类型: 证书链文件(但未找到CA证书)")
+    else:
+        can_use = cert_info.get('is_ca', False)
+        if can_use:
+            print("✅ 证书类型: CA证书(可用于服务器认证)")
+        else:
+            print("⚠️  证书类型: 不是CA证书(可能无法用于服务器认证)")
+    
+    if args.server:
+        server_verified = verify_ca_with_server(args.ca_cert, args.server, args.port)
+        if server_verified:
+            print("✅ 服务器验证: 通过")
+            can_use = True  # 实际验证通过,即使类型检查不通过也认为可用
+        else:
+            print("❌ 服务器验证: 失败")
+    else:
+        print("⚠️  服务器验证: 未测试(建议使用 --server 参数测试)")
+    
+    if args.test_url:
+        download_tested = test_https_download_with_ca(args.ca_cert, args.test_url)
+        if download_tested is True:
+            print("✅ HTTPS下载: 通过")
+            can_use = True  # 实际测试通过,认为可用
+        elif download_tested is False:
+            print("❌ HTTPS下载: 失败")
+        else:
+            print("⚠️  HTTPS下载: 未测试(requests库未安装)")
+    else:
+        print("⚠️  HTTPS下载: 未测试(建议使用 --test-url 参数测试)")
+    
+    print()
+    print("使用建议:")
+    if can_use:
+        print("✅ 证书可以用于HTTPS单向认证(服务器认证)")
+        print("   在 get_device_token_at.py 或 download_file_at.py 中设置:")
+        print(f"   AUTH_MODE = 1  # 单向认证")
+        print(f"   CA_CERT_PATH = \"{args.ca_cert}\"")
+    else:
+        print("⚠️  证书可能无法用于HTTPS服务器认证")
+        print("   建议:")
+        print("   1. 使用 --server 参数测试服务器验证")
+        print("   2. 使用 --test-url 参数测试实际HTTPS下载")
+        print("   3. 如果实际使用中成功,说明证书可用(可能是证书链文件)")
+    
+    print("=" * 70)
+    
+    # 如果实际测试通过,即使类型检查不通过也返回成功
+    if can_use:
+        sys.exit(0)
+    else:
+        sys.exit(1)
+
+
+if __name__ == "__main__":
+    main()
+
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/download_emoji_mp4.py b/code/sdk/zhihui/leiniao/leiniao_python_test/download_emoji_mp4.py
new file mode 100644 (file)
index 0000000..d8a012b
--- /dev/null
@@ -0,0 +1,161 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+根据日志中的URL模式,下载所有表情mp4文件到本地
+"""
+
+import os
+import requests
+from pathlib import Path
+from urllib.parse import urlparse
+
+# 基础URL
+BASE_URL = "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions"
+
+# 从日志中提取的表情列表
+EMOTIONS = [
+    "happy",
+    "laughing", 
+    "angry",
+    "wronged",
+    "heartbeat",
+    "awkward",
+    "confused",
+    "shocked",
+    "crying",
+    "blowing-kiss",
+    "sleepy",
+    "mischievous",
+    "speechless",
+    "dizzy",
+    "thinking",
+    "sleeping",
+    "working",
+    "exercising",
+    "playing",
+    "daydreaming"
+]
+
+# 表情名称到URL文件名的映射(从日志中看到的URL是 Laugh.mp4,而emotion是laughing)
+EMOTION_TO_FILENAME = {
+    "happy": "Happy",
+    "laughing": "Laugh",
+    "angry": "Angry",
+    "wronged": "Wronged",
+    "heartbeat": "Heartbeat",
+    "awkward": "Awkward",
+    "confused": "Confused",
+    "shocked": "Shocked",
+    "crying": "Crying",
+    "blowing-kiss": "BlowingKiss",
+    "sleepy": "Sleepy",
+    "mischievous": "Mischievous",
+    "speechless": "Speechless",
+    "dizzy": "Dizzy",
+    "thinking": "Thinking",
+    "sleeping": "Sleeping",
+    "working": "Working",
+    "exercising": "Exercising",
+    "playing": "Playing",
+    "daydreaming": "Daydreaming"
+}
+
+# 从日志中看到的配置
+EYES_SHAPE = "RoundEyes"  # URL中的格式
+EYES_COLOR = "Blue"       # URL中的格式
+
+# 本地保存路径(对应日志中的路径格式)
+LOCAL_BASE_PATH = "./emoji/round-eyes/blue"
+
+def download_file(url, save_path):
+    """下载文件"""
+    try:
+        # 创建目录
+        os.makedirs(os.path.dirname(save_path), exist_ok=True)
+        
+        # 下载文件
+        print(f"正在下载: {url}")
+        response = requests.get(url, stream=True, timeout=30)
+        response.raise_for_status()
+        
+        # 保存文件
+        total_size = int(response.headers.get('content-length', 0))
+        downloaded = 0
+        
+        with open(save_path, 'wb') as f:
+            for chunk in response.iter_content(chunk_size=8192):
+                if chunk:
+                    f.write(chunk)
+                    downloaded += len(chunk)
+                    if total_size > 0:
+                        percent = (downloaded / total_size) * 100
+                        print(f"\r进度: {percent:.1f}% ({downloaded}/{total_size} bytes)", end='', flush=True)
+        
+        print(f"\n✓ 下载完成: {save_path} ({downloaded} bytes)")
+        return True
+        
+    except requests.exceptions.RequestException as e:
+        print(f"\n✗ 下载失败: {url}")
+        print(f"  错误: {e}")
+        return False
+    except Exception as e:
+        print(f"\n✗ 保存失败: {save_path}")
+        print(f"  错误: {e}")
+        return False
+
+def main():
+    """主函数"""
+    print("=" * 60)
+    print("表情MP4文件下载工具")
+    print("=" * 60)
+    print(f"基础URL: {BASE_URL}")
+    print(f"眼型: {EYES_SHAPE}")
+    print(f"颜色: {EYES_COLOR}")
+    print(f"本地保存路径: {LOCAL_BASE_PATH}")
+    print(f"表情数量: {len(EMOTIONS)}")
+    print("=" * 60)
+    print()
+    
+    success_count = 0
+    fail_count = 0
+    
+    for emotion in EMOTIONS:
+        # 获取URL文件名
+        filename = EMOTION_TO_FILENAME.get(emotion, emotion.capitalize())
+        
+        # 构建URL
+        url = f"{BASE_URL}/{EYES_SHAPE}/{EYES_COLOR}/{filename}.mp4"
+        
+        # 构建本地保存路径
+        local_path = os.path.join(LOCAL_BASE_PATH, f"{emotion}.mp4")
+        
+        # 检查文件是否已存在
+        if os.path.exists(local_path):
+            file_size = os.path.getsize(local_path)
+            print(f"⏭  跳过(已存在): {emotion}.mp4 ({file_size} bytes)")
+            success_count += 1
+            continue
+        
+        # 下载文件
+        if download_file(url, local_path):
+            success_count += 1
+        else:
+            fail_count += 1
+        
+        print()  # 空行分隔
+    
+    # 统计结果
+    print("=" * 60)
+    print("下载完成统计")
+    print("=" * 60)
+    print(f"成功: {success_count}/{len(EMOTIONS)}")
+    print(f"失败: {fail_count}/{len(EMOTIONS)}")
+    print(f"本地路径: {os.path.abspath(LOCAL_BASE_PATH)}")
+    print("=" * 60)
+
+if __name__ == "__main__":
+    main()
+
+
+
+
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/download_mp4_at.py b/code/sdk/zhihui/leiniao/leiniao_python_test/download_mp4_at.py
new file mode 100644 (file)
index 0000000..c19025a
--- /dev/null
@@ -0,0 +1,496 @@
+#!/usr/bin/env python3\r
+# -*- coding: utf-8 -*-\r
+"""\r
+使用AT指令通过HTTPS下载MP4文件\r
+参考 get_device_token_at.py 实现\r
+"""\r
+\r
+import serial\r
+import time\r
+import re\r
+import urllib.parse\r
+import os\r
+from get_device_token_at import ATCommandHandler, HTTPSATClient, HTTPATClient\r
+\r
+# -------------------------- 1. 配置参数 --------------------------\r
+# 目标文件URL\r
+MP4_URL = "https://file-virtual-person-qcl.test.leiniao.com/ambybox/Emotions/RoundEyes/Blue/Angry.mp4"\r
+# MP4_URL = "http://snicker.com.cn:8089/LEINIAO/blue/angry.zip"\r
+\r
+# 保存路径\r
+OUTPUT_DIR = "downloads"\r
+OUTPUT_FILENAME = "Angry.mp4"\r
+\r
+# AT模块串口配置(根据实际硬件配置修改)\r
+SERIAL_PORT = "COM19"  # 修改为实际的串口设备\r
+SERIAL_BAUDRATE = 2000000\r
+SERIAL_TIMEOUT = 10\r
+\r
+# 认证模式配置\r
+AUTH_MODE = 1  # 0=无需认证, 1=单向认证, 2=双向认证\r
+# CA_CERT_PATH = None  # 例如: "certs/ca.crt"\r
+CA_CERT_PATH = "certs/ca_chain.pem"\r
+\r
+# -------------------------- 2. 下载MP4文件函数 --------------------------\r
+def download_mp4_file(url, output_path, auth_mode=0, ca_cert_path=None):\r
+    """\r
+    使用AT指令通过HTTP/HTTPS下载MP4文件\r
+    \r
+    Args:\r
+        url: MP4文件的HTTP/HTTPS URL\r
+        output_path: 保存路径\r
+        auth_mode: 认证模式,0=无需认证, 1=单向认证, 2=双向认证(仅HTTPS需要)\r
+        ca_cert_path: CA证书路径(HTTPS单向认证时需要)\r
+    """\r
+    # 解析URL,判断协议类型\r
+    parsed_url = urllib.parse.urlparse(url)\r
+    is_https = parsed_url.scheme.lower() == 'https'\r
+    is_http = parsed_url.scheme.lower() == 'http'\r
+    \r
+    if not (is_http or is_https):\r
+        raise ValueError(f"不支持的URL协议: {parsed_url.scheme},仅支持 http:// 或 https://")\r
+    \r
+    protocol_name = "HTTPS" if is_https else "HTTP"\r
+    print(f"检测到协议类型: {protocol_name}")\r
+    # 初始化AT指令处理器\r
+    at_handler = ATCommandHandler(SERIAL_PORT, SERIAL_BAUDRATE, SERIAL_TIMEOUT)\r
+    \r
+    try:\r
+        # 连接串口\r
+        if not at_handler.connect():\r
+            print("❌ 无法连接串口,请检查串口配置")\r
+            return False\r
+        \r
+        # 测试AT连接\r
+        success, response = at_handler.send_at_command("AT", timeout=3)\r
+        if not success:\r
+            print("❌ AT模块无响应,请检查连接")\r
+            return False\r
+        print("✅ AT模块连接正常")\r
+        \r
+        # 检查PPP拨号状态\r
+        print("\n" + "=" * 50)\r
+        print("【步骤1: 检查PPP拨号状态】")\r
+        print("=" * 50)\r
+        is_ppp_connected, ppp_ip = at_handler.check_ppp_status()\r
+        \r
+        if is_ppp_connected:\r
+            print(f"✅ PPP已连接,IP地址: {ppp_ip}")\r
+        else:\r
+            print(f"❌ PPP未连接,IP地址: {ppp_ip or '未知'}")\r
+            print("\n尝试建立PPP连接...")\r
+            \r
+            apn_name = None\r
+            success, ip = at_handler.establish_ppp_connection(apn_name=apn_name)\r
+            \r
+            if not success:\r
+                print(f"❌ PPP连接建立失败,无法继续{protocol_name}访问")\r
+                return False\r
+            \r
+            ppp_ip = ip\r
+        \r
+        # 根据协议类型初始化客户端\r
+        if is_https:\r
+            # 初始化HTTPS客户端\r
+            http_client = HTTPSATClient(at_handler)\r
+            \r
+            # 处理证书(如果需要)\r
+            ca_cert_name = None\r
+            if auth_mode == 1 and ca_cert_path:\r
+                print("\n" + "=" * 50)\r
+                print("【步骤2: 处理证书】")\r
+                print("=" * 50)\r
+                \r
+                ca_cert_name = os.path.basename(ca_cert_path)\r
+                \r
+                # 检查证书是否已存在\r
+                if http_client.certificate_exists(ca_cert_name):\r
+                    print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加")\r
+                else:\r
+                    print(f"证书 {ca_cert_name} 不存在,开始添加...")\r
+                    from get_device_token_at import load_certificate_file\r
+                    \r
+                    ca_cert_data = load_certificate_file(ca_cert_path)\r
+                    if not ca_cert_data:\r
+                        print("❌ 无法加载CA证书,终止流程")\r
+                        return False\r
+                    \r
+                    if not http_client.add_certificate(ca_cert_name, ca_cert_data):\r
+                        print("❌ CA证书添加失败,终止流程")\r
+                        return False\r
+                    \r
+                    if not http_client.check_certificate(ca_cert_name):\r
+                        print("❌ CA证书确认失败,终止流程")\r
+                        return False\r
+            \r
+            # ========== 开始计时:HTTPS访问耗时 ==========\r
+            access_start_time = time.time()\r
+            \r
+            # 配置HTTPS参数\r
+            print("\n" + "=" * 50)\r
+            print("【步骤3: 配置HTTPS参数】")\r
+            print("=" * 50)\r
+            if not http_client.configure_https(\r
+                ssl_version=3,  # TLS1.2\r
+                auth_mode=auth_mode,\r
+                ca_cert=ca_cert_name\r
+            ):\r
+                print("❌ HTTPS配置失败,终止流程")\r
+                return False\r
+            \r
+            # 确认HTTPS配置\r
+            print("\n确认HTTPS配置...")\r
+            config = http_client.verify_https_config()\r
+            if config:\r
+                print("✅ HTTPS配置确认成功")\r
+            else:\r
+                print("⚠️ HTTPS配置确认失败,但继续执行")\r
+            \r
+            # 设置URL\r
+            print("\n" + "=" * 50)\r
+            print("【步骤4: 设置HTTPS请求参数】")\r
+            print("=" * 50)\r
+            print(f"设置请求URL: {url}")\r
+            http_client.set_url(url)\r
+            \r
+            # 建立HTTPS连接\r
+            print("\n" + "=" * 50)\r
+            print("【步骤5: 建立HTTPS连接】")\r
+            print("=" * 50)\r
+            http_client.setup_connection()\r
+            print("✅ HTTPS连接建立成功")\r
+            \r
+            # 发送GET请求\r
+            print("\n" + "=" * 50)\r
+            print("【步骤6: 发送HTTPS GET请求】")\r
+            print("=" * 50)\r
+            \r
+            # 使用标准GET模式 (mode=0)\r
+            success, response = at_handler.send_at_command("AT+HTTPSACTION=0", timeout=5)\r
+            if not success:\r
+                raise Exception(f"发送HTTPS请求失败: {response}")\r
+            \r
+            # 等待响应数据\r
+            print("等待HTTPS响应...")\r
+            recv_indicator = b"+HTTPSRECV:"\r
+        else:\r
+            # 初始化HTTP客户端\r
+            http_client = HTTPATClient(at_handler)\r
+            \r
+            # ========== 开始计时:HTTP访问耗时 ==========\r
+            access_start_time = time.time()\r
+            \r
+            # 设置URL\r
+            print("\n" + "=" * 50)\r
+            print("【步骤3: 设置HTTP请求参数】")\r
+            print("=" * 50)\r
+            print(f"设置请求URL: {url}")\r
+            http_client.set_url(url)\r
+            \r
+            # 建立HTTP连接\r
+            print("\n" + "=" * 50)\r
+            print("【步骤4: 建立HTTP连接】")\r
+            print("=" * 50)\r
+            http_client.setup_connection()\r
+            print("✅ HTTP连接建立成功")\r
+            \r
+            # 发送GET请求\r
+            print("\n" + "=" * 50)\r
+            print("【步骤5: 发送HTTP GET请求】")\r
+            print("=" * 50)\r
+            \r
+            # 使用标准GET模式 (mode=0)\r
+            success, response = at_handler.send_at_command("AT+HTTPACTION=0", timeout=5)\r
+            if not success:\r
+                raise Exception(f"发送HTTP请求失败: {response}")\r
+            \r
+            # 等待响应数据\r
+            print("等待HTTP响应...")\r
+            recv_indicator = b"+HTTPRECV:"\r
+        \r
+        # 重要:直接读取二进制数据,不使用wait_for_unsolicited(它会解码为文本,可能丢失二进制数据)\r
+        # 手动等待+HTTPRECV:/+HTTPSRECV:提示符,然后立即开始读取二进制数据\r
+        response_data = b""\r
+        start_time = time.time()\r
+        last_data_time = time.time()\r
+        header_received = False\r
+        content_length = None\r
+        body_start_pos = None\r
+        recv_found = False\r
+        \r
+        # 等待+HTTPRECV:/+HTTPSRECV:提示符\r
+        indicator_name = protocol_name + "RECV"\r
+        print(f"等待+{indicator_name}:提示符...")\r
+        while time.time() - start_time < 60:\r
+            if at_handler.serial_conn.in_waiting > 0:\r
+                # 读取数据(二进制模式)\r
+                chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting)\r
+                if chunk:\r
+                    response_data += chunk\r
+                    last_data_time = time.time()\r
+                    \r
+                    # 检查是否包含+HTTPRECV:/+HTTPSRECV:(可能在字节串中)\r
+                    if recv_indicator in response_data:\r
+                        recv_found = True\r
+                        print(f"✅ 收到+{indicator_name}:提示符")\r
+                        # 继续读取,查找HTTP响应头\r
+                        break\r
+            time.sleep(0.1)\r
+        \r
+        if not recv_found:\r
+            # 尝试查找HTTP响应头(可能+HTTPRECV:/+HTTPSRECV:已经被读取)\r
+            if b"HTTP/" in response_data:\r
+                recv_found = True\r
+                print("✅ 检测到HTTP响应数据")\r
+        \r
+        if not recv_found:\r
+            raise Exception(f"未收到+{indicator_name}:提示符或HTTP响应")\r
+        \r
+        # 查找HTTP响应头开始位置(可能在+HTTPSRECV:之后)\r
+        http_start = response_data.find(b"HTTP/")\r
+        if http_start == -1:\r
+            # 如果没有找到HTTP/,继续读取直到找到\r
+            print("继续读取数据以查找HTTP响应头...")\r
+            while time.time() - start_time < 10:\r
+                if at_handler.serial_conn.in_waiting > 0:\r
+                    chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting)\r
+                    if chunk:\r
+                        response_data += chunk\r
+                        http_start = response_data.find(b"HTTP/")\r
+                        if http_start != -1:\r
+                            break\r
+                time.sleep(0.1)\r
+        \r
+        if http_start == -1:\r
+            raise Exception("未找到HTTP响应头")\r
+        \r
+        # 从HTTP响应头开始位置处理数据\r
+        # 跳过+HTTPSRECV:前缀(如果存在)\r
+        if http_start > 0:\r
+            print(f"跳过+HTTPSRECV:前缀 ({http_start} 字节)")\r
+            response_data = response_data[http_start:]\r
+        \r
+        # 查找HTTP响应头结束位置(\r\n\r\n)\r
+        print("读取响应头...")\r
+        header_timeout = 10\r
+        header_start = time.time()\r
+        \r
+        while time.time() - header_start < header_timeout:\r
+            if at_handler.serial_conn.in_waiting > 0:\r
+                chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting)\r
+                if chunk:\r
+                    response_data += chunk\r
+                    \r
+                    # 查找HTTP响应头结束位置(\r\n\r\n)\r
+                    header_end = response_data.find(b"\r\n\r\n")\r
+                    if header_end != -1:\r
+                        header_received = True\r
+                        body_start_pos = header_end + 4\r
+                        \r
+                        # 解析响应头\r
+                        header_text = response_data[:header_end].decode('utf-8', errors='ignore')\r
+                        print("\nHTTP响应头:")\r
+                        print(header_text)\r
+                        \r
+                        # 提取Content-Length\r
+                        content_length_match = re.search(r'Content-Length:\s*(\d+)', header_text, re.IGNORECASE)\r
+                        if content_length_match:\r
+                            content_length = int(content_length_match.group(1))\r
+                            print(f"\n文件大小: {content_length} 字节 ({content_length/1024/1024:.2f} MB)")\r
+                        else:\r
+                            print("⚠️ 未找到Content-Length,将接收所有数据直到连接关闭")\r
+                        \r
+                        # 检查状态码\r
+                        status_match = re.search(r'HTTP/\d\.\d\s+(\d+)', header_text)\r
+                        if status_match:\r
+                            status_code = int(status_match.group(1))\r
+                            if status_code != 200:\r
+                                raise Exception(f"HTTP请求失败,状态码: {status_code}")\r
+                            print(f"HTTP状态码: {status_code} ✅")\r
+                        \r
+                        # 检查文件数据开始位置的前几个字节(用于调试)\r
+                        if len(response_data) > body_start_pos:\r
+                            preview = response_data[body_start_pos:body_start_pos+20]\r
+                            print(f"\n文件数据前20字节(hex): {preview.hex()}")\r
+                            print(f"文件数据前20字节(repr): {repr(preview)}")\r
+                        \r
+                        break\r
+            time.sleep(0.1)\r
+        \r
+        if not header_received:\r
+            raise Exception("未收到完整的HTTP响应头")\r
+        \r
+        # 继续接收文件数据\r
+        print("\n开始接收文件数据...")\r
+        last_progress_time = time.time()  # 上次打印进度的时间\r
+        progress_interval = 5  # 每5秒打印一次进度\r
+        \r
+        while time.time() - start_time < 600:  # 最多等待10分钟\r
+            if at_handler.serial_conn.in_waiting > 0:\r
+                chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting)\r
+                if chunk:\r
+                    response_data += chunk\r
+                    last_data_time = time.time()\r
+                    \r
+                    # 如果知道文件大小,显示进度\r
+                    if content_length:\r
+                        body_data = response_data[body_start_pos:]\r
+                        received = len(body_data)\r
+                        if received >= content_length:\r
+                            print(f"\n✅ 文件接收完成: {received} / {content_length} 字节")\r
+                            break\r
+                        else:\r
+                            # 每5秒打印一次进度\r
+                            current_time = time.time()\r
+                            if current_time - last_progress_time >= progress_interval:\r
+                                progress = (received / content_length) * 100\r
+                                print(f"接收进度: {progress:.1f}% ({received}/{content_length} 字节, {received/1024/1024:.2f} MB)")\r
+                                last_progress_time = current_time\r
+                    else:\r
+                        # 不知道文件大小,每5秒显示已接收的数据量\r
+                        current_time = time.time()\r
+                        if current_time - last_progress_time >= progress_interval:\r
+                            body_data = response_data[body_start_pos:]\r
+                            received = len(body_data)\r
+                            print(f"已接收: {received} 字节 ({received/1024/1024:.2f} MB)")\r
+                            last_progress_time = current_time\r
+            else:\r
+                # 如果超过10秒没有新数据,检查是否接收完成\r
+                if time.time() - last_data_time > 10:\r
+                    if content_length:\r
+                        body_data = response_data[body_start_pos:]\r
+                        received = len(body_data)\r
+                        if received >= content_length:\r
+                            print(f"\n✅ 文件接收完成: {received} / {content_length} 字节")\r
+                            break\r
+                        else:\r
+                            print(f"\n⚠️ 接收超时,已接收: {received} / {content_length} 字节")\r
+                            break\r
+                    else:\r
+                        # 没有Content-Length,如果10秒没有新数据,认为接收完成\r
+                        print("\n响应数据接收完成(超时)")\r
+                        break\r
+                time.sleep(0.1)\r
+        \r
+        # 提取文件数据(跳过HTTP响应头)\r
+        file_data = response_data[body_start_pos:]\r
+        \r
+        # 验证文件数据(MP4文件格式)\r
+        # MP4文件结构:Box size (4字节) + Box type (4字节,通常是"ftyp")\r
+        # 所以文件应该以 0x00 0x00 0x00 0x1C 开头(或类似的大小值),然后是 "ftyp"\r
+        print(f"\n文件数据前16字节(hex): {file_data[:16].hex()}")\r
+        print(f"文件数据前16字节(repr): {repr(file_data[:16])}")\r
+        \r
+        # MP4文件应该以Box size开头(4字节大端整数),然后是"ftyp"\r
+        # 检查前4字节是否为有效的Box size(通常不会太大,比如小于100MB)\r
+        if len(file_data) >= 8:\r
+            box_size_bytes = file_data[:4]\r
+            box_type = file_data[4:8]\r
+            \r
+            # 计算Box size(大端序)\r
+            box_size = int.from_bytes(box_size_bytes, byteorder='big')\r
+            \r
+            print(f"检测到的Box size: {box_size} (hex: {box_size_bytes.hex()})")\r
+            print(f"检测到的Box type: {box_type} (hex: {box_type.hex()})")\r
+            \r
+            # 验证是否为有效的MP4文件\r
+            if box_type == b"ftyp":\r
+                print("✅ 检测到有效的MP4文件格式(ftyp box)")\r
+            elif box_size > 0 and box_size < 100 * 1024 * 1024:  # Box size在合理范围内\r
+                print(f"⚠️ Box size看起来合理,但Box type不是'ftyp',可能是其他格式")\r
+            else:\r
+                # 如果前4字节不是有效的Box size,尝试查找ftyp\r
+                ftyp_pos = file_data.find(b"ftyp")\r
+                if ftyp_pos != -1 and ftyp_pos < 100:\r
+                    print(f"⚠️ 检测到文件数据偏移,ftyp在位置 {ftyp_pos}")\r
+                    print(f"   丢失了前 {ftyp_pos} 字节数据")\r
+                    print(f"   建议检查数据接收过程,确保没有丢失数据")\r
+                    # 不自动调整,保留原始数据\r
+                    # file_data = file_data[ftyp_pos:]\r
+        \r
+        # 保存文件\r
+        print("\n" + "=" * 50)\r
+        print("【步骤7: 保存文件】")\r
+        print("=" * 50)\r
+        \r
+        # 创建输出目录\r
+        os.makedirs(os.path.dirname(output_path), exist_ok=True)\r
+        \r
+        # 写入文件\r
+        with open(output_path, 'wb') as f:\r
+            f.write(file_data)\r
+        \r
+        file_size = len(file_data)\r
+        print(f"✅ 文件保存成功: {output_path}")\r
+        print(f"   文件大小: {file_size} 字节 ({file_size/1024/1024:.2f} MB)")\r
+        \r
+        # 关闭连接\r
+        print("\n" + "=" * 50)\r
+        print(f"【步骤8: 关闭{protocol_name}连接】")\r
+        print("=" * 50)\r
+        try:\r
+            http_client.close_connection()\r
+            print(f"✅ {protocol_name}连接已关闭")\r
+        except Exception as e:\r
+            print(f"警告: 关闭{protocol_name}连接时出错: {str(e)}")\r
+        \r
+        # ========== 结束计时:HTTP/HTTPS访问耗时 ==========\r
+        access_end_time = time.time()\r
+        access_duration = access_end_time - access_start_time\r
+        \r
+        print("\n" + "=" * 50)\r
+        print(f"【{protocol_name}访问耗时统计】")\r
+        print("=" * 50)\r
+        print(f"{protocol_name}访问总耗时: {access_duration:.3f} 秒 ({access_duration*1000:.1f} 毫秒)")\r
+        print(f"下载速度: {file_size/1024/1024/access_duration:.2f} MB/s")\r
+        print("\n耗时包含以下步骤:")\r
+        if is_https:\r
+            print("  - HTTPS配置和验证")\r
+        print("  - URL和端口设置")\r
+        print(f"  - {protocol_name}连接建立")\r
+        print("  - HTTP请求发送")\r
+        print("  - 文件数据接收")\r
+        print("  - 连接关闭")\r
+        print("=" * 50)\r
+        \r
+        return True\r
+        \r
+    except Exception as e:\r
+        print("\n" + "=" * 50)\r
+        print("【错误处理】")\r
+        print("=" * 50)\r
+        print(f"❌ 下载异常: {str(e)}")\r
+        import traceback\r
+        traceback.print_exc()\r
+        return False\r
+    finally:\r
+        # 断开串口连接\r
+        print("\n断开串口连接...")\r
+        at_handler.disconnect()\r
+\r
+# -------------------------- 3. 主程序 --------------------------\r
+if __name__ == "__main__":\r
+    print("=" * 50)\r
+    print("HTTPS MP4文件下载工具(AT指令版本)")\r
+    print("=" * 50)\r
+    print(f"目标URL: {MP4_URL}")\r
+    \r
+    # 构建输出路径\r
+    output_path = os.path.join(OUTPUT_DIR, OUTPUT_FILENAME)\r
+    print(f"保存路径: {output_path}")\r
+    print("=" * 50)\r
+    \r
+    # 执行下载\r
+    success = download_mp4_file(\r
+        url=MP4_URL,\r
+        output_path=output_path,\r
+        auth_mode=AUTH_MODE,\r
+        ca_cert_path=CA_CERT_PATH\r
+    )\r
+    \r
+    if success:\r
+        print("\n✅ MP4文件下载完成!")\r
+    else:\r
+        print("\n❌ MP4文件下载失败!")\r
+        exit(1)\r
+\r
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/get_device_token_at.py b/code/sdk/zhihui/leiniao/leiniao_python_test/get_device_token_at.py
new file mode 100644 (file)
index 0000000..667db3f
--- /dev/null
@@ -0,0 +1,1320 @@
+import serial
+import time
+import hashlib
+import hmac
+import random
+import string
+import json
+import re
+import urllib.parse
+import os
+
+# -------------------------- 1. 接口基础配置 --------------------------
+BASE_URL = "https://ambybox-api.test.leiniao.com/ambybox-device-api"
+INTERFACE_PATH = "/v1/auth/getDeviceToken"
+FULL_URL = BASE_URL + INTERFACE_PATH
+SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus="
+
+# AT模块串口配置(根据实际硬件配置修改)
+SERIAL_PORT = "COM19"  # 修改为实际的串口设备
+SERIAL_BAUDRATE = 1000000
+SERIAL_TIMEOUT = 10
+
+# -------------------------- 2. AT指令通信类 --------------------------
+class ATCommandHandler:
+    """AT指令处理器,用于与N706A模组通信"""
+    
+    def __init__(self, port=SERIAL_PORT, baudrate=SERIAL_BAUDRATE, timeout=SERIAL_TIMEOUT):
+        self.port = port
+        self.baudrate = baudrate
+        self.timeout = timeout
+        self.serial_conn = None
+        
+    def connect(self):
+        """打开串口连接"""
+        try:
+            self.serial_conn = serial.Serial(
+                port=self.port,
+                baudrate=self.baudrate,
+                timeout=self.timeout,
+                bytesize=serial.EIGHTBITS,
+                parity=serial.PARITY_NONE,
+                stopbits=serial.STOPBITS_ONE
+            )
+            print(f"串口连接成功: {self.port} @ {self.baudrate}")
+            return True
+        except Exception as e:
+            print(f"串口连接失败: {e}")
+            return False
+    
+    def disconnect(self):
+        """关闭串口连接"""
+        if self.serial_conn and self.serial_conn.is_open:
+            self.serial_conn.close()
+            print("串口连接已关闭")
+    
+    def send_at_command(self, command, expected_response="OK", timeout=5):
+        """
+        发送AT指令并等待响应
+        
+        Args:
+            command: AT指令字符串(不需要包含\r\n)
+            expected_response: 期望的响应(可以是字符串或正则表达式)
+            timeout: 超时时间(秒)
+        
+        Returns:
+            tuple: (success: bool, response: str)
+        """
+        if not self.serial_conn or not self.serial_conn.is_open:
+            return False, "串口未连接"
+        
+        # 清空输入缓冲区
+        self.serial_conn.reset_input_buffer()
+        
+        # 发送AT指令(添加\r\n)
+        cmd_bytes = (command + "\r\n").encode('utf-8')
+        self.serial_conn.write(cmd_bytes)
+        print(f"发送: {command}")
+        
+        # 等待响应
+        start_time = time.time()
+        response_lines = []
+        
+        while time.time() - start_time < timeout:
+            if self.serial_conn.in_waiting > 0:
+                line = self.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                if line:
+                    response_lines.append(line)
+                    print(f"接收: {line}")
+                    
+                    # 检查是否收到期望的响应
+                    if isinstance(expected_response, str):
+                        if expected_response in line:
+                            return True, "\n".join(response_lines)
+                    else:  # 正则表达式
+                        if expected_response.search(line):
+                            return True, "\n".join(response_lines)
+                    
+                    # 检查错误响应
+                    if "ERROR" in line:
+                        return False, "\n".join(response_lines)
+            
+            time.sleep(0.1)
+        
+        # 超时
+        return False, f"超时: {timeout}秒内未收到期望响应\n" + "\n".join(response_lines)
+    
+    def wait_for_unsolicited(self, pattern, timeout=30):
+        """
+        等待主动上报消息(如+HTTPSRECV)
+        
+        Args:
+            pattern: 匹配模式(字符串或正则表达式)
+            timeout: 超时时间(秒)
+        
+        Returns:
+            tuple: (success: bool, response: str)
+        """
+        if not self.serial_conn or not self.serial_conn.is_open:
+            return False, "串口未连接"
+        
+        start_time = time.time()
+        response_lines = []
+        pattern_compiled = re.compile(pattern) if isinstance(pattern, str) else pattern
+        
+        while time.time() - start_time < timeout:
+            if self.serial_conn.in_waiting > 0:
+                line = self.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                if line:
+                    response_lines.append(line)
+                    print(f"接收: {line}")
+                    
+                    if pattern_compiled.search(line):
+                        return True, "\n".join(response_lines)
+            
+            time.sleep(0.1)
+        
+        return False, f"超时: {timeout}秒内未收到主动上报\n" + "\n".join(response_lines)
+    
+    def check_ppp_status(self):
+        """
+        检查PPP拨号状态
+        
+        Returns:
+            tuple: (is_connected: bool, ip_address: str)
+        """
+        success, response = self.send_at_command("AT+XIIC?", timeout=5)
+        if not success:
+            return False, None
+        
+        # 解析响应格式: +XIIC: <state>,<ip>
+        # 注意:手册说明state前面可能有4个空格
+        xiic_pattern = re.compile(r'\+XIIC:\s*(\d+),([0-9.]+)')
+        match = xiic_pattern.search(response)
+        
+        if match:
+            state = int(match.group(1))
+            ip = match.group(2)
+            
+            # state=1表示已连接,且IP不是0.0.0.0
+            if state == 1 and ip != "0.0.0.0":
+                return True, ip
+            else:
+                return False, ip
+        else:
+            print(f"警告: 无法解析PPP状态响应: {response}")
+            return False, None
+    
+    def check_network_registration(self):
+        """
+        检查网络注册状态
+        
+        Returns:
+            bool: True表示已注册,False表示未注册
+        """
+        success, response = self.send_at_command("AT+CREG?", timeout=5)
+        if not success:
+            return False
+        
+        # 解析响应格式: +CREG: <n>,<stat>
+        # stat=1或5表示已注册
+        creg_pattern = re.compile(r'\+CREG:\s*\d+,([15])')
+        match = creg_pattern.search(response)
+        
+        if match:
+            return True
+        else:
+            print(f"警告: 网络未注册,响应: {response}")
+            return False
+    
+    def set_apn(self, apn_name, cid=1, pdp_type="IP"):
+        """
+        设置APN参数
+        
+        Args:
+            apn_name: APN名称(如"CMNET")
+            cid: 上下文标识符,默认1
+            pdp_type: PDP类型,默认"IP"
+        
+        Returns:
+            bool: 是否设置成功
+        """
+        command = f'AT+CGDCONT={cid},"{pdp_type}","{apn_name}"'
+        success, response = self.send_at_command(command, timeout=5)
+        return success
+    
+    def establish_ppp_connection(self, apn_name=None, max_retries=3):
+        """
+        建立PPP连接
+        
+        Args:
+            apn_name: APN名称(可选,如果为None则不设置APN)
+            max_retries: 最大重试次数
+        
+        Returns:
+            tuple: (success: bool, ip_address: str)
+        """
+        # 检查网络注册状态
+        print("检查网络注册状态...")
+        if not self.check_network_registration():
+            print("❌ 网络未注册,无法建立PPP连接")
+            return False, None
+        
+        print("✅ 网络已注册")
+        
+        # 设置APN(如果提供)
+        if apn_name:
+            print(f"设置APN: {apn_name}")
+            if not self.set_apn(apn_name):
+                print(f"警告: APN设置失败,继续尝试拨号")
+        
+        # 尝试建立PPP连接
+        for attempt in range(1, max_retries + 1):
+            print(f"\n尝试建立PPP连接 (第{attempt}次)...")
+            success, response = self.send_at_command("AT+XIIC=1", timeout=30)
+            
+            if not success:
+                print(f"❌ PPP连接失败: {response}")
+                if attempt < max_retries:
+                    print(f"等待3秒后重试...")
+                    time.sleep(3)
+                continue
+            
+            # 等待一下,然后查询状态确认
+            time.sleep(2)
+            is_connected, ip = self.check_ppp_status()
+            
+            if is_connected:
+                print(f"✅ PPP连接成功,IP地址: {ip}")
+                return True, ip
+            else:
+                print(f"❌ PPP连接状态检查失败,IP: {ip}")
+                if attempt < max_retries:
+                    print(f"等待3秒后重试...")
+                    time.sleep(3)
+        
+        return False, None
+
+# -------------------------- 3. HTTPS AT指令封装 --------------------------
+class HTTPSATClient:
+    """基于AT指令的HTTPS客户端"""
+    
+    def __init__(self, at_handler):
+        self.at = at_handler
+        self.response_data = None
+    
+    def add_certificate(self, cert_name, cert_data):
+        """
+        添加SSL证书到模块(根据SSL/TLS应用指南)
+        
+        注意:AT+CERTADD命令的响应格式特殊:
+        1. 发送 AT+CERTADD=<file_name>,<length>
+        2. 立即返回 CONNECT(不是OK)
+        3. 发送证书数据
+        4. 返回 +CERTADD: <length>,OK 或 +CERTADD: ERROR
+        
+        Args:
+            cert_name: 证书文件名(如 "ca.crt")
+            cert_data: 证书内容(字节数据或字符串)
+        
+        Returns:
+            bool: 是否添加成功
+        """
+        # 如果是字符串,转换为字节
+        if isinstance(cert_data, str):
+            cert_data = cert_data.encode('utf-8')
+        
+        cert_length = len(cert_data)
+        
+        # 清空输入缓冲区
+        self.at.serial_conn.reset_input_buffer()
+        
+        # 直接发送AT+CERTADD命令(不使用send_at_command,因为响应是CONNECT而不是OK)
+        command = f'AT+CERTADD={cert_name},{cert_length}'
+        cmd_bytes = (command + "\r\n").encode('utf-8')
+        self.at.serial_conn.write(cmd_bytes)
+        print(f"发送: {command}")
+        
+        # 等待CONNECT提示符(AT+CERTADD会立即返回CONNECT,而不是OK)
+        print("等待'CONNECT'提示符...")
+        start_time = time.time()
+        connect_received = False
+        response_lines = []
+        
+        while time.time() - start_time < 5:
+            if self.at.serial_conn.in_waiting > 0:
+                line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                if line:
+                    response_lines.append(line)
+                    print(f"接收: {line}")
+                    
+                    # 检查是否收到CONNECT
+                    if "CONNECT" in line.upper():
+                        connect_received = True
+                        break
+                    
+                    # 检查错误响应
+                    if "ERROR" in line:
+                        print(f"❌ 证书添加命令失败: {line}")
+                        return False
+            time.sleep(0.1)
+        
+        if not connect_received:
+            newline = '\n'
+            print(f"❌ 错误: 未收到'CONNECT'提示符,响应: {newline.join(response_lines)}")
+            return False
+        
+        # 发送证书数据
+        print(f"发送证书数据 ({cert_length} 字节)...")
+        self.at.serial_conn.write(cert_data)
+        # 确保数据发送完成
+        self.at.serial_conn.flush()
+        
+        # 等待证书添加完成响应(+CERTADD: <length>,OK 或 +CERTADD: ERROR)
+        print("等待证书添加完成响应...")
+        start_time = time.time()
+        certadd_received = False
+        response_lines = []
+        
+        while time.time() - start_time < 30:  # 证书写入可能需要较长时间
+            if self.at.serial_conn.in_waiting > 0:
+                line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                if line:
+                    response_lines.append(line)
+                    print(f"接收: {line}")
+                    
+                    # 检查是否收到+CERTADD响应
+                    if "+CERTADD:" in line:
+                        certadd_received = True
+                        # 检查是否成功
+                        if f"{cert_length},OK" in line or ",OK" in line:
+                            print(f"✅ 证书 {cert_name} 添加成功")
+                            return True
+                        elif "ERROR" in line:
+                            print(f"❌ 证书添加失败: {line}")
+                            return False
+            time.sleep(0.1)
+        
+        if not certadd_received:
+            newline = '\n'
+            print(f"❌ 错误: 证书添加超时,响应: {newline.join(response_lines)}")
+            return False
+        
+        return False
+    
+    def check_certificate(self, cert_name):
+        """
+        确认证书是否成功写入(根据SSL/TLS应用指南)
+        
+        Args:
+            cert_name: 证书文件名
+        
+        Returns:
+            bool: 证书是否存在
+        """
+        success, response = self.at.send_at_command(
+            f'AT+CERTCHECK={cert_name}',
+            timeout=5
+        )
+        if not success:
+            return False
+        
+        # 检查响应格式: +CERTCHECK: <file_name>,OK
+        if f"{cert_name},OK" in response:
+            print(f"✅ 证书 {cert_name} 确认成功")
+            return True
+        else:
+            print(f"❌ 证书 {cert_name} 不存在或验证失败: {response}")
+            return False
+    
+    def list_certificates(self):
+        """
+        查询设备中所有已添加的证书列表
+        
+        Returns:
+            list: 证书文件名列表,如果查询失败返回None
+        """
+        success, response = self.at.send_at_command("AT+CERTCHECK?", timeout=5)
+        if not success:
+            print(f"警告: 证书列表查询失败: {response}")
+            return None
+        
+        # 解析响应格式: 每行一个证书文件名,最后是OK
+        # 例如:
+        # cacert.pem
+        # keycert.pem
+        # OK
+        cert_list = []
+        for line in response.split('\n'):
+            line = line.strip()
+            if line and line not in ['OK', 'AT+CERTCHECK?']:
+                # 跳过响应前缀(如果有)
+                if line.startswith('+CERTCHECK:'):
+                    continue
+                cert_list.append(line)
+        
+        return cert_list
+    
+    def certificate_exists(self, cert_name):
+        """
+        检查证书是否已存在于设备中(避免重复添加)
+        
+        Args:
+            cert_name: 证书文件名
+        
+        Returns:
+            bool: 证书是否存在
+        """
+        success, response = self.at.send_at_command(
+            f'AT+CERTCHECK={cert_name}',
+            timeout=5
+        )
+        if not success:
+            return False
+        
+        # 检查响应格式: +CERTCHECK: <file_name>,OK 或 +CERTCHECK: ERROR
+        if f"{cert_name},OK" in response:
+            return True
+        else:
+            return False
+    
+    def configure_https(self, ssl_version=3, auth_mode=0, ca_cert=None, client_cert=None, client_key=None):
+        """
+        配置HTTPS SSL参数
+        
+        Args:
+            ssl_version: SSL版本,0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2
+            auth_mode: 认证模式,0=不认证, 1=单向认证, 2=双向认证
+            ca_cert: CA证书文件名(单向/双向认证需要)
+            client_cert: 客户端证书文件名(双向认证需要)
+            client_key: 客户端密钥文件名(双向认证需要)
+        """
+        # 设置SSL版本
+        success, response = self.at.send_at_command(
+            f'AT+HTTPSCFG="sslversion",{ssl_version}',
+            timeout=5
+        )
+        if not success:
+            print(f"警告: SSL版本配置失败: {response}")
+            return False
+        
+        # 设置认证模式
+        success, response = self.at.send_at_command(
+            f'AT+HTTPSCFG="authmode",{auth_mode}',
+            timeout=5
+        )
+        if not success:
+            print(f"警告: 认证模式配置失败: {response}")
+            return False
+        
+        # 单向认证:配置CA证书
+        if auth_mode == 1:
+            if not ca_cert:
+                print("错误: 单向认证需要CA证书")
+                return False
+            
+            success, response = self.at.send_at_command(
+                f'AT+HTTPSCFG="cacert",{ca_cert}',
+                timeout=5
+            )
+            if not success:
+                print(f"错误: CA证书配置失败: {response}")
+                return False
+            print(f"✅ 已配置CA证书: {ca_cert}")
+        
+        # 双向认证:配置CA证书、客户端证书和密钥
+        elif auth_mode == 2:
+            if not ca_cert or not client_cert or not client_key:
+                print("错误: 双向认证需要CA证书、客户端证书和密钥")
+                return False
+            
+            # 配置CA证书
+            success, response = self.at.send_at_command(
+                f'AT+HTTPSCFG="cacert",{ca_cert}',
+                timeout=5
+            )
+            if not success:
+                print(f"错误: CA证书配置失败: {response}")
+                return False
+            
+            # 配置客户端证书
+            success, response = self.at.send_at_command(
+                f'AT+HTTPSCFG="clientcert",{client_cert}',
+                timeout=5
+            )
+            if not success:
+                print(f"错误: 客户端证书配置失败: {response}")
+                return False
+            
+            # 配置客户端密钥
+            success, response = self.at.send_at_command(
+                f'AT+HTTPSCFG="clientkey",{client_key}',
+                timeout=5
+            )
+            if not success:
+                print(f"错误: 客户端密钥配置失败: {response}")
+                return False
+            
+            print(f"✅ 已配置双向认证证书: CA={ca_cert}, ClientCert={client_cert}, ClientKey={client_key}")
+        
+        return True
+    
+    def verify_https_config(self):
+        """
+        确认HTTPS配置(根据SSL/TLS应用指南要求)
+        
+        Returns:
+            dict: 包含配置信息的字典,如果查询失败返回None
+        """
+        success, response = self.at.send_at_command("AT+HTTPSCFG?", timeout=5)
+        if not success:
+            print(f"警告: HTTPS配置查询失败: {response}")
+            return None
+        
+        # 解析响应格式: +HTTPSCFG: <sslversion>,<authmode>,<cacert>,<clientcert>,<clientkey>
+        # 例如: +HTTPSCFG: 3,0,,,
+        httpcfg_pattern = re.compile(r'\+HTTPSCFG:\s*(\d+),(\d+),([^,]*),([^,]*),([^,\r\n]*)')
+        match = httpcfg_pattern.search(response)
+        
+        if match:
+            config = {
+                'sslversion': int(match.group(1)),
+                'authmode': int(match.group(2)),
+                'cacert': match.group(3).strip() if match.group(3).strip() else None,
+                'clientcert': match.group(4).strip() if match.group(4).strip() else None,
+                'clientkey': match.group(5).strip() if match.group(5).strip() else None
+            }
+            
+            # 打印配置信息
+            ssl_version_names = {0: "SSL3.0", 1: "TLS1.0", 2: "TLS1.1", 3: "TLS1.2"}
+            auth_mode_names = {0: "无需认证", 1: "单向认证", 2: "双向认证"}
+            
+            print(f"  SSL版本: {config['sslversion']} ({ssl_version_names.get(config['sslversion'], '未知')})")
+            print(f"  认证模式: {config['authmode']} ({auth_mode_names.get(config['authmode'], '未知')})")
+            if config['cacert']:
+                print(f"  CA证书: {config['cacert']}")
+            if config['clientcert']:
+                print(f"  客户端证书: {config['clientcert']}")
+            if config['clientkey']:
+                print(f"  客户端密钥: {config['clientkey']}")
+            
+            return config
+        else:
+            print(f"警告: 无法解析HTTPS配置响应: {response}")
+            return None
+    
+    def set_url(self, url, keepalive=0):
+        """
+        设置HTTPS请求URL
+        
+        Args:
+            url: 完整的URL(如 https://example.com/path)
+            keepalive: 是否启用长连接,0=关闭(默认),1=启用
+                     根据SSL/TLS应用指南,启用后可以多次请求而无需重新建立连接
+        """
+        # 解析URL
+        parsed = urllib.parse.urlparse(url)
+        host = parsed.netloc
+        path = parsed.path
+        if parsed.query:
+            path += "?" + parsed.query
+        
+        # 设置URL(不包含协议和端口)
+        url_param = f"{host}{path}"
+        success, response = self.at.send_at_command(
+            f'AT+HTTPSPARA=url,"{url_param}"',
+            timeout=5
+        )
+        if not success:
+            raise Exception(f"设置URL失败: {response}")
+        
+        # 设置端口(HTTPS默认443)
+        port = parsed.port if parsed.port else 443
+        success, response = self.at.send_at_command(
+            f'AT+HTTPSPARA=port,{port}',
+            timeout=5
+        )
+        if not success:
+            raise Exception(f"设置端口失败: {response}")
+        
+        # 设置keepalive(根据SSL/TLS应用指南,可选配置)
+        if keepalive in [0, 1]:
+            success, response = self.at.send_at_command(
+                f'AT+HTTPSPARA=keepalive,{keepalive}',
+                timeout=5
+            )
+            if not success:
+                print(f"警告: keepalive配置失败: {response}")
+            else:
+                if keepalive == 1:
+                    print("  ✅ 已启用长连接(keepalive=1)")
+                else:
+                    print("  ✅ 已关闭长连接(keepalive=0,默认)")
+        
+        return True
+    
+    def setup_connection(self):
+        """建立HTTPS连接"""
+        success, response = self.at.send_at_command("AT+HTTPSSETUP", timeout=15)
+        if not success or "ERROR" in response:
+            raise Exception(f"HTTPS连接建立失败: {response}")
+        return True
+    
+    def send_get_request(self, headers=None):
+        """
+        发送HTTPS GET请求
+        
+        Args:
+            headers: 自定义HTTP头字典
+        
+        Returns:
+            str: 响应数据
+        """
+        if headers:
+            # 使用OPEN_MODE (mode=99) 自定义HTTP报文
+            # 根据手册要求:用户自定义报文时需遵循HTTP协议
+            # 构建自定义HTTP请求
+            parsed = urllib.parse.urlparse(FULL_URL)
+            path = parsed.path
+            if parsed.query:
+                path += "?" + parsed.query
+            
+            # 构建HTTP请求报文(严格按照HTTP协议格式)
+            # 请求行:GET /path HTTP/1.1<CRLF>
+            http_request = f"GET {path} HTTP/1.1\r\n"
+            # Host头:Host: hostname<CRLF>
+            http_request += f"Host: {parsed.netloc}\r\n"
+            
+            # 添加自定义头(每行格式:Header-Name: value<CRLF>)
+            for key, value in headers.items():
+                http_request += f"{key}: {value}\r\n"
+            
+            # Connection头
+            http_request += "Connection: close\r\n"
+            # 请求头和请求体之间的空行(即使没有请求体也需要)<CRLF><CRLF>
+            http_request += "\r\n"
+            
+            # 计算请求长度(字节数,用于AT+HTTPSACTION=99,<length>)
+            request_length = len(http_request.encode('utf-8'))
+            
+            # 步骤1:直接发送AT+HTTPSACTION=99,<length>命令
+            # 注意:根据手册,AT+HTTPSACTION=99会先返回">"提示符,而不是OK
+            # 所以不能使用send_at_command(它等待OK),需要直接发送并等待">"
+            self.at.serial_conn.reset_input_buffer()
+            command = f"AT+HTTPSACTION=99,{request_length}"
+            cmd_bytes = (command + "\r\n").encode('utf-8')
+            self.at.serial_conn.write(cmd_bytes)
+            print(f"发送: {command}")
+            
+            # 步骤2:等待">"提示符(根据手册,发送AT+HTTPSACTION=99后会收到">"提示符)
+            print("等待'>'提示符...")
+            start_time = time.time()
+            prompt_received = False
+            response_lines = []
+            
+            while time.time() - start_time < 5:
+                if self.at.serial_conn.in_waiting > 0:
+                    line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                    if line:
+                        response_lines.append(line)
+                        print(f"接收: {line}")
+                        
+                        # 检查是否收到">"提示符
+                        if ">" in line:
+                            prompt_received = True
+                            break
+                        
+                        # 检查错误响应
+                        if "ERROR" in line:
+                            newline = '\n'
+                            raise Exception(f"HTTPS请求命令失败: {newline.join(response_lines)}")
+                time.sleep(0.1)
+            
+            if not prompt_received:
+                newline = '\n'
+                raise Exception(f"未收到'>'提示符,响应: {newline.join(response_lines)}")
+            
+            # 步骤3:发送完整的HTTP请求报文(根据手册,需要发送完整的HTTP协议报文)
+            # 注意:手册要求遵循HTTP协议,使用<CRLF>作为行结束符
+            print(f"发送HTTP请求 ({request_length} 字节)...")
+            self.at.serial_conn.write(http_request.encode('utf-8'))
+            # 确保数据发送完成
+            self.at.serial_conn.flush()
+            print(f"HTTP请求内容:\n{http_request}")
+            
+            # 步骤4:等待OK响应(发送数据后应该会收到OK)
+            print("等待OK响应...")
+            start_time = time.time()
+            ok_received = False
+            
+            while time.time() - start_time < 5:
+                if self.at.serial_conn.in_waiting > 0:
+                    line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                    if line:
+                        print(f"接收: {line}")
+                        if "OK" in line:
+                            ok_received = True
+                            break
+                        if "ERROR" in line:
+                            raise Exception(f"HTTPS请求发送失败: {line}")
+                time.sleep(0.1)
+            
+            if not ok_received:
+                print("⚠️ 未收到OK响应,但继续等待HTTPS响应...")
+        else:
+            # 使用标准GET模式 (mode=0)
+            success, response = self.at.send_at_command("AT+HTTPSACTION=0", timeout=5)
+            if not success:
+                raise Exception(f"发送HTTPS请求失败: {response}")
+        
+        # 等待响应数据
+        print("等待HTTPS响应...")
+        success, response = self.at.wait_for_unsolicited(r"\+HTTPSRECV:", timeout=30)
+        if not success:
+            raise Exception(f"等待HTTPS响应超时: {response}")
+        
+        # 继续读取响应数据
+        response_data = response
+        start_time = time.time()
+        last_data_time = time.time()
+        
+        while time.time() - start_time < 30:  # 最多等待30秒接收完整响应
+            if self.at.serial_conn.in_waiting > 0:
+                line = self.at.serial_conn.readline().decode('utf-8', errors='ignore')
+                if line.strip():
+                    response_data += line
+                    print(f"接收: {line.strip()[:100]}...")  # 只打印前100个字符
+                    last_data_time = time.time()
+                
+                # 检查是否收到连接关闭消息
+                if "+HTTPSCLOSED" in line or "+HTTPSCLOSE" in line:
+                    print("收到连接关闭消息")
+                    break
+            else:
+                # 如果超过2秒没有新数据,认为响应接收完成
+                if time.time() - last_data_time > 2:
+                    print("响应数据接收完成(超时)")
+                    break
+                time.sleep(0.1)
+        
+        return response_data
+    
+    def close_connection(self):
+        """关闭HTTPS连接"""
+        success, response = self.at.send_at_command("AT+HTTPSCLOSE", timeout=5)
+        return success
+
+# -------------------------- 4. HTTP AT指令封装 --------------------------
+class HTTPATClient:
+    """基于AT指令的HTTP客户端"""
+    
+    def __init__(self, at_handler):
+        self.at = at_handler
+        self.response_data = None
+    
+    def set_url(self, url, keepalive=0):
+        """
+        设置HTTP请求URL
+        
+        Args:
+            url: 完整的URL(如 http://example.com/path)
+            keepalive: 是否启用长连接,0=关闭(默认),1=启用
+        """
+        # 解析URL
+        parsed = urllib.parse.urlparse(url)
+        host = parsed.netloc
+        path = parsed.path
+        if parsed.query:
+            path += "?" + parsed.query
+        
+        # 设置URL(不包含协议和端口)
+        url_param = f"{host}{path}"
+        success, response = self.at.send_at_command(
+            f'AT+HTTPPARA=url,"{url_param}"',
+            timeout=5
+        )
+        if not success:
+            raise Exception(f"设置URL失败: {response}")
+        
+        # 设置端口(如果URL中指定了端口)
+        if parsed.port:
+            port = parsed.port
+        else:
+            port = 80  # HTTP默认端口
+        
+        success, response = self.at.send_at_command(
+            f'AT+HTTPPARA=port,{port}',
+            timeout=5
+        )
+        if not success:
+            raise Exception(f"设置端口失败: {response}")
+        
+        # 设置keepalive(可选)
+        if keepalive:
+            success, response = self.at.send_at_command(
+                f'AT+HTTPPARA=keepalive,{keepalive}',
+                timeout=5
+            )
+            if not success:
+                print(f"警告: 设置keepalive失败: {response}")
+    
+    def setup_connection(self):
+        """
+        建立HTTP连接
+        
+        Returns:
+            bool: 是否建立成功
+        """
+        success, response = self.at.send_at_command("AT+HTTPSETUP", timeout=15)
+        if not success:
+            raise Exception(f"建立HTTP连接失败: {response}")
+        return True
+    
+    def send_get_request(self, timeout=60):
+        """
+        发送HTTP GET请求(使用标准GET模式)
+        
+        Args:
+            timeout: 超时时间(秒)
+        
+        Returns:
+            tuple: (success: bool, response: str)
+        """
+        # 使用标准GET模式 (mode=0)
+        success, response = self.at.send_at_command("AT+HTTPACTION=0", timeout=5)
+        if not success:
+            raise Exception(f"发送HTTP请求失败: {response}")
+        
+        # 等待响应(+HTTPRECV:)
+        success, response = self.at.wait_for_unsolicited(r"\+HTTPRECV:", timeout=timeout)
+        return success, response
+    
+    def close_connection(self):
+        """
+        关闭HTTP连接
+        
+        Returns:
+            bool: 是否关闭成功
+        """
+        success, response = self.at.send_at_command("AT+HTTPCLOSE", timeout=5)
+        return success
+    
+    def parse_http_response(self, raw_response):
+        """
+        解析HTTP响应
+        
+        Returns:
+            dict: 包含status_code, headers, body的字典
+        """
+        # 查找HTTP响应开始位置(可能在+HTTPSRECV:之后)
+        http_start = raw_response.find("HTTP/")
+        if http_start == -1:
+            # 如果没有找到HTTP/,尝试查找状态码
+            status_match = re.search(r'(\d{3})\s+', raw_response)
+            if status_match:
+                http_start = status_match.start()
+            else:
+                raise Exception("未找到HTTP响应")
+        
+        response_text = raw_response[http_start:]
+        
+        # 分离头部和正文
+        header_end = response_text.find("\r\n\r\n")
+        if header_end == -1:
+            header_end = response_text.find("\n\n")
+        
+        if header_end == -1:
+            # 如果没有找到空行分隔符,尝试查找第一个{作为JSON开始
+            json_start = response_text.find("{")
+            if json_start != -1:
+                headers_text = response_text[:json_start]
+                body_text = response_text[json_start:]
+            else:
+                raise Exception("无法解析HTTP响应格式")
+        else:
+            headers_text = response_text[:header_end]
+            body_text = response_text[header_end:].strip()
+        
+        # 解析状态行
+        lines = headers_text.split("\n")
+        status_line = lines[0].strip()
+        status_match = re.search(r'HTTP/\d\.\d\s+(\d+)', status_line)
+        status_code = int(status_match.group(1)) if status_match else 0
+        
+        # 解析头部
+        headers = {}
+        for line in lines[1:]:
+            line = line.strip()
+            if ":" in line:
+                key, value = line.split(":", 1)
+                headers[key.strip()] = value.strip()
+        
+        # 提取JSON正文(去除+HTTPSRECV:前缀等)
+        # 查找JSON开始位置
+        json_start = body_text.find("{")
+        json_end = body_text.rfind("}") + 1
+        
+        if json_start != -1 and json_end > json_start:
+            body_json = body_text[json_start:json_end]
+        else:
+            # 如果没有找到JSON,返回原始正文
+            body_json = body_text.strip()
+            # 清理可能的AT指令响应前缀
+            for prefix in ["+HTTPSRECV:", "+HTTPSCLOSED:", "+HTTPSCLOSE:"]:
+                if body_json.startswith(prefix):
+                    body_json = body_json[len(prefix):].strip()
+        
+        return {
+            "status_code": status_code,
+            "headers": headers,
+            "body": body_json
+        }
+
+# -------------------------- 4. 生成合法请求参数 --------------------------
+def generate_request_params():
+    device_id = "DEVICE_PC_TEST_001"
+    device_iccid = "86123456789012345678"
+    
+    # 1. 时间戳格式修正:确保是字符串类型的毫秒级时间戳
+    timestamp = str(int(time.time() * 1000))
+    
+    # 2. 随机数生成:确保长度在8-16位之间(当前12位符合要求)
+    nonce_length = 12
+    nonce = ''.join(random.choices(string.ascii_letters + string.digits, k=nonce_length))
+    
+    # 3. 签名生成(保持不变,符合协议要求)
+    sign_data = f"{device_id}:{device_iccid}:{timestamp}:{nonce}"
+    hmac_obj = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data.encode("utf-8"), hashlib.sha256)
+    signature = hmac_obj.hexdigest().lower()
+    
+    # 4. 返回参数(键名严格匹配文档,保持不变)
+    return {
+        "deviceId": device_id,
+        "deviceIccid": device_iccid,
+        "timestamp": timestamp,
+        "nonce": nonce,
+        "signature": signature,
+        "deviceModel": "AB101",
+        "deviceVersion": "1.0.0"
+    }
+
+# -------------------------- 5. 证书管理辅助函数 --------------------------
+def load_certificate_file(cert_path):
+    """
+    从文件加载证书内容
+    
+    Args:
+        cert_path: 证书文件路径
+    
+    Returns:
+        bytes: 证书内容(字节数据)
+    """
+    try:
+        with open(cert_path, 'rb') as f:
+            cert_data = f.read()
+        print(f"✅ 成功加载证书文件: {cert_path} ({len(cert_data)} 字节)")
+        return cert_data
+    except FileNotFoundError:
+        print(f"❌ 证书文件不存在: {cert_path}")
+        return None
+    except Exception as e:
+        print(f"❌ 读取证书文件失败: {e}")
+        return None
+
+# -------------------------- 6. 发送请求并解析响应 --------------------------
+def send_get_device_token_request(auth_mode=0, ca_cert_path=None, client_cert_path=None, client_key_path=None):
+    """使用AT指令发送HTTPS GET请求"""
+    
+    # 初始化AT指令处理器
+    at_handler = ATCommandHandler()
+    
+    try:
+        # 连接串口
+        if not at_handler.connect():
+            print("❌ 无法连接串口,请检查串口配置")
+            return
+        
+        # 测试AT连接
+        success, response = at_handler.send_at_command("AT", timeout=3)
+        if not success:
+            print("❌ AT模块无响应,请检查连接")
+            return
+        print("✅ AT模块连接正常")
+        
+        # 查询版本号
+        print("\n查询模块版本号...")
+        success, response = at_handler.send_at_command("AT+GMR", timeout=5)
+        if success:
+            print("✅ 版本信息:")
+            # 打印响应中的所有行(去除OK等状态行)
+            for line in response.split('\n'):
+                line = line.strip()
+                if line and line not in ['OK', 'AT+GMR']:
+                    print(f"  {line}")
+        else:
+            print(f"⚠️  版本查询失败: {response}")
+        
+        # 检查PPP拨号状态
+        print("\n" + "=" * 50)
+        print("【步骤1: 检查PPP拨号状态】")
+        print("=" * 50)
+        is_ppp_connected, ppp_ip = at_handler.check_ppp_status()
+        
+        if is_ppp_connected:
+            print(f"✅ PPP已连接,IP地址: {ppp_ip}")
+        else:
+            print(f"❌ PPP未连接,IP地址: {ppp_ip or '未知'}")
+            print("\n尝试建立PPP连接...")
+            
+            # 建立PPP连接(可以根据实际情况设置APN)
+            # 如果不需要设置APN,可以传入None
+            apn_name = None  # 可以根据实际情况修改,如 "CMNET"、"3GNET" 等
+            success, ip = at_handler.establish_ppp_connection(apn_name=apn_name)
+            
+            if not success:
+                print("❌ PPP连接建立失败,无法继续HTTPS访问")
+                return
+            
+            ppp_ip = ip
+        
+        # 初始化HTTPS客户端
+        https_client = HTTPSATClient(at_handler)
+        
+        # 查询设备中已有的证书列表(可选,用于调试)
+        if auth_mode in [1, 2]:
+            print("\n" + "=" * 50)
+            print("【步骤2: 查询设备证书列表】")
+            print("=" * 50)
+            cert_list = https_client.list_certificates()
+            if cert_list:
+                print(f"设备中已有 {len(cert_list)} 个证书:")
+                for cert in cert_list:
+                    print(f"  - {cert}")
+            else:
+                print("设备中暂无证书")
+        
+        # 配置HTTPS(根据认证模式)
+        print("\n" + "=" * 50)
+        print("【步骤3: 配置HTTPS参数】")
+        print("=" * 50)
+        
+        # 根据认证模式处理证书
+        ca_cert_name = None
+        client_cert_name = None
+        client_key_name = None
+        
+        if auth_mode == 1:  # 单向认证
+            print("配置模式: 单向认证")
+            if not ca_cert_path:
+                print("❌ 错误: 单向认证需要提供CA证书路径")
+                return
+            
+            # 提取证书文件名
+            ca_cert_name = os.path.basename(ca_cert_path)
+            
+            # 检查证书是否已存在(避免重复添加)
+            print(f"\n检查证书是否已存在: {ca_cert_name}")
+            if https_client.certificate_exists(ca_cert_name):
+                print(f"✅ 证书 {ca_cert_name} 已存在于设备中,跳过添加步骤")
+            else:
+                print(f"证书 {ca_cert_name} 不存在,开始添加...")
+                
+                # 加载CA证书
+                ca_cert_data = load_certificate_file(ca_cert_path)
+                if not ca_cert_data:
+                    print("❌ 无法加载CA证书,终止流程")
+                    return
+                
+                # 添加CA证书到模块
+                print(f"\n添加CA证书: {ca_cert_name}")
+                if not https_client.add_certificate(ca_cert_name, ca_cert_data):
+                    print("❌ CA证书添加失败,终止流程")
+                    return
+                
+                # 确认证书
+                if not https_client.check_certificate(ca_cert_name):
+                    print("❌ CA证书确认失败,终止流程")
+                    return
+        
+        elif auth_mode == 2:  # 双向认证
+            print("配置模式: 双向认证")
+            if not ca_cert_path or not client_cert_path or not client_key_path:
+                print("❌ 错误: 双向认证需要提供CA证书、客户端证书和密钥路径")
+                return
+            
+            ca_cert_name = os.path.basename(ca_cert_path)
+            client_cert_name = os.path.basename(client_cert_path)
+            client_key_name = os.path.basename(client_key_path)
+            
+            # 检查并添加CA证书
+            print(f"\n检查CA证书: {ca_cert_name}")
+            if https_client.certificate_exists(ca_cert_name):
+                print(f"✅ 证书 {ca_cert_name} 已存在,跳过添加")
+            else:
+                ca_cert_data = load_certificate_file(ca_cert_path)
+                if not ca_cert_data:
+                    return
+                if not https_client.add_certificate(ca_cert_name, ca_cert_data):
+                    return
+                if not https_client.check_certificate(ca_cert_name):
+                    return
+            
+            # 检查并添加客户端证书
+            print(f"\n检查客户端证书: {client_cert_name}")
+            if https_client.certificate_exists(client_cert_name):
+                print(f"✅ 证书 {client_cert_name} 已存在,跳过添加")
+            else:
+                client_cert_data = load_certificate_file(client_cert_path)
+                if not client_cert_data:
+                    return
+                if not https_client.add_certificate(client_cert_name, client_cert_data):
+                    return
+                if not https_client.check_certificate(client_cert_name):
+                    return
+            
+            # 检查并添加客户端密钥
+            print(f"\n检查客户端密钥: {client_key_name}")
+            if https_client.certificate_exists(client_key_name):
+                print(f"✅ 密钥 {client_key_name} 已存在,跳过添加")
+            else:
+                client_key_data = load_certificate_file(client_key_path)
+                if not client_key_data:
+                    return
+                if not https_client.add_certificate(client_key_name, client_key_data):
+                    return
+                if not https_client.check_certificate(client_key_name):
+                    return
+        else:
+            print("配置模式: 无需认证")
+        
+        # ========== 开始计时:HTTPS访问耗时 ==========
+        https_start_time = time.time()
+        
+        # 配置HTTPS参数
+        print("\n" + "=" * 50)
+        print("【步骤4: 配置HTTPS参数】")
+        print("=" * 50)
+        if not https_client.configure_https(
+            ssl_version=3,  # TLS1.2
+            auth_mode=auth_mode,
+            ca_cert=ca_cert_name,
+            client_cert=client_cert_name,
+            client_key=client_key_name
+        ):
+            print("❌ HTTPS配置失败,终止流程")
+            return
+        
+        # 确认HTTPS配置(根据SSL/TLS应用指南要求)
+        print("\n确认HTTPS配置...")
+        config = https_client.verify_https_config()
+        if config:
+            print("✅ HTTPS配置确认成功")
+        else:
+            print("⚠️ HTTPS配置确认失败,但继续执行")
+        
+        # 设置URL
+        print("\n" + "=" * 50)
+        print("【步骤5: 设置HTTPS请求参数】")
+        print("=" * 50)
+        print(f"设置请求URL: {FULL_URL}")
+        https_client.set_url(FULL_URL)
+        
+        # 建立HTTPS连接
+        print("\n" + "=" * 50)
+        print("【步骤6: 建立HTTPS连接】")
+        print("=" * 50)
+        https_client.setup_connection()
+        print("✅ HTTPS连接建立成功")
+        
+        # 生成请求参数
+        params = generate_request_params()
+        
+        # 构建HTTP头(根据API文档要求,参数需要作为HTTP头传递,而不是URL查询参数)
+        # 参考:设备API接口文档.md - 请求头参数部分
+        # 注意:为了与get_device_token_1.py保持一致,先添加参数,最后添加Content-Type
+        headers = {}
+        # 将参数添加到HTTP头(deviceId, deviceIccid, timestamp, nonce, signature等)
+        for key, value in params.items():
+            headers[key] = str(value)
+        # Content-Type放在最后(与requests库的行为保持一致)
+        headers["Content-Type"] = "application/json"
+        
+        print("\n" + "=" * 50)
+        print("【步骤7: 发送HTTPS GET请求】")
+        print("=" * 50)
+        print("【请求信息】")
+        print(f"请求 URL: {FULL_URL}")
+        print(f"请求头: {headers}")
+        print("=" * 50)
+        
+        # 发送请求
+        try:
+            raw_response = https_client.send_get_request(headers=headers)
+        except Exception as e:
+            print(f"❌ HTTPS请求发送失败: {str(e)}")
+            raise
+        
+        # 解析响应
+        print("\n" + "=" * 50)
+        print("【步骤8: 解析HTTPS响应】")
+        print("=" * 50)
+        try:
+            parsed_response = https_client.parse_http_response(raw_response)
+        except Exception as e:
+            print(f"❌ HTTP响应解析失败: {str(e)}")
+            print(f"原始响应: {raw_response[:500]}...")  # 只打印前500字符
+            raise
+        
+        print(f"HTTP状态码: {parsed_response['status_code']}")
+        print(f"响应头: {parsed_response['headers']}")
+        print(f"响应体: {parsed_response['body']}")
+        
+        # 解析JSON响应
+        print("\n" + "=" * 50)
+        print("【步骤9: 解析JSON响应】")
+        print("=" * 50)
+        try:
+            response_data = json.loads(parsed_response['body'])
+            print("\n【JSON响应】")
+            print(json.dumps(response_data, ensure_ascii=False, indent=2))
+            
+            # 响应判断逻辑
+            if response_data.get("errorCode") is None:
+                print("\n✅ 请求成功!")
+                if "data" in response_data:
+                    print(f"设备 Token: {response_data['data'].get('token')}")
+                    print(f"Token 过期时间: {response_data['data'].get('expireTime')} 秒({response_data['data'].get('expireTime', 0)//86400} 天)")
+            else:
+                print(f"\n❌ 请求失败!错误码: {response_data['errorCode']}, 原因: {response_data.get('message', '未知错误')}")
+        except json.JSONDecodeError as e:
+            print(f"\n❌ JSON解析失败: {e}")
+            print(f"原始响应体: {parsed_response['body']}")
+            raise
+        
+        # 关闭连接
+        print("\n" + "=" * 50)
+        print("【步骤10: 关闭HTTPS连接】")
+        print("=" * 50)
+        try:
+            https_client.close_connection()
+            print("✅ HTTPS连接已关闭")
+        except Exception as e:
+            print(f"警告: 关闭HTTPS连接时出错: {str(e)}")
+        
+        # ========== 结束计时:HTTPS访问耗时 ==========
+        https_end_time = time.time()
+        https_duration = https_end_time - https_start_time
+        
+        print("\n" + "=" * 50)
+        print("【HTTPS访问耗时统计】")
+        print("=" * 50)
+        print(f"HTTPS访问总耗时: {https_duration:.3f} 秒 ({https_duration*1000:.1f} 毫秒)")
+        print("\n耗时包含以下步骤:")
+        print("  - HTTPS配置和验证")
+        print("  - URL和端口设置")
+        print("  - HTTPS连接建立")
+        print("  - HTTP请求发送")
+        print("  - HTTP响应接收")
+        print("  - 响应解析")
+        print("  - 连接关闭")
+        print("=" * 50)
+        
+    except Exception as e:
+        print("\n" + "=" * 50)
+        print("【错误处理】")
+        print("=" * 50)
+        print(f"❌ 请求异常: {str(e)}")
+        import traceback
+        traceback.print_exc()
+    finally:
+        # 断开串口连接
+        print("\n断开串口连接...")
+        at_handler.disconnect()
+
+# -------------------------- 7. 执行请求 --------------------------
+if __name__ == "__main__":
+    # 认证模式配置
+    # auth_mode: 0=无需认证, 1=单向认证, 2=双向认证
+    # AUTH_MODE = 0  # 默认无需认证
+    
+    # 证书文件路径(单向/双向认证时需要)
+    # CA_CERT_PATH = None  # 例如: "certs/ca.crt"
+    CLIENT_CERT_PATH = None  # 例如: "certs/client.crt"
+    CLIENT_KEY_PATH = None  # 例如: "certs/client.key"
+    
+    # 如果使用单向认证,取消下面的注释并设置证书路径
+    AUTH_MODE = 1
+    CA_CERT_PATH = "certs/ca_chain.pem"
+    
+    # 如果使用双向认证,取消下面的注释并设置所有证书路径
+    # AUTH_MODE = 2
+    # CA_CERT_PATH = "certs/ca.crt"
+    # CLIENT_CERT_PATH = "certs/client.crt"
+    # CLIENT_KEY_PATH = "certs/client.key"
+    
+    send_get_device_token_request(
+        auth_mode=AUTH_MODE,
+        ca_cert_path=CA_CERT_PATH,
+        client_cert_path=CLIENT_CERT_PATH,
+        client_key_path=CLIENT_KEY_PATH
+    )
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_at_handler.py b/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_at_handler.py
new file mode 100644 (file)
index 0000000..3aa037b
--- /dev/null
@@ -0,0 +1,295 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+N706A AT指令处理器
+提供基础的AT指令通信功能,包括串口通信、PPP拨号等
+"""
+
+import serial
+import time
+import re
+
+
+class ATCommandHandler:
+    """AT指令处理器,用于与N706A模组通信"""
+    
+    def __init__(self, port="COM19", baudrate=1000000, timeout=10):
+        """
+        初始化AT指令处理器
+        
+        Args:
+            port: 串口设备路径(如 "COM19" 或 "/dev/ttyUSB0")
+            baudrate: 波特率,默认1000000
+            timeout: 超时时间(秒),默认10
+        """
+        self.port = port
+        self.baudrate = baudrate
+        self.timeout = timeout
+        self.serial_conn = None
+        
+    def connect(self):
+        """
+        打开串口连接
+        
+        Returns:
+            bool: 连接是否成功
+        """
+        try:
+            self.serial_conn = serial.Serial(
+                port=self.port,
+                baudrate=self.baudrate,
+                timeout=self.timeout,
+                bytesize=serial.EIGHTBITS,
+                parity=serial.PARITY_NONE,
+                stopbits=serial.STOPBITS_ONE
+            )
+            print(f"串口连接成功: {self.port} @ {self.baudrate}")
+            return True
+        except Exception as e:
+            print(f"串口连接失败: {e}")
+            return False
+    
+    def disconnect(self):
+        """关闭串口连接"""
+        if self.serial_conn and self.serial_conn.is_open:
+            self.serial_conn.close()
+            print("串口连接已关闭")
+    
+    def send_at_command(self, command, expected_response="OK", timeout=5):
+        """
+        发送AT指令并等待响应
+        
+        Args:
+            command: AT指令字符串(不需要包含\r\n)
+            expected_response: 期望的响应(可以是字符串或正则表达式)
+            timeout: 超时时间(秒)
+        
+        Returns:
+            tuple: (success: bool, response: str)
+        """
+        if not self.serial_conn or not self.serial_conn.is_open:
+            return False, "串口未连接"
+        
+        # 清空输入缓冲区
+        self.serial_conn.reset_input_buffer()
+        
+        # 发送AT指令(添加\r\n)
+        cmd_bytes = (command + "\r\n").encode('utf-8')
+        self.serial_conn.write(cmd_bytes)
+        print(f"发送: {command}")
+        
+        # 等待响应
+        start_time = time.time()
+        response_lines = []
+        
+        while time.time() - start_time < timeout:
+            if self.serial_conn.in_waiting > 0:
+                line = self.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                if line:
+                    response_lines.append(line)
+                    print(f"接收: {line}")
+                    
+                    # 检查是否收到期望的响应
+                    if isinstance(expected_response, str):
+                        if expected_response in line:
+                            return True, "\n".join(response_lines)
+                    else:  # 正则表达式
+                        if expected_response.search(line):
+                            return True, "\n".join(response_lines)
+                    
+                    # 检查错误响应
+                    if "ERROR" in line:
+                        return False, "\n".join(response_lines)
+            
+            time.sleep(0.1)
+        
+        # 超时
+        return False, f"超时: {timeout}秒内未收到期望响应\n" + "\n".join(response_lines)
+    
+    def wait_for_unsolicited(self, pattern, timeout=30):
+        """
+        等待主动上报消息(如+HTTPSRECV、+HTTPRECV等)
+        
+        Args:
+            pattern: 匹配模式(字符串或正则表达式)
+            timeout: 超时时间(秒)
+        
+        Returns:
+            tuple: (success: bool, response: str)
+        """
+        if not self.serial_conn or not self.serial_conn.is_open:
+            return False, "串口未连接"
+        
+        start_time = time.time()
+        response_lines = []
+        pattern_compiled = re.compile(pattern) if isinstance(pattern, str) else pattern
+        
+        while time.time() - start_time < timeout:
+            if self.serial_conn.in_waiting > 0:
+                line = self.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                if line:
+                    response_lines.append(line)
+                    print(f"接收: {line}")
+                    
+                    if pattern_compiled.search(line):
+                        return True, "\n".join(response_lines)
+            
+            time.sleep(0.1)
+        
+        return False, f"超时: {timeout}秒内未收到主动上报\n" + "\n".join(response_lines)
+    
+    def read_binary_data(self, max_bytes=None, timeout=30):
+        """
+        读取二进制数据(用于接收文件等场景)
+        
+        Args:
+            max_bytes: 最大读取字节数,None表示读取所有可用数据
+            timeout: 超时时间(秒)
+        
+        Returns:
+            bytes: 读取的二进制数据
+        """
+        if not self.serial_conn or not self.serial_conn.is_open:
+            return b""
+        
+        start_time = time.time()
+        data = b""
+        
+        while time.time() - start_time < timeout:
+            if self.serial_conn.in_waiting > 0:
+                if max_bytes:
+                    remaining = max_bytes - len(data)
+                    if remaining <= 0:
+                        break
+                    chunk = self.serial_conn.read(min(remaining, self.serial_conn.in_waiting))
+                else:
+                    chunk = self.serial_conn.read(self.serial_conn.in_waiting)
+                
+                if chunk:
+                    data += chunk
+                    start_time = time.time()  # 重置超时计时器
+            else:
+                # 如果没有更多数据,等待一小段时间
+                time.sleep(0.1)
+        
+        return data
+    
+    def check_ppp_status(self):
+        """
+        检查PPP拨号状态
+        
+        Returns:
+            tuple: (is_connected: bool, ip_address: str)
+        """
+        success, response = self.send_at_command("AT+XIIC?", timeout=5)
+        if not success:
+            return False, None
+        
+        # 解析响应格式: +XIIC: <state>,<ip>
+        # 注意:手册说明state前面可能有4个空格
+        xiic_pattern = re.compile(r'\+XIIC:\s*(\d+),([0-9.]+)')
+        match = xiic_pattern.search(response)
+        
+        if match:
+            state = int(match.group(1))
+            ip = match.group(2)
+            
+            # state=1表示已连接,且IP不是0.0.0.0
+            if state == 1 and ip != "0.0.0.0":
+                return True, ip
+            else:
+                return False, ip
+        else:
+            print(f"警告: 无法解析PPP状态响应: {response}")
+            return False, None
+    
+    def check_network_registration(self):
+        """
+        检查网络注册状态
+        
+        Returns:
+            bool: True表示已注册,False表示未注册
+        """
+        success, response = self.send_at_command("AT+CREG?", timeout=5)
+        if not success:
+            return False
+        
+        # 解析响应格式: +CREG: <n>,<stat>
+        # stat=1或5表示已注册
+        creg_pattern = re.compile(r'\+CREG:\s*\d+,([15])')
+        match = creg_pattern.search(response)
+        
+        if match:
+            return True
+        else:
+            print(f"警告: 网络未注册,响应: {response}")
+            return False
+    
+    def set_apn(self, apn_name, cid=1, pdp_type="IP"):
+        """
+        设置APN参数
+        
+        Args:
+            apn_name: APN名称(如"CMNET")
+            cid: 上下文标识符,默认1
+            pdp_type: PDP类型,默认"IP"
+        
+        Returns:
+            bool: 是否设置成功
+        """
+        command = f'AT+CGDCONT={cid},"{pdp_type}","{apn_name}"'
+        success, response = self.send_at_command(command, timeout=5)
+        return success
+    
+    def establish_ppp_connection(self, apn_name=None, max_retries=3):
+        """
+        建立PPP连接
+        
+        Args:
+            apn_name: APN名称(可选,如果为None则不设置APN)
+            max_retries: 最大重试次数
+        
+        Returns:
+            tuple: (success: bool, ip_address: str)
+        """
+        # 检查网络注册状态
+        print("检查网络注册状态...")
+        if not self.check_network_registration():
+            print("❌ 网络未注册,无法建立PPP连接")
+            return False, None
+        
+        print("✅ 网络已注册")
+        
+        # 设置APN(如果提供)
+        if apn_name:
+            print(f"设置APN: {apn_name}")
+            if not self.set_apn(apn_name):
+                print(f"警告: APN设置失败,继续尝试拨号")
+        
+        # 尝试建立PPP连接
+        for attempt in range(1, max_retries + 1):
+            print(f"\n尝试建立PPP连接 (第{attempt}次)...")
+            success, response = self.send_at_command("AT+XIIC=1", timeout=30)
+            
+            if not success:
+                print(f"❌ PPP连接失败: {response}")
+                if attempt < max_retries:
+                    print(f"等待3秒后重试...")
+                    time.sleep(3)
+                continue
+            
+            # 等待一下,然后查询状态确认
+            time.sleep(2)
+            is_connected, ip = self.check_ppp_status()
+            
+            if is_connected:
+                print(f"✅ PPP连接成功,IP地址: {ip}")
+                return True, ip
+            else:
+                print(f"❌ PPP连接状态检查失败,IP: {ip}")
+                if attempt < max_retries:
+                    print(f"等待3秒后重试...")
+                    time.sleep(3)
+        
+        return False, None
+
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_http_client.py b/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_http_client.py
new file mode 100644 (file)
index 0000000..4d68bf2
--- /dev/null
@@ -0,0 +1,910 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+N706A HTTP/HTTPS客户端
+支持HTTP和HTTPS协议,通过URL自动判断协议类型
+"""
+
+import urllib.parse
+import re
+import time
+import os
+from n706a_at_handler import ATCommandHandler
+
+
+class N706AHTTPClient:
+    """基于AT指令的HTTP/HTTPS客户端,支持通过URL自动判断协议类型"""
+    
+    def __init__(self, at_handler):
+        """
+        初始化HTTP/HTTPS客户端
+        
+        Args:
+            at_handler: ATCommandHandler实例
+        """
+        self.at = at_handler
+        self.is_https = False
+        self.current_url = None
+        
+    def add_certificate(self, cert_name, cert_data):
+        """
+        添加SSL证书到模块(仅HTTPS需要)
+        
+        Args:
+            cert_name: 证书文件名(如 "ca.crt")
+            cert_data: 证书内容(字节数据或字符串)
+        
+        Returns:
+            bool: 是否添加成功
+        """
+        # 如果是字符串,转换为字节
+        if isinstance(cert_data, str):
+            cert_data = cert_data.encode('utf-8')
+        
+        cert_length = len(cert_data)
+        
+        # 清空输入缓冲区
+        self.at.serial_conn.reset_input_buffer()
+        
+        # 直接发送AT+CERTADD命令(不使用send_at_command,因为响应是CONNECT而不是OK)
+        command = f'AT+CERTADD={cert_name},{cert_length}'
+        cmd_bytes = (command + "\r\n").encode('utf-8')
+        self.at.serial_conn.write(cmd_bytes)
+        print(f"发送: {command}")
+        
+        # 等待CONNECT提示符
+        print("等待'CONNECT'提示符...")
+        start_time = time.time()
+        connect_received = False
+        response_lines = []
+        
+        while time.time() - start_time < 5:
+            if self.at.serial_conn.in_waiting > 0:
+                line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                if line:
+                    response_lines.append(line)
+                    print(f"接收: {line}")
+                    
+                    if "CONNECT" in line.upper():
+                        connect_received = True
+                        break
+                    
+                    if "ERROR" in line:
+                        print(f"❌ 证书添加命令失败: {line}")
+                        return False
+            time.sleep(0.1)
+        
+        if not connect_received:
+            newline = '\n'
+            print(f"❌ 错误: 未收到'CONNECT'提示符,响应: {newline.join(response_lines)}")
+            return False
+        
+        # 发送证书数据
+        print(f"发送证书数据 ({cert_length} 字节)...")
+        self.at.serial_conn.write(cert_data)
+        self.at.serial_conn.flush()
+        
+        # 等待证书添加完成响应
+        print("等待证书添加完成响应...")
+        start_time = time.time()
+        certadd_received = False
+        response_lines = []
+        
+        while time.time() - start_time < 30:
+            if self.at.serial_conn.in_waiting > 0:
+                line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                if line:
+                    response_lines.append(line)
+                    print(f"接收: {line}")
+                    
+                    if "+CERTADD:" in line:
+                        certadd_received = True
+                        if f"{cert_length},OK" in line or ",OK" in line:
+                            print(f"✅ 证书 {cert_name} 添加成功")
+                            return True
+                        elif "ERROR" in line:
+                            print(f"❌ 证书添加失败: {line}")
+                            return False
+            time.sleep(0.1)
+        
+        if not certadd_received:
+            newline = '\n'
+            print(f"❌ 错误: 证书添加超时,响应: {newline.join(response_lines)}")
+            return False
+        
+        return False
+    
+    def check_certificate(self, cert_name):
+        """
+        确认证书是否成功写入
+        
+        Args:
+            cert_name: 证书文件名
+        
+        Returns:
+            bool: 证书是否存在
+        """
+        success, response = self.at.send_at_command(
+            f'AT+CERTCHECK={cert_name}',
+            timeout=5
+        )
+        if not success:
+            return False
+        
+        if f"{cert_name},OK" in response:
+            print(f"✅ 证书 {cert_name} 确认成功")
+            return True
+        else:
+            print(f"❌ 证书 {cert_name} 不存在或验证失败: {response}")
+            return False
+    
+    def list_certificates(self):
+        """
+        查询设备中所有已添加的证书列表
+        
+        Returns:
+            list: 证书文件名列表,如果查询失败返回None
+        """
+        success, response = self.at.send_at_command("AT+CERTCHECK?", timeout=5)
+        if not success:
+            print(f"警告: 证书列表查询失败: {response}")
+            return None
+        
+        cert_list = []
+        for line in response.split('\n'):
+            line = line.strip()
+            if line and line not in ['OK', 'AT+CERTCHECK?']:
+                if line.startswith('+CERTCHECK:'):
+                    continue
+                cert_list.append(line)
+        
+        return cert_list
+    
+    def certificate_exists(self, cert_name):
+        """
+        检查证书是否已存在于设备中
+        
+        Args:
+            cert_name: 证书文件名
+        
+        Returns:
+            bool: 证书是否存在
+        """
+        success, response = self.at.send_at_command(
+            f'AT+CERTCHECK={cert_name}',
+            timeout=5
+        )
+        if not success:
+            return False
+        
+        if f"{cert_name},OK" in response:
+            return True
+        else:
+            return False
+    
+    def delete_certificate(self, cert_name):
+        """
+        删除SSL证书
+        
+        Args:
+            cert_name: 证书文件名
+        
+        Returns:
+            bool: 是否删除成功
+        """
+        success, response = self.at.send_at_command(
+            f'AT+CERTDEL={cert_name}',
+            timeout=5
+        )
+        if not success:
+            print(f"❌ 证书删除命令失败: {response}")
+            return False
+        
+        if "OK" in response:
+            print(f"✅ 证书 {cert_name} 已删除")
+            return True
+        else:
+            print(f"⚠️ 证书删除响应异常: {response}")
+            # 即使响应异常,也可能删除成功(如果证书不存在)
+            return "ERROR" not in response.upper()
+    
+    def configure_https(self, ssl_version=3, auth_mode=0, ca_cert=None, client_cert=None, client_key=None):
+        """
+        配置HTTPS SSL参数(仅HTTPS需要)
+        
+        Args:
+            ssl_version: SSL版本,0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2
+            auth_mode: 认证模式,0=不认证, 1=单向认证, 2=双向认证
+            ca_cert: CA证书文件名(单向/双向认证需要)
+            client_cert: 客户端证书文件名(双向认证需要)
+            client_key: 客户端密钥文件名(双向认证需要)
+        """
+        if not self.is_https:
+            print("警告: 当前URL不是HTTPS,无需配置SSL参数")
+            return True
+        
+        # 设置SSL版本
+        success, response = self.at.send_at_command(
+            f'AT+HTTPSCFG="sslversion",{ssl_version}',
+            timeout=5
+        )
+        if not success:
+            print(f"警告: SSL版本配置失败: {response}")
+            return False
+        
+        # 设置认证模式
+        success, response = self.at.send_at_command(
+            f'AT+HTTPSCFG="authmode",{auth_mode}',
+            timeout=5
+        )
+        if not success:
+            print(f"警告: 认证模式配置失败: {response}")
+            return False
+        
+        # 单向认证:配置CA证书
+        if auth_mode == 1:
+            if not ca_cert:
+                print("错误: 单向认证需要CA证书")
+                return False
+            
+            success, response = self.at.send_at_command(
+                f'AT+HTTPSCFG="cacert",{ca_cert}',
+                timeout=5
+            )
+            if not success:
+                print(f"错误: CA证书配置失败: {response}")
+                return False
+            print(f"✅ 已配置CA证书: {ca_cert}")
+        
+        # 双向认证:配置CA证书、客户端证书和密钥
+        elif auth_mode == 2:
+            if not ca_cert or not client_cert or not client_key:
+                print("错误: 双向认证需要CA证书、客户端证书和密钥")
+                return False
+            
+            # 配置CA证书
+            success, response = self.at.send_at_command(
+                f'AT+HTTPSCFG="cacert",{ca_cert}',
+                timeout=5
+            )
+            if not success:
+                print(f"错误: CA证书配置失败: {response}")
+                return False
+            
+            # 配置客户端证书
+            success, response = self.at.send_at_command(
+                f'AT+HTTPSCFG="clientcert",{client_cert}',
+                timeout=5
+            )
+            if not success:
+                print(f"错误: 客户端证书配置失败: {response}")
+                return False
+            
+            # 配置客户端密钥
+            success, response = self.at.send_at_command(
+                f'AT+HTTPSCFG="clientkey",{client_key}',
+                timeout=5
+            )
+            if not success:
+                print(f"错误: 客户端密钥配置失败: {response}")
+                return False
+            
+            print(f"✅ 已配置双向认证证书: CA={ca_cert}, ClientCert={client_cert}, ClientKey={client_key}")
+        
+        return True
+    
+    def verify_https_config(self):
+        """
+        确认HTTPS配置(仅HTTPS需要)
+        
+        Returns:
+            dict: 包含配置信息的字典,如果查询失败返回None
+        """
+        if not self.is_https:
+            return None
+        
+        success, response = self.at.send_at_command("AT+HTTPSCFG?", timeout=5)
+        if not success:
+            print(f"警告: HTTPS配置查询失败: {response}")
+            return None
+        
+        # 解析响应格式: +HTTPSCFG: <sslversion>,<authmode>,<cacert>,<clientcert>,<clientkey>
+        httpcfg_pattern = re.compile(r'\+HTTPSCFG:\s*(\d+),(\d+),([^,]*),([^,]*),([^,\r\n]*)')
+        match = httpcfg_pattern.search(response)
+        
+        if match:
+            config = {
+                'sslversion': int(match.group(1)),
+                'authmode': int(match.group(2)),
+                'cacert': match.group(3).strip() if match.group(3).strip() else None,
+                'clientcert': match.group(4).strip() if match.group(4).strip() else None,
+                'clientkey': match.group(5).strip() if match.group(5).strip() else None
+            }
+            
+            # 打印配置信息
+            ssl_version_names = {0: "SSL3.0", 1: "TLS1.0", 2: "TLS1.1", 3: "TLS1.2"}
+            auth_mode_names = {0: "无需认证", 1: "单向认证", 2: "双向认证"}
+            
+            print(f"  SSL版本: {config['sslversion']} ({ssl_version_names.get(config['sslversion'], '未知')})")
+            print(f"  认证模式: {config['authmode']} ({auth_mode_names.get(config['authmode'], '未知')})")
+            if config['cacert']:
+                print(f"  CA证书: {config['cacert']}")
+            if config['clientcert']:
+                print(f"  客户端证书: {config['clientcert']}")
+            if config['clientkey']:
+                print(f"  客户端密钥: {config['clientkey']}")
+            
+            return config
+        else:
+            print(f"警告: 无法解析HTTPS配置响应: {response}")
+            return None
+    
+    def set_url(self, url, keepalive=0):
+        """
+        设置HTTP/HTTPS请求URL(自动判断协议类型)
+        
+        Args:
+            url: 完整的URL(如 http://example.com/path 或 https://example.com/path)
+            keepalive: 是否启用长连接,0=关闭(默认),1=启用
+        """
+        # 解析URL,判断协议类型
+        parsed = urllib.parse.urlparse(url)
+        self.is_https = parsed.scheme.lower() == 'https'
+        self.current_url = url
+        
+        if not (parsed.scheme.lower() in ['http', 'https']):
+            raise ValueError(f"不支持的URL协议: {parsed.scheme},仅支持 http:// 或 https://")
+        
+        protocol_name = "HTTPS" if self.is_https else "HTTP"
+        print(f"检测到协议类型: {protocol_name}")
+        
+        host = parsed.netloc
+        path = parsed.path
+        if parsed.query:
+            path += "?" + parsed.query
+        
+        # 根据协议类型设置URL
+        if self.is_https:
+            # HTTPS: 使用AT+HTTPSPARA
+            url_param = f"{host}{path}"
+            success, response = self.at.send_at_command(
+                f'AT+HTTPSPARA=url,"{url_param}"',
+                timeout=5
+            )
+            if not success:
+                raise Exception(f"设置HTTPS URL失败: {response}")
+            
+            # 设置端口(HTTPS默认443)
+            port = parsed.port if parsed.port else 443
+            success, response = self.at.send_at_command(
+                f'AT+HTTPSPARA=port,{port}',
+                timeout=5
+            )
+            if not success:
+                raise Exception(f"设置HTTPS端口失败: {response}")
+            
+            # 设置keepalive
+            if keepalive in [0, 1]:
+                success, response = self.at.send_at_command(
+                    f'AT+HTTPSPARA=keepalive,{keepalive}',
+                    timeout=5
+                )
+                if not success:
+                    print(f"警告: keepalive配置失败: {response}")
+        else:
+            # HTTP: 使用AT+HTTPPARA
+            url_param = f"{host}{path}"
+            success, response = self.at.send_at_command(
+                f'AT+HTTPPARA=url,"{url_param}"',
+                timeout=5
+            )
+            if not success:
+                raise Exception(f"设置HTTP URL失败: {response}")
+            
+            # 设置端口(HTTP默认80)
+            port = parsed.port if parsed.port else 80
+            success, response = self.at.send_at_command(
+                f'AT+HTTPPARA=port,{port}',
+                timeout=5
+            )
+            if not success:
+                raise Exception(f"设置HTTP端口失败: {response}")
+            
+            # 设置keepalive
+            if keepalive:
+                success, response = self.at.send_at_command(
+                    f'AT+HTTPPARA=keepalive,{keepalive}',
+                    timeout=5
+                )
+                if not success:
+                    print(f"警告: 设置keepalive失败: {response}")
+        
+        return True
+    
+    def setup_connection(self):
+        """
+        建立HTTP/HTTPS连接(根据URL自动选择)
+        
+        Returns:
+            bool: 是否建立成功
+        """
+        if self.is_https:
+            success, response = self.at.send_at_command("AT+HTTPSSETUP", timeout=15)
+            if not success or "ERROR" in response:
+                raise Exception(f"HTTPS连接建立失败: {response}")
+        else:
+            success, response = self.at.send_at_command("AT+HTTPSETUP", timeout=15)
+            if not success:
+                raise Exception(f"HTTP连接建立失败: {response}")
+        
+        return True
+    
+    def send_get_request(self, headers=None, timeout=60, return_full_response=False):
+        """
+        发送HTTP/HTTPS GET请求
+        
+        Args:
+            headers: 自定义HTTP头字典(可选)
+            timeout: 超时时间(秒)
+            return_full_response: 是否返回完整响应(包括响应头),默认False只返回响应体
+        
+        Returns:
+            bytes: 响应数据(二进制),如果return_full_response=True,返回完整响应
+        """
+        if self.is_https:
+            full_response, header_text, body_data = self._send_https_get_request(headers, timeout)
+        else:
+            full_response, header_text, body_data = self._send_http_get_request(headers, timeout)
+        
+        if return_full_response:
+            return full_response
+        else:
+            return body_data
+    
+    def _send_https_get_request(self, headers=None, timeout=60):
+        """发送HTTPS GET请求"""
+        if headers:
+            # 使用OPEN_MODE (mode=99) 自定义HTTP报文
+            parsed = urllib.parse.urlparse(self.current_url)
+            path = parsed.path
+            if parsed.query:
+                path += "?" + parsed.query
+            
+            # 构建HTTP请求报文
+            http_request = f"GET {path} HTTP/1.1\r\n"
+            http_request += f"Host: {parsed.netloc}\r\n"
+            
+            for key, value in headers.items():
+                http_request += f"{key}: {value}\r\n"
+            
+            http_request += "Connection: close\r\n"
+            http_request += "\r\n"
+            
+            request_length = len(http_request.encode('utf-8'))
+            
+            # 发送AT+HTTPSACTION=99,<length>命令
+            self.at.serial_conn.reset_input_buffer()
+            command = f"AT+HTTPSACTION=99,{request_length}"
+            cmd_bytes = (command + "\r\n").encode('utf-8')
+            self.at.serial_conn.write(cmd_bytes)
+            print(f"发送: {command}")
+            
+            # 等待">"提示符
+            print("等待'>'提示符...")
+            start_time = time.time()
+            prompt_received = False
+            
+            while time.time() - start_time < 5:
+                if self.at.serial_conn.in_waiting > 0:
+                    line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                    if line:
+                        print(f"接收: {line}")
+                        if ">" in line:
+                            prompt_received = True
+                            break
+                        if "ERROR" in line:
+                            raise Exception(f"HTTPS请求命令失败: {line}")
+                time.sleep(0.1)
+            
+            if not prompt_received:
+                raise Exception("未收到'>'提示符")
+            
+            # 发送HTTP请求报文
+            print(f"发送HTTP请求 ({request_length} 字节)...")
+            self.at.serial_conn.write(http_request.encode('utf-8'))
+            self.at.serial_conn.flush()
+            
+            # 等待OK响应
+            print("等待OK响应...")
+            start_time = time.time()
+            ok_received = False
+            
+            while time.time() - start_time < 5:
+                if self.at.serial_conn.in_waiting > 0:
+                    line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                    if line:
+                        print(f"接收: {line}")
+                        if "OK" in line:
+                            ok_received = True
+                            break
+                        if "ERROR" in line:
+                            raise Exception(f"HTTPS请求发送失败: {line}")
+                time.sleep(0.1)
+        else:
+            # 使用标准GET模式 (mode=0)
+            success, response = self.at.send_at_command("AT+HTTPSACTION=0", timeout=5)
+            if not success:
+                raise Exception(f"发送HTTPS请求失败: {response}")
+        
+        # 等待响应数据(二进制模式)
+        return self._receive_http_response(b"+HTTPSRECV:", timeout)
+    
+    def _send_http_get_request(self, headers=None, timeout=60):
+        """发送HTTP GET请求"""
+        # 使用标准GET模式 (mode=0)
+        success, response = self.at.send_at_command("AT+HTTPACTION=0", timeout=5)
+        if not success:
+            raise Exception(f"发送HTTP请求失败: {response}")
+        
+        # 等待响应数据(二进制模式)
+        return self._receive_http_response(b"+HTTPRECV:", timeout)
+    
+    def _receive_http_response(self, recv_indicator, timeout=60):
+        """
+        接收HTTP/HTTPS响应数据(二进制模式)
+        
+        Args:
+            recv_indicator: 接收指示符(b"+HTTPSRECV:" 或 b"+HTTPRECV:")
+            timeout: 超时时间(秒)
+        
+        Returns:
+            tuple: (完整响应数据bytes, 响应头文本str, 响应体bytes)
+        """
+        response_data = b""
+        start_time = time.time()
+        last_data_time = time.time()
+        recv_found = False
+        
+        # 等待接收指示符
+        indicator_name = recv_indicator.decode('utf-8', errors='ignore').replace(':', '')
+        print(f"等待+{indicator_name}:提示符...")
+        while time.time() - start_time < timeout:
+            if self.at.serial_conn.in_waiting > 0:
+                chunk = self.at.serial_conn.read(self.at.serial_conn.in_waiting)
+                if chunk:
+                    response_data += chunk
+                    last_data_time = time.time()
+                    
+                    if recv_indicator in response_data:
+                        recv_found = True
+                        print(f"✅ 收到+{indicator_name}:提示符")
+                        break
+            time.sleep(0.1)
+        
+        if not recv_found:
+            if b"HTTP/" in response_data:
+                recv_found = True
+                print("✅ 检测到HTTP响应数据")
+        
+        if not recv_found:
+            raise Exception(f"未收到+{indicator_name}:提示符或HTTP响应")
+        
+        # 查找HTTP响应头开始位置
+        http_start = response_data.find(b"HTTP/")
+        if http_start == -1:
+            # 继续读取直到找到HTTP响应头
+            print("继续读取数据以查找HTTP响应头...")
+            while time.time() - start_time < 10:
+                if self.at.serial_conn.in_waiting > 0:
+                    chunk = self.at.serial_conn.read(self.at.serial_conn.in_waiting)
+                    if chunk:
+                        response_data += chunk
+                        http_start = response_data.find(b"HTTP/")
+                        if http_start != -1:
+                            break
+                time.sleep(0.1)
+        
+        if http_start == -1:
+            raise Exception("未找到HTTP响应头")
+        
+        # 跳过接收指示符前缀
+        if http_start > 0:
+            print(f"跳过+{indicator_name}:前缀 ({http_start} 字节)")
+            response_data = response_data[http_start:]
+        
+        # 查找HTTP响应头结束位置(\r\n\r\n)
+        print("读取响应头...")
+        header_timeout = 10
+        header_start = time.time()
+        header_received = False
+        body_start_pos = None
+        content_length = None
+        transfer_encoding_chunked = False
+        header_text = ""
+        
+        while time.time() - header_start < header_timeout:
+            if self.at.serial_conn.in_waiting > 0:
+                chunk = self.at.serial_conn.read(self.at.serial_conn.in_waiting)
+                if chunk:
+                    response_data += chunk
+                    
+                    header_end = response_data.find(b"\r\n\r\n")
+                    if header_end != -1:
+                        header_received = True
+                        body_start_pos = header_end + 4
+                        
+                        # 解析响应头
+                        header_text = response_data[:header_end].decode('utf-8', errors='ignore')
+                        print("\nHTTP响应头:")
+                        print(header_text)
+                        
+                        # 检查Transfer-Encoding
+                        if re.search(r'Transfer-Encoding:\s*chunked', header_text, re.IGNORECASE):
+                            transfer_encoding_chunked = True
+                            print("检测到Transfer-Encoding: chunked")
+                        
+                        # 提取Content-Length
+                        content_length_match = re.search(r'Content-Length:\s*(\d+)', header_text, re.IGNORECASE)
+                        if content_length_match:
+                            content_length = int(content_length_match.group(1))
+                            print(f"\n文件大小: {content_length} 字节 ({content_length/1024/1024:.2f} MB)")
+                        
+                        # 检查状态码
+                        status_match = re.search(r'HTTP/\d\.\d\s+(\d+)', header_text)
+                        if status_match:
+                            status_code = int(status_match.group(1))
+                            if status_code != 200:
+                                raise Exception(f"HTTP请求失败,状态码: {status_code}")
+                            print(f"HTTP状态码: {status_code} ✅")
+                        
+                        break
+            time.sleep(0.1)
+        
+        if not header_received:
+            raise Exception("未收到完整的HTTP响应头")
+        
+        # 继续接收响应体数据
+        print("\n开始接收响应数据...")
+        last_progress_time = time.time()
+        progress_interval = 5
+        last_chunked_check_size = 0  # 上次检查chunked编码时的数据大小
+        chunked_check_interval = 1024  # 每1KB检查一次chunked编码结束标记
+        
+        while time.time() - start_time < 600:  # 最多等待10分钟
+            if self.at.serial_conn.in_waiting > 0:
+                chunk = self.at.serial_conn.read(self.at.serial_conn.in_waiting)
+                if chunk:
+                    response_data += chunk
+                    last_data_time = time.time()
+                    
+                    # 对于chunked编码,减少检查频率(每1KB检查一次)
+                    if transfer_encoding_chunked:
+                        current_size = len(response_data)
+                        if current_size - last_chunked_check_size >= chunked_check_interval:
+                            if response_data.endswith(b"0\r\n\r\n") or b"\r\n0\r\n\r\n" in response_data:
+                                print("\n✅ Chunked编码响应接收完成")
+                                break
+                            last_chunked_check_size = current_size
+                    # 如果知道文件大小,使用长度计算避免频繁切片
+                    elif content_length:
+                        # 使用长度计算,避免每次循环都切片
+                        received = len(response_data) - body_start_pos
+                        if received >= content_length:
+                            print(f"\n✅ 文件接收完成: {received} / {content_length} 字节")
+                            break
+                        else:
+                            # 只在需要显示进度时才切片
+                            current_time = time.time()
+                            if current_time - last_progress_time >= progress_interval:
+                                body_data = response_data[body_start_pos:]
+                                progress = (received / content_length) * 100
+                                print(f"接收进度: {progress:.1f}% ({received}/{content_length} 字节, {received/1024/1024:.2f} MB)")
+                                last_progress_time = current_time
+                    else:
+                        # 不知道文件大小,只在需要显示进度时才切片
+                        current_time = time.time()
+                        if current_time - last_progress_time >= progress_interval:
+                            body_data = response_data[body_start_pos:]
+                            received = len(body_data)
+                            print(f"已接收: {received} 字节 ({received/1024/1024:.2f} MB)")
+                            last_progress_time = current_time
+            else:
+                # 如果超过10秒没有新数据,检查是否接收完成
+                if time.time() - last_data_time > 10:
+                    if transfer_encoding_chunked:
+                        # 检查是否有结束标记
+                        if b"0\r\n\r\n" in response_data or response_data.endswith(b"\r\n0\r\n\r\n"):
+                            print("\n响应数据接收完成(chunked编码)")
+                            break
+                        else:
+                            print("\n⚠️ Chunked编码响应接收超时")
+                            break
+                    elif content_length:
+                        received = len(response_data) - body_start_pos
+                        if received >= content_length:
+                            print(f"\n✅ 文件接收完成: {received} / {content_length} 字节")
+                            break
+                        else:
+                            print(f"\n⚠️ 接收超时,已接收: {received} / {content_length} 字节")
+                            break
+                    else:
+                        print("\n响应数据接收完成(超时)")
+                        break
+                time.sleep(0.1)
+        
+        # 提取响应体数据(跳过HTTP响应头)
+        body_data = response_data[body_start_pos:]
+        
+        # 如果是chunked编码,解码chunked格式
+        if transfer_encoding_chunked:
+            body_data = self._decode_chunked_response(body_data)
+        
+        # 返回完整响应数据、响应头文本和响应体
+        return response_data, header_text, body_data
+    
+    def _decode_chunked_response(self, chunked_data):
+        """
+        解码Transfer-Encoding: chunked格式的响应体
+        
+        Args:
+            chunked_data: chunked编码的响应体数据(bytes)
+        
+        Returns:
+            bytes: 解码后的响应体数据
+        """
+        decoded_data = b""
+        data_str = chunked_data.decode('utf-8', errors='ignore')
+        lines = data_str.split('\r\n')
+        
+        i = 0
+        while i < len(lines):
+            # 读取chunk size(十六进制)
+            chunk_size_line = lines[i].strip()
+            if not chunk_size_line:
+                i += 1
+                continue
+            
+            try:
+                # 解析chunk size(可能包含分号后的扩展信息,忽略)
+                chunk_size_str = chunk_size_line.split(';')[0].strip()
+                chunk_size = int(chunk_size_str, 16)
+            except ValueError:
+                # 如果解析失败,尝试查找下一个有效的chunk size
+                i += 1
+                continue
+            
+            if chunk_size == 0:
+                # 最后一个chunk
+                break
+            
+            # 读取chunk数据
+            i += 1
+            if i < len(lines):
+                chunk_data = lines[i]
+                decoded_data += chunk_data.encode('utf-8')[:chunk_size]
+                i += 1  # 跳过chunk数据后的空行
+        
+        return decoded_data
+    
+    def parse_http_response(self, raw_response, header_text=None, body_data=None):
+        """
+        解析HTTP响应(文本模式,用于JSON等文本响应)
+        
+        Args:
+            raw_response: 原始响应字符串或bytes(完整响应或仅响应体)
+            header_text: 响应头文本(可选,如果提供则直接使用)
+            body_data: 响应体数据bytes(可选,如果提供则直接使用)
+        
+        Returns:
+            dict: 包含status_code, headers, body的字典
+        """
+        # 如果提供了header_text和body_data,直接使用
+        if header_text and body_data is not None:
+            headers_text = header_text
+            body_text = body_data.decode('utf-8', errors='ignore') if isinstance(body_data, bytes) else body_data
+        else:
+            # 尝试从raw_response解析
+            if isinstance(raw_response, bytes):
+                raw_response = raw_response.decode('utf-8', errors='ignore')
+            
+            # 查找HTTP响应开始位置
+            http_start = raw_response.find("HTTP/")
+            if http_start == -1:
+                # 如果没有找到HTTP/,可能是只有响应体,尝试直接解析为JSON
+                json_start = raw_response.find("{")
+                if json_start != -1:
+                    # 只有响应体,没有响应头
+                    return {
+                        "status_code": 200,  # 假设成功
+                        "headers": {},
+                        "body": raw_response[json_start:].strip()
+                    }
+                else:
+                    raise Exception("未找到HTTP响应或JSON数据")
+            
+            response_text = raw_response[http_start:]
+            
+            # 分离头部和正文
+            header_end = response_text.find("\r\n\r\n")
+            if header_end == -1:
+                header_end = response_text.find("\n\n")
+            
+            if header_end == -1:
+                json_start = response_text.find("{")
+                if json_start != -1:
+                    headers_text = response_text[:json_start]
+                    body_text = response_text[json_start:]
+                else:
+                    raise Exception("无法解析HTTP响应格式")
+            else:
+                headers_text = response_text[:header_end]
+                body_text = response_text[header_end:].strip()
+        
+        # 解析状态行
+        lines = headers_text.split("\n")
+        status_line = lines[0].strip()
+        status_match = re.search(r'HTTP/\d\.\d\s+(\d+)', status_line)
+        status_code = int(status_match.group(1)) if status_match else 0
+        
+        # 解析头部
+        headers = {}
+        for line in lines[1:]:
+            line = line.strip()
+            if ":" in line:
+                key, value = line.split(":", 1)
+                headers[key.strip()] = value.strip()
+        
+        # 提取JSON正文
+        json_start = body_text.find("{")
+        json_end = body_text.rfind("}") + 1
+        
+        if json_start != -1 and json_end > json_start:
+            body_json = body_text[json_start:json_end]
+        else:
+            body_json = body_text.strip()
+            for prefix in ["+HTTPSRECV:", "+HTTPRECV:", "+HTTPSCLOSED:", "+HTTPSCLOSE:"]:
+                if body_json.startswith(prefix):
+                    body_json = body_json[len(prefix):].strip()
+        
+        return {
+            "status_code": status_code,
+            "headers": headers,
+            "body": body_json
+        }
+    
+    def close_connection(self):
+        """
+        关闭HTTP/HTTPS连接
+        
+        Returns:
+            bool: 是否关闭成功
+        """
+        if self.is_https:
+            success, response = self.at.send_at_command("AT+HTTPSCLOSE", timeout=5)
+        else:
+            success, response = self.at.send_at_command("AT+HTTPCLOSE", timeout=5)
+        return success
+
+
+def load_certificate_file(cert_path):
+    """
+    从文件加载证书内容(辅助函数)
+    
+    Args:
+        cert_path: 证书文件路径
+    
+    Returns:
+        bytes: 证书内容(字节数据)
+    """
+    try:
+        with open(cert_path, 'rb') as f:
+            cert_data = f.read()
+        print(f"✅ 成功加载证书文件: {cert_path} ({len(cert_data)} 字节)")
+        return cert_data
+    except FileNotFoundError:
+        print(f"❌ 证书文件不存在: {cert_path}")
+        return None
+    except Exception as e:
+        print(f"❌ 读取证书文件失败: {e}")
+        return None
+
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_tcp_client.py b/code/sdk/zhihui/leiniao/leiniao_python_test/n706a_tcp_client.py
new file mode 100644 (file)
index 0000000..9ece1e6
--- /dev/null
@@ -0,0 +1,521 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+N706A TCP/TCP+SSL客户端
+支持TCP和TCP+SSL连接,通过参数选择连接类型
+"""
+
+import re
+import time
+from n706a_at_handler import ATCommandHandler
+
+
+class N706ATCPClient:
+    """基于AT指令的TCP/TCP+SSL客户端"""
+    
+    def __init__(self, at_handler, socket_id=0):
+        """
+        初始化TCP客户端
+        
+        Args:
+            at_handler: ATCommandHandler实例
+            socket_id: Socket ID(0-5),默认0
+        """
+        self.at = at_handler
+        self.socket_id = socket_id
+        self.is_ssl = False
+        self.is_connected = False
+        self.server_ip = None
+        self.server_port = None
+        self.transparent_mode = False  # 透传模式标志
+    
+    def configure_ssl(self, ssl_version=3, auth_mode=0, ca_cert=None, client_cert=None, client_key=None):
+        """
+        配置SSL TCP参数(仅TCP+SSL需要)
+        
+        Args:
+            ssl_version: SSL版本,0=SSL3.0, 1=TLS1.0, 2=TLS1.1, 3=TLS1.2, 4=TLS1.3
+            auth_mode: 认证模式,0=不认证, 1=单向认证, 2=双向认证
+            ca_cert: CA证书文件名(单向/双向认证需要)
+            client_cert: 客户端证书文件名(双向认证需要)
+            client_key: 客户端密钥文件名(双向认证需要)
+        
+        Returns:
+            bool: 是否配置成功
+        """
+        # 设置SSL版本
+        success, response = self.at.send_at_command(
+            f'AT+SSLTCPCFG="sslversion",{ssl_version}',
+            timeout=5
+        )
+        if not success:
+            print(f"警告: SSL版本配置失败: {response}")
+            return False
+        
+        # 设置认证模式
+        success, response = self.at.send_at_command(
+            f'AT+SSLTCPCFG="authmode",{auth_mode}',
+            timeout=5
+        )
+        if not success:
+            print(f"警告: 认证模式配置失败: {response}")
+            return False
+        
+        # 单向认证:配置CA证书
+        if auth_mode == 1:
+            if not ca_cert:
+                print("错误: 单向认证需要CA证书")
+                return False
+            
+            success, response = self.at.send_at_command(
+                f'AT+SSLTCPCFG="cacert",{ca_cert}',
+                timeout=5
+            )
+            if not success:
+                print(f"错误: CA证书配置失败: {response}")
+                return False
+            print(f"✅ 已配置CA证书: {ca_cert}")
+        
+        # 双向认证:配置CA证书、客户端证书和密钥
+        elif auth_mode == 2:
+            if not ca_cert or not client_cert or not client_key:
+                print("错误: 双向认证需要CA证书、客户端证书和密钥")
+                return False
+            
+            # 配置CA证书
+            success, response = self.at.send_at_command(
+                f'AT+SSLTCPCFG="cacert",{ca_cert}',
+                timeout=5
+            )
+            if not success:
+                print(f"错误: CA证书配置失败: {response}")
+                return False
+            
+            # 配置客户端证书
+            success, response = self.at.send_at_command(
+                f'AT+SSLTCPCFG="clientcert",{client_cert}',
+                timeout=5
+            )
+            if not success:
+                print(f"错误: 客户端证书配置失败: {response}")
+                return False
+            
+            # 配置客户端密钥
+            success, response = self.at.send_at_command(
+                f'AT+SSLTCPCFG="clientkey",{client_key}',
+                timeout=5
+            )
+            if not success:
+                print(f"错误: 客户端密钥配置失败: {response}")
+                return False
+            
+            print(f"✅ 已配置双向认证证书: CA={ca_cert}, ClientCert={client_cert}, ClientKey={client_key}")
+        
+        return True
+    
+    def connect(self, server_ip, server_port, use_ssl=False, transparent_mode=False):
+        """
+        建立TCP或TCP+SSL连接
+        
+        Args:
+            server_ip: 服务器IP地址或域名
+            server_port: 服务器端口
+            use_ssl: 是否使用SSL/TLS加密,默认False(普通TCP)
+            transparent_mode: 是否使用透传模式,默认False(非透传)
+        
+        Returns:
+            bool: 是否连接成功
+        """
+        self.server_ip = server_ip
+        self.server_port = server_port
+        self.is_ssl = use_ssl
+        self.transparent_mode = transparent_mode
+        
+        if use_ssl:
+            # SSL TCP连接
+            mode = 1 if transparent_mode else 0
+            command = f'AT+SSLTCPSETUP={self.socket_id},{server_ip},{server_port},{mode}'
+            success, response = self.at.send_at_command(command, timeout=60)
+            
+            if not success:
+                print(f"❌ SSL TCP连接失败: {response}")
+                return False
+            
+            # 等待连接结果(+SSLTCPSETUP: <n>,<status> 或 CONNECT)
+            if transparent_mode:
+                # 透传模式会返回CONNECT
+                if "CONNECT" in response:
+                    self.is_connected = True
+                    print(f"✅ SSL TCP连接成功(透传模式): {server_ip}:{server_port}")
+                    return True
+            else:
+                # 非透传模式会返回+SSLTCPSETUP: <n>,OK
+                sslsetup_pattern = re.compile(r'\+SSLTCPSETUP:\s*(\d+),(OK|FAIL|ERROR|AUTHFAIL)')
+                match = sslsetup_pattern.search(response)
+                if match:
+                    status = match.group(2)
+                    if status == "OK":
+                        self.is_connected = True
+                        print(f"✅ SSL TCP连接成功: {server_ip}:{server_port}")
+                        return True
+                    else:
+                        print(f"❌ SSL TCP连接失败: {status}")
+                        return False
+            
+            # 如果没有匹配到预期响应,尝试等待主动上报
+            print("等待SSL TCP连接结果...")
+            success, response = self.at.wait_for_unsolicited(r"\+SSLTCPSETUP:", timeout=30)
+            if success:
+                sslsetup_pattern = re.compile(r'\+SSLTCPSETUP:\s*(\d+),(OK|FAIL|ERROR|AUTHFAIL)')
+                match = sslsetup_pattern.search(response)
+                if match:
+                    status = match.group(2)
+                    if status == "OK":
+                        self.is_connected = True
+                        print(f"✅ SSL TCP连接成功: {server_ip}:{server_port}")
+                        return True
+            
+            print(f"❌ SSL TCP连接失败: {response}")
+            return False
+        else:
+            # 普通TCP连接
+            command = f'AT+TCPSETUP={self.socket_id},{server_ip},{server_port}'
+            success, response = self.at.send_at_command(command, timeout=60)
+            
+            if not success:
+                print(f"❌ TCP连接失败: {response}")
+                return False
+            
+            # 等待连接结果(+TCPSETUP: <n>,OK 或 +TCPSETUP: <n>,FAIL)
+            tcpsetup_pattern = re.compile(r'\+TCPSETUP:\s*(\d+),(OK|FAIL|ERROR)')
+            match = tcpsetup_pattern.search(response)
+            if match:
+                status = match.group(2)
+                if status == "OK":
+                    self.is_connected = True
+                    print(f"✅ TCP连接成功: {server_ip}:{server_port}")
+                    return True
+                else:
+                    print(f"❌ TCP连接失败: {status}")
+                    return False
+            
+            # 如果没有匹配到预期响应,尝试等待主动上报
+            print("等待TCP连接结果...")
+            success, response = self.at.wait_for_unsolicited(r"\+TCPSETUP:", timeout=30)
+            if success:
+                tcpsetup_pattern = re.compile(r'\+TCPSETUP:\s*(\d+),(OK|FAIL|ERROR)')
+                match = tcpsetup_pattern.search(response)
+                if match:
+                    status = match.group(2)
+                    if status == "OK":
+                        self.is_connected = True
+                        print(f"✅ TCP连接成功: {server_ip}:{server_port}")
+                        return True
+            
+            print(f"❌ TCP连接失败: {response}")
+            return False
+    
+    def send(self, data):
+        """
+        发送TCP/TCP+SSL数据
+        
+        Args:
+            data: 要发送的数据(bytes或str)
+        
+        Returns:
+            bool: 是否发送成功
+        """
+        if not self.is_connected:
+            print("❌ 连接未建立,无法发送数据")
+            return False
+        
+        # 如果是字符串,转换为字节
+        if isinstance(data, str):
+            data = data.encode('utf-8')
+        
+        data_length = len(data)
+        
+        if self.is_ssl:
+            # SSL TCP发送
+            return self._send_ssl_tcp_data(data, data_length)
+        else:
+            # 普通TCP发送
+            return self._send_tcp_data(data, data_length)
+    
+    def _send_tcp_data(self, data, data_length):
+        """发送普通TCP数据"""
+        # 使用buffer模式发送(不包含content参数)
+        command = f'AT+TCPSEND={self.socket_id},{data_length}'
+        self.at.serial_conn.reset_input_buffer()
+        cmd_bytes = (command + "\r\n").encode('utf-8')
+        self.at.serial_conn.write(cmd_bytes)
+        print(f"发送: {command}")
+        
+        # 等待">"提示符
+        print("等待'>'提示符...")
+        start_time = time.time()
+        prompt_received = False
+        
+        while time.time() - start_time < 5:
+            if self.at.serial_conn.in_waiting > 0:
+                line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                if line:
+                    print(f"接收: {line}")
+                    if ">" in line:
+                        prompt_received = True
+                        break
+                    if "ERROR" in line:
+                        print(f"❌ TCP发送命令失败: {line}")
+                        return False
+            time.sleep(0.1)
+        
+        if not prompt_received:
+            print("❌ 未收到'>'提示符")
+            return False
+        
+        # 延迟50-100ms(根据手册建议)
+        time.sleep(0.1)
+        
+        # 发送数据
+        print(f"发送TCP数据 ({data_length} 字节)...")
+        self.at.serial_conn.write(data)
+        self.at.serial_conn.flush()
+        
+        # 等待发送完成响应(+TCPSEND: <n>,<length>)
+        print("等待发送完成响应...")
+        start_time = time.time()
+        send_complete = False
+        
+        while time.time() - start_time < 30:
+            if self.at.serial_conn.in_waiting > 0:
+                line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                if line:
+                    print(f"接收: {line}")
+                    if f"+TCPSEND: {self.socket_id},{data_length}" in line:
+                        send_complete = True
+                        print(f"✅ TCP数据发送成功: {data_length} 字节")
+                        break
+                    if "ERROR" in line or "FAIL" in line:
+                        print(f"❌ TCP数据发送失败: {line}")
+                        return False
+            time.sleep(0.1)
+        
+        if not send_complete:
+            print("❌ TCP数据发送超时")
+            return False
+        
+        return True
+    
+    def _send_ssl_tcp_data(self, data, data_length):
+        """发送SSL TCP数据"""
+        # SSL TCP发送命令格式: AT+SSLTCPSEND=<n>,<data_length>
+        command = f'AT+SSLTCPSEND={self.socket_id},{data_length}'
+        self.at.serial_conn.reset_input_buffer()
+        cmd_bytes = (command + "\r\n").encode('utf-8')
+        self.at.serial_conn.write(cmd_bytes)
+        print(f"发送: {command}")
+        
+        # 等待">"提示符
+        print("等待'>'提示符...")
+        start_time = time.time()
+        prompt_received = False
+        
+        while time.time() - start_time < 5:
+            if self.at.serial_conn.in_waiting > 0:
+                line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                if line:
+                    print(f"接收: {line}")
+                    if ">" in line:
+                        prompt_received = True
+                        break
+                    if "ERROR" in line:
+                        print(f"❌ SSL TCP发送命令失败: {line}")
+                        return False
+            time.sleep(0.1)
+        
+        if not prompt_received:
+            print("❌ 未收到'>'提示符")
+            return False
+        
+        # 发送数据
+        print(f"发送SSL TCP数据 ({data_length} 字节)...")
+        self.at.serial_conn.write(data)
+        self.at.serial_conn.flush()
+        
+        # 等待发送完成响应(+SSLTCPSEND: <n>,OK 或 +SSLTCPSEND: <n>,FAIL)
+        print("等待发送完成响应...")
+        start_time = time.time()
+        send_complete = False
+        
+        while time.time() - start_time < 30:
+            if self.at.serial_conn.in_waiting > 0:
+                line = self.at.serial_conn.readline().decode('utf-8', errors='ignore').strip()
+                if line:
+                    print(f"接收: {line}")
+                    if f"+SSLTCPSEND: {self.socket_id},OK" in line:
+                        send_complete = True
+                        print(f"✅ SSL TCP数据发送成功: {data_length} 字节")
+                        break
+                    if "FAIL" in line or "ERROR" in line:
+                        print(f"❌ SSL TCP数据发送失败: {line}")
+                        return False
+            time.sleep(0.1)
+        
+        if not send_complete:
+            print("❌ SSL TCP数据发送超时")
+            return False
+        
+        return True
+    
+    def receive(self, max_length=2048, timeout=30):
+        """
+        接收TCP/TCP+SSL数据(非透传模式)
+        
+        Args:
+            max_length: 最大接收长度(1-2048字节),默认2048
+            timeout: 超时时间(秒),默认30
+        
+        Returns:
+            bytes: 接收到的数据,如果失败返回None
+        """
+        if not self.is_connected:
+            print("❌ 连接未建立,无法接收数据")
+            return None
+        
+        if self.transparent_mode:
+            print("⚠️ 透传模式下,请使用被动接收方式(等待+TCPRECV或+SSLTCPRECV)")
+            return None
+        
+        if self.is_ssl:
+            # SSL TCP读取
+            command = f'AT+SSLTCPREAD={self.socket_id},{max_length}'
+        else:
+            # 普通TCP读取
+            command = f'AT+TCPREAD={self.socket_id},{max_length}'
+        
+        success, response = self.at.send_at_command(command, timeout=timeout)
+        if not success:
+            print(f"❌ 读取数据失败: {response}")
+            return None
+        
+        # 解析响应
+        if self.is_ssl:
+            # +SSLTCPREAD: <n>,<len>,<data>
+            pattern = re.compile(r'\+SSLTCPREAD:\s*(\d+),(\d+),(.+)')
+        else:
+            # +TCPREAD: <n>,<len>,<data>
+            pattern = re.compile(r'\+TCPREAD:\s*(\d+),(\d+),(.+)')
+        
+        match = pattern.search(response)
+        if match:
+            length = int(match.group(2))
+            data_str = match.group(3).strip()
+            
+            # 尝试解析数据(可能是ASCII或HEX)
+            try:
+                # 先尝试作为ASCII字符串
+                data = data_str.encode('utf-8')
+                if len(data) == length:
+                    return data
+                # 如果长度不匹配,尝试作为HEX字符串
+                data = bytes.fromhex(data_str)
+                if len(data) == length:
+                    return data
+            except:
+                pass
+            
+            # 如果解析失败,返回原始字符串的字节
+            return data_str.encode('utf-8')
+        
+        return None
+    
+    def wait_for_data(self, timeout=30):
+        """
+        被动等待接收数据(透传或非透传模式都支持)
+        
+        Args:
+            timeout: 超时时间(秒),默认30
+        
+        Returns:
+            bytes: 接收到的数据,如果超时返回None
+        """
+        if not self.is_connected:
+            print("❌ 连接未建立,无法接收数据")
+            return None
+        
+        if self.is_ssl:
+            # 等待+SSLTCPRECV:提示符
+            pattern = r"\+SSLTCPRECV:"
+        else:
+            # 等待+TCPRECV:提示符
+            pattern = r"\+TCPRECV:"
+        
+        success, response = self.at.wait_for_unsolicited(pattern, timeout=timeout)
+        if not success:
+            print(f"等待数据超时: {timeout}秒")
+            return None
+        
+        # 解析接收到的数据
+        if self.is_ssl:
+            # +SSLTCPRECV: <n>,<len>,<data>
+            recv_pattern = re.compile(r'\+SSLTCPRECV:\s*(\d+),(\d+),(.+)')
+        else:
+            # +TCPRECV: <n>,<len>,<data> 或 +TCPRECV: <n>
+            recv_pattern = re.compile(r'\+TCPRECV:\s*(\d+)(?:,(\d+),(.+))?')
+        
+        match = recv_pattern.search(response)
+        if match:
+            if self.is_ssl:
+                length = int(match.group(2))
+                data_str = match.group(3).strip()
+            else:
+                if match.group(2):  # 有数据
+                    length = int(match.group(2))
+                    data_str = match.group(3).strip()
+                else:  # 只有socket ID,需要主动读取
+                    print("收到+TCPRECV提示,需要主动读取数据")
+                    return self.receive()
+            
+            # 尝试解析数据
+            try:
+                # 先尝试作为ASCII字符串
+                data = data_str.encode('utf-8')
+                if len(data) == length:
+                    return data
+                # 如果长度不匹配,尝试作为HEX字符串
+                data = bytes.fromhex(data_str)
+                if len(data) == length:
+                    return data
+            except:
+                pass
+            
+            # 如果解析失败,返回原始字符串的字节
+            return data_str.encode('utf-8')
+        
+        return None
+    
+    def close(self):
+        """
+        关闭TCP/TCP+SSL连接
+        
+        Returns:
+            bool: 是否关闭成功
+        """
+        if not self.is_connected:
+            print("⚠️ 连接未建立,无需关闭")
+            return True
+        
+        if self.is_ssl:
+            command = f'AT+SSLTCPCLOSE={self.socket_id}'
+        else:
+            command = f'AT+TCPCLOSE={self.socket_id}'
+        
+        success, response = self.at.send_at_command(command, timeout=30)
+        if success:
+            self.is_connected = False
+            print(f"✅ 连接已关闭")
+            return True
+        else:
+            print(f"❌ 关闭连接失败: {response}")
+            return False
+
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/performance_analysis.md b/code/sdk/zhihui/leiniao/leiniao_python_test/performance_analysis.md
new file mode 100644 (file)
index 0000000..8171d85
--- /dev/null
@@ -0,0 +1,139 @@
+# 下载速度性能分析\r
+\r
+## 问题描述\r
+- `download_mp4_at.py`: 下载速度 0.02 MB/s\r
+- `at_test_download_file.py`: 下载速度 0.01 MB/s(慢了一倍)\r
+\r
+## 关键差异分析\r
+\r
+### 1. 数据接收循环中的性能差异\r
+\r
+#### `download_mp4_at.py` (lines 328-373)\r
+```python\r
+while time.time() - start_time < 600:\r
+    if at_handler.serial_conn.in_waiting > 0:\r
+        chunk = at_handler.serial_conn.read(at_handler.serial_conn.in_waiting)\r
+        if chunk:\r
+            response_data += chunk\r
+            last_data_time = time.time()\r
+            \r
+            # 如果知道文件大小,显示进度\r
+            if content_length:\r
+                body_data = response_data[body_start_pos:]  # 只在需要时切片\r
+                received = len(body_data)\r
+                if received >= content_length:\r
+                    break\r
+                else:\r
+                    # 每5秒打印一次进度\r
+                    current_time = time.time()\r
+                    if current_time - last_progress_time >= progress_interval:\r
+                        # 进度计算和打印\r
+                        last_progress_time = current_time\r
+```\r
+\r
+**特点:**\r
+- 切片操作 `response_data[body_start_pos:]` 只在需要显示进度时执行(每5秒一次)\r
+- 逻辑简单,条件判断少\r
+- 没有额外的字符串搜索操作\r
+\r
+#### `n706a_http_client.py` (lines 645-700)\r
+```python\r
+while time.time() - start_time < 600:\r
+    if self.at.serial_conn.in_waiting > 0:\r
+        chunk = self.at.serial_conn.read(self.at.serial_conn.in_waiting)\r
+        if chunk:\r
+            response_data += chunk\r
+            last_data_time = time.time()\r
+            \r
+            # 对于chunked编码,检查是否收到结束标记\r
+            if transfer_encoding_chunked:\r
+                if response_data.endswith(b"0\r\n\r\n") or b"\r\n0\r\n\r\n" in response_data:\r
+                    break\r
+            # 如果知道文件大小,显示进度\r
+            elif content_length:\r
+                body_data = response_data[body_start_pos:]  # 每次循环都切片!\r
+                received = len(body_data)\r
+                if received >= content_length:\r
+                    break\r
+                else:\r
+                    current_time = time.time()\r
+                    if current_time - last_progress_time >= progress_interval:\r
+                        # 进度计算和打印\r
+                        last_progress_time = current_time\r
+```\r
+\r
+**问题:**\r
+1. **每次循环都执行切片操作**:`body_data = response_data[body_start_pos:]` 在每次有数据时都会执行,即使不需要显示进度\r
+2. **额外的字符串搜索**:对于chunked编码,每次循环都检查 `response_data.endswith()` 和 `in` 操作\r
+3. **条件判断更复杂**:有 `if transfer_encoding_chunked` 分支\r
+\r
+### 2. 性能影响分析\r
+\r
+#### 切片操作的开销\r
+- `response_data[body_start_pos:]` 会创建一个新的字节对象\r
+- 如果 `response_data` 很大(比如几MB),这个操作会复制大量数据\r
+- 在 `n706a_http_client.py` 中,这个操作在**每次接收到数据时**都执行\r
+- 在 `download_mp4_at.py` 中,这个操作**只在需要显示进度时**执行(每5秒一次)\r
+\r
+#### 计算频率差异\r
+假设每秒接收 20KB 数据:\r
+- `download_mp4_at.py`: 切片操作约 0.2 次/秒(每5秒一次)\r
+- `n706a_http_client.py`: 切片操作约 10-100 次/秒(取决于串口缓冲区大小和读取频率)\r
+\r
+如果串口每次读取 1KB,那么每秒会读取约 20 次,每次都会执行切片操作。\r
+\r
+### 3. 优化建议\r
+\r
+#### 方案1:延迟切片操作(推荐)\r
+只在需要检查进度或完成条件时才执行切片:\r
+\r
+```python\r
+# 如果知道文件大小,显示进度\r
+elif content_length:\r
+    # 只在需要时计算已接收的数据量\r
+    current_time = time.time()\r
+    need_check = (current_time - last_progress_time >= progress_interval) or True  # 简化:总是检查完成条件\r
+    \r
+    if need_check:\r
+        body_data = response_data[body_start_pos:]  # 只在需要时切片\r
+        received = len(body_data)\r
+        if received >= content_length:\r
+            break\r
+        \r
+        if current_time - last_progress_time >= progress_interval:\r
+            # 显示进度\r
+            last_progress_time = current_time\r
+```\r
+\r
+#### 方案2:使用已接收长度缓存\r
+避免频繁切片,使用长度计算:\r
+\r
+```python\r
+# 在循环外初始化\r
+initial_body_length = len(response_data) - body_start_pos if len(response_data) > body_start_pos else 0\r
+\r
+# 在循环中\r
+elif content_length:\r
+    # 使用增量计算,避免切片\r
+    received = len(response_data) - body_start_pos\r
+    if received >= content_length:\r
+        break\r
+```\r
+\r
+#### 方案3:减少chunked编码检查频率\r
+对于chunked编码的检查,也应该减少频率:\r
+\r
+```python\r
+if transfer_encoding_chunked:\r
+    # 只在特定条件下检查(比如每接收一定数据量后)\r
+    if len(response_data) % 1024 == 0:  # 每1KB检查一次\r
+        if response_data.endswith(b"0\r\n\r\n") or b"\r\n0\r\n\r\n" in response_data:\r
+            break\r
+```\r
+\r
+## 结论\r
+\r
+**主要原因:** `n706a_http_client.py` 在每次接收到数据时都执行 `response_data[body_start_pos:]` 切片操作,而 `download_mp4_at.py` 只在需要显示进度时(每5秒)才执行。对于大文件,频繁的切片操作会显著影响性能。\r
+\r
+**建议修复:** 优化 `_receive_http_response` 方法,减少不必要的切片操作和字符串搜索操作。\r
+\r
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/performance_optimization_summary.md b/code/sdk/zhihui/leiniao/leiniao_python_test/performance_optimization_summary.md
new file mode 100644 (file)
index 0000000..2997241
--- /dev/null
@@ -0,0 +1,91 @@
+# 性能优化总结\r
+\r
+## 问题\r
+`at_test_download_file.py` 的下载速度(0.01 MB/s)比 `download_mp4_at.py`(0.02 MB/s)慢了一倍。\r
+\r
+## 根本原因\r
+在 `n706a_http_client.py` 的 `_receive_http_response` 方法中,数据接收循环存在性能瓶颈:\r
+\r
+1. **频繁的切片操作**:每次接收到数据时都执行 `response_data[body_start_pos:]`,这会创建新的字节对象并复制大量数据\r
+2. **频繁的字符串搜索**:对于chunked编码,每次循环都检查结束标记\r
+3. **不必要的计算**:即使不需要显示进度,也在每次循环中计算已接收的数据量\r
+\r
+## 优化方案\r
+\r
+### 1. 使用长度计算替代频繁切片\r
+**优化前:**\r
+```python\r
+elif content_length:\r
+    body_data = response_data[body_start_pos:]  # 每次循环都切片\r
+    received = len(body_data)\r
+    if received >= content_length:\r
+        break\r
+```\r
+\r
+**优化后:**\r
+```python\r
+elif content_length:\r
+    # 使用长度计算,避免每次循环都切片\r
+    received = len(response_data) - body_start_pos\r
+    if received >= content_length:\r
+        break\r
+    else:\r
+        # 只在需要显示进度时才切片\r
+        current_time = time.time()\r
+        if current_time - last_progress_time >= progress_interval:\r
+            body_data = response_data[body_start_pos:]  # 每5秒才切片一次\r
+            progress = (received / content_length) * 100\r
+            print(f"接收进度: {progress:.1f}% ...")\r
+            last_progress_time = current_time\r
+```\r
+\r
+### 2. 减少chunked编码检查频率\r
+**优化前:**\r
+```python\r
+if transfer_encoding_chunked:\r
+    if response_data.endswith(b"0\r\n\r\n") or b"\r\n0\r\n\r\n" in response_data:\r
+        break\r
+```\r
+\r
+**优化后:**\r
+```python\r
+last_chunked_check_size = 0\r
+chunked_check_interval = 1024  # 每1KB检查一次\r
+\r
+if transfer_encoding_chunked:\r
+    current_size = len(response_data)\r
+    if current_size - last_chunked_check_size >= chunked_check_interval:\r
+        if response_data.endswith(b"0\r\n\r\n") or b"\r\n0\r\n\r\n" in response_data:\r
+            break\r
+        last_chunked_check_size = current_size\r
+```\r
+\r
+### 3. 优化超时检查逻辑\r
+在超时检查分支中也使用长度计算,避免不必要的切片。\r
+\r
+## 性能提升预期\r
+\r
+### 优化前\r
+- 假设每秒接收 20KB 数据,串口每次读取 1KB\r
+- 每秒执行约 20 次切片操作\r
+- 对于 10MB 文件,需要约 500 秒,执行约 10,000 次切片操作\r
+\r
+### 优化后\r
+- 切片操作减少到每 5 秒一次(显示进度时)\r
+- 对于 10MB 文件,约执行 100 次切片操作(减少 99%)\r
+- 使用长度计算替代大部分切片操作,性能提升显著\r
+\r
+## 预期效果\r
+- **下载速度提升**:预计从 0.01 MB/s 提升到接近 0.02 MB/s(与 `download_mp4_at.py` 相当)\r
+- **CPU 使用率降低**:减少不必要的内存复制操作\r
+- **内存使用优化**:减少临时对象的创建\r
+\r
+## 测试建议\r
+1. 使用相同的测试文件(MP4文件)进行对比测试\r
+2. 记录下载时间和速度\r
+3. 监控CPU和内存使用情况\r
+4. 验证下载的文件完整性\r
+\r
+## 修改文件\r
+- `n706a_http_client.py`: 优化 `_receive_http_response` 方法(lines 640-707)\r
+\r
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/request_headers_comparison.md b/code/sdk/zhihui/leiniao/leiniao_python_test/request_headers_comparison.md
new file mode 100644 (file)
index 0000000..cdfb699
--- /dev/null
@@ -0,0 +1,118 @@
+# 请求头信息对比分析\r
+\r
+## 两个请求头对比\r
+\r
+### 请求1\r
+```\r
+请求 URL: https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/auth/getDeviceToken\r
+请求头: {\r
+    'deviceId': 'DEVICE_PC_TEST_001', \r
+    'deviceIccid': '86123456789012345678', \r
+    'timestamp': '1762845627028', \r
+    'nonce': 'DSAgsyFYxaOR', \r
+    'signature': '0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977', \r
+    'deviceModel': 'AB101', \r
+    'deviceVersion': '1.0.0', \r
+    'Content-Type': 'application/json'\r
+}\r
+```\r
+\r
+### 请求2\r
+```\r
+请求 URL: https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/auth/getDeviceToken\r
+请求头: {\r
+    'deviceId': 'DEVICE_PC_TEST_001', \r
+    'deviceIccid': '86123456789012345678', \r
+    'timestamp': '1762845599887', \r
+    'nonce': 'ogHL53iSPoaU', \r
+    'signature': '1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987', \r
+    'deviceModel': 'AB101', \r
+    'deviceVersion': '1.0.0', \r
+    'Content-Type': 'application/json'\r
+}\r
+```\r
+\r
+## 详细对比\r
+\r
+| 字段 | 请求1 | 请求2 | 状态 | 说明 |\r
+|------|-------|-------|------|------|\r
+| **URL** | `https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/auth/getDeviceToken` | `https://ambybox-api.test.leiniao.com/ambybox-device-api/v1/auth/getDeviceToken` | ✅ 相同 | 两个请求使用相同的URL |\r
+| **deviceId** | `DEVICE_PC_TEST_001` | `DEVICE_PC_TEST_001` | ✅ 相同 | 设备ID一致 |\r
+| **deviceIccid** | `86123456789012345678` | `86123456789012345678` | ✅ 相同 | 设备ICCID一致 |\r
+| **deviceModel** | `AB101` | `AB101` | ✅ 相同 | 设备型号一致 |\r
+| **deviceVersion** | `1.0.0` | `1.0.0` | ✅ 相同 | 设备版本一致 |\r
+| **Content-Type** | `application/json` | `application/json` | ✅ 相同 | 内容类型一致 |\r
+| **timestamp** | `1762845627028` | `1762845599887` | ⚠️ 不同 | **正常差异**:时间戳不同(相差约27秒) |\r
+| **nonce** | `DSAgsyFYxaOR` | `ogHL53iSPoaU` | ⚠️ 不同 | **正常差异**:随机数不同(每次请求都会生成新的) |\r
+| **signature** | `0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977` | `1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987` | ⚠️ 不同 | **正常差异**:签名不同(因为基于timestamp和nonce计算) |\r
+\r
+## 时间戳分析\r
+\r
+- **请求1时间戳**: `1762845627028` (毫秒)\r
+- **请求2时间戳**: `1762845599887` (毫秒)\r
+- **时间差**: `27141` 毫秒 ≈ **27.14秒**\r
+\r
+转换为可读时间:\r
+- 请求1: 约 2025-11-11 的某个时间点\r
+- 请求2: 约 2025-11-11 的某个时间点(比请求1早约27秒)\r
+\r
+## 签名验证\r
+\r
+签名计算公式:`HMAC-SHA256(signSecret, "deviceId:deviceIccid:timestamp:nonce")`\r
+\r
+### 请求1的签名数据\r
+```\r
+signData = "DEVICE_PC_TEST_001:86123456789012345678:1762845627028:DSAgsyFYxaOR"\r
+signature = "0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977"\r
+```\r
+\r
+### 请求2的签名数据\r
+```\r
+signData = "DEVICE_PC_TEST_001:86123456789012345678:1762845599887:ogHL53iSPoaU"\r
+signature = "1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987"\r
+```\r
+\r
+## 结论\r
+\r
+### ✅ 正常情况\r
+两个请求头信息**符合预期**,差异是正常的:\r
+\r
+1. **静态字段一致**:deviceId, deviceIccid, deviceModel, deviceVersion, Content-Type 都相同\r
+2. **动态字段不同**:timestamp, nonce, signature 不同是**正常的**,因为:\r
+   - `timestamp` 是当前时间戳,每次请求都会变化\r
+   - `nonce` 是随机生成的,每次请求都不同(用于防重放攻击)\r
+   - `signature` 基于 timestamp 和 nonce 计算,因此也会不同\r
+\r
+### 📊 请求头结构\r
+两个请求的请求头结构完全一致,都包含:\r
+- 设备标识信息(deviceId, deviceIccid)\r
+- 设备信息(deviceModel, deviceVersion)\r
+- 认证信息(timestamp, nonce, signature)\r
+- 内容类型(Content-Type)\r
+\r
+### 🔍 验证建议\r
+如果需要验证签名是否正确,可以使用以下Python代码:\r
+\r
+```python\r
+import hmac\r
+import hashlib\r
+\r
+SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus="\r
+\r
+# 验证请求1\r
+sign_data_1 = "DEVICE_PC_TEST_001:86123456789012345678:1762845627028:DSAgsyFYxaOR"\r
+hmac_obj_1 = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data_1.encode("utf-8"), hashlib.sha256)\r
+signature_1 = hmac_obj_1.hexdigest().lower()\r
+print(f"请求1签名: {signature_1}")\r
+print(f"期望签名: 0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977")\r
+print(f"匹配: {signature_1 == '0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977'}")\r
+\r
+# 验证请求2\r
+sign_data_2 = "DEVICE_PC_TEST_001:86123456789012345678:1762845599887:ogHL53iSPoaU"\r
+hmac_obj_2 = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data_2.encode("utf-8"), hashlib.sha256)\r
+signature_2 = hmac_obj_2.hexdigest().lower()\r
+print(f"请求2签名: {signature_2}")\r
+print(f"期望签名: 1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987")\r
+print(f"匹配: {signature_2 == '1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987'}")\r
+```\r
+\r
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/single_auth_usage.md b/code/sdk/zhihui/leiniao/leiniao_python_test/single_auth_usage.md
new file mode 100644 (file)
index 0000000..5522537
--- /dev/null
@@ -0,0 +1,145 @@
+# 单向认证使用说明\r
+\r
+## 概述\r
+\r
+单向认证(服务器认证)是HTTPS最常用的认证方式,客户端会验证服务器的身份。根据SSL/TLS应用指南,需要以下步骤:\r
+\r
+1. 配置认证模式为单向认证(auth_mode=1)\r
+2. 添加CA证书到模块\r
+3. 确认证书\r
+4. 配置CA证书到HTTPS\r
+\r
+## 代码修改步骤\r
+\r
+### 1. 准备CA证书文件\r
+\r
+将CA证书文件放在可访问的路径,例如:\r
+```\r
+certs/\r
+  └── ca.crt\r
+```\r
+\r
+### 2. 修改主程序配置\r
+\r
+在 `get_device_token_at.py` 文件末尾,修改以下配置:\r
+\r
+```python\r
+# 认证模式配置\r
+AUTH_MODE = 1  # 改为1(单向认证)\r
+\r
+# 证书文件路径\r
+CA_CERT_PATH = "certs/ca.crt"  # 设置CA证书路径\r
+```\r
+\r
+### 3. 运行程序\r
+\r
+```bash\r
+python3 get_device_token_at.py\r
+```\r
+\r
+## 完整流程说明\r
+\r
+### 步骤1: 模块初始化\r
+- AT+GMR 查询版本号\r
+- 检查PPP拨号状态\r
+\r
+### 步骤2: 配置HTTPS参数(单向认证)\r
+1. **加载CA证书文件**\r
+   ```python\r
+   ca_cert_data = load_certificate_file("certs/ca.crt")\r
+   ```\r
+\r
+2. **添加CA证书到模块**\r
+   ```\r
+   AT+CERTADD=ca.crt,<length>\r
+   CONNECT\r
+   <证书内容>\r
+   +CERTADD: <length>,OK\r
+   ```\r
+\r
+3. **确认证书**\r
+   ```\r
+   AT+CERTCHECK=ca.crt\r
+   +CERTCHECK: ca.crt,OK\r
+   ```\r
+\r
+4. **配置SSL/TLS参数**\r
+   ```\r
+   AT+HTTPSCFG="sslversion",3\r
+   AT+HTTPSCFG="authmode",1\r
+   AT+HTTPSCFG="cacert",ca.crt\r
+   ```\r
+\r
+5. **确认配置**\r
+   ```\r
+   AT+HTTPSCFG?\r
+   +HTTPSCFG: 3,1,ca.crt,,\r
+   ```\r
+\r
+### 步骤3: 设置HTTPS连接参数\r
+- 设置URL\r
+- 设置端口(默认443)\r
+- 可选:设置keepalive\r
+\r
+### 步骤4: 建立HTTPS连接\r
+```\r
+AT+HTTPSSETUP\r
+```\r
+\r
+### 步骤5: 发送HTTPS请求\r
+```\r
+AT+HTTPSACTION=99,<length>\r
+> <HTTP请求>\r
+```\r
+\r
+### 步骤6: 关闭连接\r
+```\r
+AT+HTTPSCLOSE\r
+```\r
+\r
+## 代码示例\r
+\r
+### 使用单向认证的完整示例\r
+\r
+```python\r
+# 在文件末尾修改\r
+if __name__ == "__main__":\r
+    # 单向认证配置\r
+    AUTH_MODE = 1\r
+    CA_CERT_PATH = "certs/ca.crt"  # 根据实际路径修改\r
+    \r
+    send_get_device_token_request(\r
+        auth_mode=AUTH_MODE,\r
+        ca_cert_path=CA_CERT_PATH\r
+    )\r
+```\r
+\r
+## 注意事项\r
+\r
+1. **证书文件格式**:支持PEM格式的证书文件\r
+2. **证书长度**:确保 `AT+CERTADD` 中的长度参数与实际证书文件大小(字节数)一致\r
+3. **证书路径**:使用相对路径或绝对路径都可以\r
+4. **证书文件名**:模块中存储的证书名称就是文件名(如 `ca.crt`)\r
+\r
+## 与无需认证的对比\r
+\r
+| 项目 | 无需认证 | 单向认证 |\r
+|------|---------|---------|\r
+| auth_mode | 0 | 1 |\r
+| 需要证书 | ❌ 否 | ✅ CA证书 |\r
+| 安全性 | ⚠️ 较低 | ✅ 较高 |\r
+| 配置步骤 | 简单 | 需要添加证书 |\r
+\r
+## 错误处理\r
+\r
+如果证书添加失败,程序会:\r
+1. 打印错误信息\r
+2. 终止流程\r
+3. 不会继续执行HTTPS请求\r
+\r
+常见错误:\r
+- 证书文件不存在\r
+- 证书长度不匹配\r
+- 证书格式错误\r
+- 模块存储空间不足\r
+\r
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/tcp_test_server.py b/code/sdk/zhihui/leiniao/leiniao_python_test/tcp_test_server.py
new file mode 100644 (file)
index 0000000..c4c30ca
--- /dev/null
@@ -0,0 +1,491 @@
+#!/usr/bin/env python3\r
+# -*- coding: utf-8 -*-\r
+"""\r
+TCP测试服务器\r
+监听端口8910,支持多客户端连接\r
+可以查看连接状态、接收设备数据、发送数据给设备\r
+"""\r
+\r
+import socket\r
+import threading\r
+import time\r
+import sys\r
+from datetime import datetime\r
+from collections import defaultdict\r
+\r
+class TCPTestServer:\r
+    def __init__(self, host='0.0.0.0', port=8910):\r
+        """\r
+        初始化TCP测试服务器\r
+        \r
+        Args:\r
+            host: 监听地址,默认0.0.0.0(所有网络接口)\r
+            port: 监听端口,默认8910\r
+        """\r
+        self.host = host\r
+        self.port = port\r
+        self.server_socket = None\r
+        self.clients = {}  # {client_id: {'socket': socket, 'address': (ip, port), 'connect_time': datetime}}\r
+        self.client_counter = 0\r
+        self.running = False\r
+        self.lock = threading.Lock()\r
+        \r
+    def start(self):\r
+        """启动服务器"""\r
+        try:\r
+            self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\r
+            self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\r
+            self.server_socket.bind((self.host, self.port))\r
+            self.server_socket.listen(10)  # 最多10个待连接\r
+            self.server_socket.settimeout(1.0)  # 设置超时以便定期检查running状态\r
+            \r
+            self.running = True\r
+            print("=" * 60)\r
+            print(f"TCP测试服务器已启动")\r
+            print(f"监听地址: {self.host}:{self.port}")\r
+            print(f"服务器地址: snicker.com.cn:{self.port}")\r
+            print("=" * 60)\r
+            print("\n命令说明:")\r
+            print("  list          - 显示所有连接的客户端")\r
+            print("  send <id>     - 发送数据给指定客户端(会提示输入数据)")\r
+            print("  sendall       - 发送数据给所有客户端(会提示输入数据)")\r
+            print("  close <id>    - 关闭指定客户端连接")\r
+            print("  closeall      - 关闭所有客户端连接")\r
+            print("  help          - 显示帮助信息")\r
+            print("  quit/exit     - 退出服务器")\r
+            print("=" * 60)\r
+            print()\r
+            \r
+            # 启动接受连接的线程\r
+            accept_thread = threading.Thread(target=self._accept_connections, daemon=True)\r
+            accept_thread.start()\r
+            \r
+            # 启动命令行输入处理线程\r
+            input_thread = threading.Thread(target=self._handle_commands, daemon=True)\r
+            input_thread.start()\r
+            \r
+            # 主线程等待\r
+            accept_thread.join()\r
+            \r
+        except Exception as e:\r
+            print(f"❌ 服务器启动失败: {e}")\r
+            self.running = False\r
+    \r
+    def _accept_connections(self):\r
+        """接受客户端连接"""\r
+        while self.running:\r
+            try:\r
+                client_socket, address = self.server_socket.accept()\r
+                client_socket.settimeout(1.0)\r
+                \r
+                with self.lock:\r
+                    self.client_counter += 1\r
+                    client_id = self.client_counter\r
+                    self.clients[client_id] = {\r
+                        'socket': client_socket,\r
+                        'address': address,\r
+                        'connect_time': datetime.now()\r
+                    }\r
+                \r
+                # 显示连接信息\r
+                print(f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] ✅ 客户端连接")\r
+                print(f"  客户端ID: {client_id}")\r
+                print(f"  地址: {address[0]}:{address[1]}")\r
+                print(f"  当前连接数: {len(self.clients)}")\r
+                print()\r
+                \r
+                # 启动接收数据的线程\r
+                recv_thread = threading.Thread(\r
+                    target=self._handle_client,\r
+                    args=(client_id, client_socket, address),\r
+                    daemon=True\r
+                )\r
+                recv_thread.start()\r
+                \r
+            except socket.timeout:\r
+                continue\r
+            except Exception as e:\r
+                if self.running:\r
+                    print(f"❌ 接受连接时出错: {e}")\r
+    \r
+    def _handle_client(self, client_id, client_socket, address):\r
+        """处理客户端数据接收"""\r
+        buffer = b""\r
+        try:\r
+            while self.running:\r
+                try:\r
+                    data = client_socket.recv(4096)\r
+                    if not data:\r
+                        # 客户端关闭连接\r
+                        break\r
+                    \r
+                    buffer += data\r
+                    \r
+                    # 显示接收到的数据\r
+                    timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')\r
+                    print(f"\n[{timestamp}] 📥 收到数据 [客户端ID: {client_id}, {address[0]}:{address[1]}]")\r
+                    print(f"  数据长度: {len(data)} 字节")\r
+                    \r
+                    # 尝试以文本和十六进制显示\r
+                    try:\r
+                        text = data.decode('utf-8', errors='ignore')\r
+                        if text.isprintable() or '\n' in text or '\r' in text:\r
+                            print(f"  文本内容:")\r
+                            for line in text.splitlines():\r
+                                print(f"    {line}")\r
+                        else:\r
+                            print(f"  十六进制: {data.hex()}")\r
+                    except:\r
+                        print(f"  十六进制: {data.hex()}")\r
+                    \r
+                    # 显示前64字节的ASCII表示\r
+                    if len(data) <= 64:\r
+                        ascii_repr = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data)\r
+                        print(f"  ASCII: {ascii_repr}")\r
+                    else:\r
+                        ascii_repr = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data[:64])\r
+                        print(f"  ASCII (前64字节): {ascii_repr}...")\r
+                    \r
+                    print()\r
+                    \r
+                except socket.timeout:\r
+                    continue\r
+                except Exception as e:\r
+                    if self.running:\r
+                        print(f"❌ 接收数据时出错 [客户端ID: {client_id}]: {e}")\r
+                    break\r
+                    \r
+        except Exception as e:\r
+            print(f"❌ 处理客户端时出错 [客户端ID: {client_id}]: {e}")\r
+        finally:\r
+            # 客户端断开连接\r
+            self._disconnect_client(client_id, address)\r
+    \r
+    def _disconnect_client(self, client_id, address):\r
+        """断开客户端连接"""\r
+        with self.lock:\r
+            if client_id in self.clients:\r
+                try:\r
+                    self.clients[client_id]['socket'].close()\r
+                except:\r
+                    pass\r
+                del self.clients[client_id]\r
+        \r
+        timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')\r
+        print(f"\n[{timestamp}] ❌ 客户端断开连接")\r
+        print(f"  客户端ID: {client_id}")\r
+        print(f"  地址: {address[0]}:{address[1]}")\r
+        print(f"  当前连接数: {len(self.clients)}")\r
+        print()\r
+    \r
+    def _handle_commands(self):\r
+        """处理命令行输入"""\r
+        while self.running:\r
+            try:\r
+                command = input().strip()\r
+                if not command:\r
+                    continue\r
+                \r
+                parts = command.split(None, 1)\r
+                cmd = parts[0].lower()\r
+                args = parts[1] if len(parts) > 1 else ""\r
+                \r
+                if cmd in ['quit', 'exit', 'q']:\r
+                    print("\n正在关闭服务器...")\r
+                    self.stop()\r
+                    break\r
+                elif cmd == 'list':\r
+                    self._list_clients()\r
+                elif cmd == 'send':\r
+                    if args:\r
+                        try:\r
+                            client_id = int(args)\r
+                            self._send_to_client(client_id)\r
+                        except ValueError:\r
+                            print("❌ 无效的客户端ID,请输入数字")\r
+                    else:\r
+                        print("❌ 用法: send <客户端ID>")\r
+                elif cmd == 'sendall':\r
+                    self._send_to_all_clients()\r
+                elif cmd == 'close':\r
+                    if args:\r
+                        try:\r
+                            client_id = int(args)\r
+                            self._close_client(client_id)\r
+                        except ValueError:\r
+                            print("❌ 无效的客户端ID,请输入数字")\r
+                    else:\r
+                        print("❌ 用法: close <客户端ID>")\r
+                elif cmd == 'closeall':\r
+                    self._close_all_clients()\r
+                elif cmd in ['help', 'h']:\r
+                    self._show_help()\r
+                else:\r
+                    print(f"❌ 未知命令: {cmd},输入 help 查看帮助")\r
+                    \r
+            except EOFError:\r
+                # 输入流关闭(Ctrl+D)\r
+                break\r
+            except KeyboardInterrupt:\r
+                print("\n正在关闭服务器...")\r
+                self.stop()\r
+                break\r
+            except Exception as e:\r
+                print(f"❌ 处理命令时出错: {e}")\r
+    \r
+    def _list_clients(self):\r
+        """列出所有连接的客户端"""\r
+        with self.lock:\r
+            if not self.clients:\r
+                print("\n当前没有连接的客户端")\r
+                return\r
+            \r
+            print("\n" + "=" * 60)\r
+            print("连接的客户端列表:")\r
+            print("=" * 60)\r
+            print(f"{'ID':<6} {'地址':<25} {'连接时间':<20}")\r
+            print("-" * 60)\r
+            \r
+            for client_id, client_info in self.clients.items():\r
+                address = f"{client_info['address'][0]}:{client_info['address'][1]}"\r
+                connect_time = client_info['connect_time'].strftime('%Y-%m-%d %H:%M:%S')\r
+                print(f"{client_id:<6} {address:<25} {connect_time:<20}")\r
+            \r
+            print("=" * 60)\r
+            print(f"总计: {len(self.clients)} 个客户端")\r
+            print()\r
+    \r
+    def _send_to_client(self, client_id):\r
+        """发送数据给指定客户端"""\r
+        with self.lock:\r
+            if client_id not in self.clients:\r
+                print(f"❌ 客户端ID {client_id} 不存在")\r
+                return\r
+            \r
+            client_info = self.clients[client_id]\r
+            client_socket = client_info['socket']\r
+            address = client_info['address']\r
+        \r
+        try:\r
+            print(f"\n发送数据给客户端 {client_id} ({address[0]}:{address[1]})")\r
+            print("输入数据(支持文本或十六进制,十六进制以0x或hex:开头):")\r
+            data_input = input("> ").strip()\r
+            \r
+            if not data_input:\r
+                print("❌ 数据为空,取消发送")\r
+                return\r
+            \r
+            # 解析输入数据\r
+            data = self._parse_input_data(data_input)\r
+            if data is None:\r
+                print("❌ 数据解析失败")\r
+                return\r
+            \r
+            # 发送数据\r
+            client_socket.sendall(data)\r
+            \r
+            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')\r
+            print(f"\n[{timestamp}] 📤 已发送数据 [客户端ID: {client_id}]")\r
+            print(f"  数据长度: {len(data)} 字节")\r
+            print(f"  十六进制: {data.hex()}")\r
+            if len(data) <= 64:\r
+                ascii_repr = ''.join(chr(b) if 32 <= b < 127 else '.' for b in data)\r
+                print(f"  ASCII: {ascii_repr}")\r
+            print()\r
+            \r
+        except Exception as e:\r
+            print(f"❌ 发送数据失败: {e}")\r
+    \r
+    def _send_to_all_clients(self):\r
+        """发送数据给所有客户端"""\r
+        with self.lock:\r
+            if not self.clients:\r
+                print("❌ 当前没有连接的客户端")\r
+                return\r
+            \r
+            client_ids = list(self.clients.keys())\r
+        \r
+        try:\r
+            print(f"\n发送数据给所有客户端(共 {len(client_ids)} 个)")\r
+            print("输入数据(支持文本或十六进制,十六进制以0x或hex:开头):")\r
+            data_input = input("> ").strip()\r
+            \r
+            if not data_input:\r
+                print("❌ 数据为空,取消发送")\r
+                return\r
+            \r
+            # 解析输入数据\r
+            data = self._parse_input_data(data_input)\r
+            if data is None:\r
+                print("❌ 数据解析失败")\r
+                return\r
+            \r
+            # 发送给所有客户端\r
+            success_count = 0\r
+            failed_clients = []\r
+            \r
+            with self.lock:\r
+                for client_id in client_ids:\r
+                    if client_id in self.clients:\r
+                        try:\r
+                            self.clients[client_id]['socket'].sendall(data)\r
+                            success_count += 1\r
+                        except Exception as e:\r
+                            failed_clients.append((client_id, str(e)))\r
+            \r
+            timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')\r
+            print(f"\n[{timestamp}] 📤 已发送数据给 {success_count} 个客户端")\r
+            print(f"  数据长度: {len(data)} 字节")\r
+            if failed_clients:\r
+                print(f"  失败: {len(failed_clients)} 个客户端")\r
+                for client_id, error in failed_clients:\r
+                    print(f"    客户端 {client_id}: {error}")\r
+            print()\r
+            \r
+        except Exception as e:\r
+            print(f"❌ 发送数据失败: {e}")\r
+    \r
+    def _parse_input_data(self, data_input):\r
+        """\r
+        解析输入数据\r
+        支持文本、十六进制(0x开头或hex:开头)\r
+        \r
+        Args:\r
+            data_input: 输入字符串\r
+            \r
+        Returns:\r
+            bytes: 解析后的数据,失败返回None\r
+        """\r
+        data_input = data_input.strip()\r
+        \r
+        # 十六进制格式: 0x... 或 hex:...\r
+        if data_input.lower().startswith('0x'):\r
+            hex_str = data_input[2:].strip()\r
+            # 移除空格\r
+            hex_str = hex_str.replace(' ', '').replace('\n', '').replace('\r', '')\r
+            try:\r
+                return bytes.fromhex(hex_str)\r
+            except ValueError:\r
+                print(f"❌ 无效的十六进制数据: {hex_str}")\r
+                return None\r
+        elif data_input.lower().startswith('hex:'):\r
+            hex_str = data_input[4:].strip()\r
+            # 移除空格\r
+            hex_str = hex_str.replace(' ', '').replace('\n', '').replace('\r', '')\r
+            try:\r
+                return bytes.fromhex(hex_str)\r
+            except ValueError:\r
+                print(f"❌ 无效的十六进制数据: {hex_str}")\r
+                return None\r
+        else:\r
+            # 文本格式\r
+            return data_input.encode('utf-8')\r
+    \r
+    def _close_client(self, client_id):\r
+        """关闭指定客户端连接"""\r
+        with self.lock:\r
+            if client_id not in self.clients:\r
+                print(f"❌ 客户端ID {client_id} 不存在")\r
+                return\r
+            \r
+            client_info = self.clients[client_id]\r
+            address = client_info['address']\r
+        \r
+        try:\r
+            client_info['socket'].close()\r
+            print(f"✅ 已关闭客户端 {client_id} 的连接")\r
+        except Exception as e:\r
+            print(f"❌ 关闭客户端连接失败: {e}")\r
+    \r
+    def _close_all_clients(self):\r
+        """关闭所有客户端连接"""\r
+        with self.lock:\r
+            if not self.clients:\r
+                print("当前没有连接的客户端")\r
+                return\r
+            \r
+            client_ids = list(self.clients.keys())\r
+            count = len(client_ids)\r
+        \r
+        closed_count = 0\r
+        for client_id in client_ids:\r
+            try:\r
+                with self.lock:\r
+                    if client_id in self.clients:\r
+                        self.clients[client_id]['socket'].close()\r
+                        closed_count += 1\r
+            except:\r
+                pass\r
+        \r
+        print(f"✅ 已关闭 {closed_count}/{count} 个客户端连接")\r
+    \r
+    def _show_help(self):\r
+        """显示帮助信息"""\r
+        print("\n" + "=" * 60)\r
+        print("命令帮助:")\r
+        print("=" * 60)\r
+        print("  list                    - 显示所有连接的客户端")\r
+        print("  send <id>               - 发送数据给指定客户端")\r
+        print("                          例如: send 1")\r
+        print("  sendall                 - 发送数据给所有客户端")\r
+        print("  close <id>              - 关闭指定客户端连接")\r
+        print("                          例如: close 1")\r
+        print("  closeall                - 关闭所有客户端连接")\r
+        print("  help                    - 显示此帮助信息")\r
+        print("  quit/exit/q             - 退出服务器")\r
+        print()\r
+        print("数据输入格式:")\r
+        print("  - 文本: 直接输入文本,例如: Hello World")\r
+        print("  - 十六进制: 以0x开头,例如: 0x48656c6c6f")\r
+        print("  - 十六进制: 以hex:开头,例如: hex:48656c6c6f")\r
+        print("=" * 60)\r
+        print()\r
+    \r
+    def stop(self):\r
+        """停止服务器"""\r
+        self.running = False\r
+        \r
+        # 关闭所有客户端连接\r
+        with self.lock:\r
+            for client_id, client_info in list(self.clients.items()):\r
+                try:\r
+                    client_info['socket'].close()\r
+                except:\r
+                    pass\r
+            self.clients.clear()\r
+        \r
+        # 关闭服务器socket\r
+        if self.server_socket:\r
+            try:\r
+                self.server_socket.close()\r
+            except:\r
+                pass\r
+        \r
+        print("✅ 服务器已关闭")\r
+\r
+\r
+def main():\r
+    """主函数"""\r
+    import argparse\r
+    \r
+    parser = argparse.ArgumentParser(description='TCP测试服务器')\r
+    parser.add_argument('--host', default='0.0.0.0', help='监听地址(默认: 0.0.0.0)')\r
+    parser.add_argument('--port', type=int, default=8910, help='监听端口(默认: 8910)')\r
+    \r
+    args = parser.parse_args()\r
+    \r
+    server = TCPTestServer(host=args.host, port=args.port)\r
+    \r
+    try:\r
+        server.start()\r
+    except KeyboardInterrupt:\r
+        print("\n正在关闭服务器...")\r
+        server.stop()\r
+    except Exception as e:\r
+        print(f"❌ 服务器运行出错: {e}")\r
+        server.stop()\r
+\r
+\r
+if __name__ == "__main__":\r
+    main()\r
+\r
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/verify_signatures.py b/code/sdk/zhihui/leiniao/leiniao_python_test/verify_signatures.py
new file mode 100644 (file)
index 0000000..4dd8841
--- /dev/null
@@ -0,0 +1,46 @@
+#!/usr/bin/env python3\r
+# -*- coding: utf-8 -*-\r
+"""验证两个请求的签名是否正确"""\r
+\r
+import hmac\r
+import hashlib\r
+\r
+SIGN_SECRET = "vDdXVXgPUEUM6_piUjz5AEhMvYTSFzQdPaN-ogKnSus="\r
+\r
+# 请求1的签名验证\r
+sign_data_1 = "DEVICE_PC_TEST_001:86123456789012345678:1762845627028:DSAgsyFYxaOR"\r
+hmac_obj_1 = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data_1.encode("utf-8"), hashlib.sha256)\r
+signature_1 = hmac_obj_1.hexdigest().lower()\r
+expected_signature_1 = "0652010b2d574b034086a0efd707c94fef8250eac3fb02948c049b43cfb1f977"\r
+\r
+print("=" * 60)\r
+print("请求1签名验证:")\r
+print(f"  签名数据: {sign_data_1}")\r
+print(f"  计算签名: {signature_1}")\r
+print(f"  期望签名: {expected_signature_1}")\r
+print(f"  匹配结果: {'✅ 正确' if signature_1 == expected_signature_1 else '❌ 错误'}")\r
+print("=" * 60)\r
+\r
+# 请求2的签名验证\r
+sign_data_2 = "DEVICE_PC_TEST_001:86123456789012345678:1762845599887:ogHL53iSPoaU"\r
+hmac_obj_2 = hmac.new(SIGN_SECRET.encode("utf-8"), sign_data_2.encode("utf-8"), hashlib.sha256)\r
+signature_2 = hmac_obj_2.hexdigest().lower()\r
+expected_signature_2 = "1457b817ddeffa108341b750fa32063febc9dee9a24eaca1918fb1748b5e7987"\r
+\r
+print("请求2签名验证:")\r
+print(f"  签名数据: {sign_data_2}")\r
+print(f"  计算签名: {signature_2}")\r
+print(f"  期望签名: {expected_signature_2}")\r
+print(f"  匹配结果: {'✅ 正确' if signature_2 == expected_signature_2 else '❌ 错误'}")\r
+print("=" * 60)\r
+\r
+# 时间戳差异分析\r
+timestamp_1 = 1762845627028\r
+timestamp_2 = 1762845599887\r
+time_diff = timestamp_1 - timestamp_2\r
+print(f"\n时间戳差异分析:")\r
+print(f"  请求1时间戳: {timestamp_1}")\r
+print(f"  请求2时间戳: {timestamp_2}")\r
+print(f"  时间差: {time_diff} 毫秒 ({time_diff/1000:.2f} 秒)")\r
+print("=" * 60)\r
+\r
diff --git a/code/sdk/zhihui/leiniao/leiniao_python_test/websocket_feasibility_analysis.md b/code/sdk/zhihui/leiniao/leiniao_python_test/websocket_feasibility_analysis.md
new file mode 100644 (file)
index 0000000..980173a
--- /dev/null
@@ -0,0 +1,394 @@
+# WebSocket实现可行性分析\r
+\r
+## 问题\r
+AT命令手册中的TCP指令和HTTP指令是否足够实现WebSocket(ws和wss)接口,要求支持二进制数据和文本数据传输?\r
+\r
+## 简短回答\r
+\r
+**✅ 是的,AT指令集中的TCP和SSL TCP指令足够实现WebSocket接口。**\r
+\r
+- **ws(非加密)**:使用 `AT+TCPSETUP` 建立TCP连接\r
+- **wss(加密)**:使用 `AT+SSLTCPSETUP` 建立SSL TCP连接\r
+- **二进制和文本数据**:都支持,通过HEX模式发送二进制,ASCII模式发送文本\r
+\r
+**⚠️ 但需要手动实现WebSocket协议层**(帧封装、握手处理等)\r
+\r
+## WebSocket协议要求\r
+\r
+### 1. WebSocket连接建立流程\r
+```\r
+1. 建立TCP/SSL TCP连接\r
+2. 发送HTTP/HTTPS握手请求(包含Upgrade: websocket头)\r
+3. 接收服务器握手响应(HTTP 101 Switching Protocols)\r
+4. 连接升级为WebSocket协议\r
+5. 在TCP连接上传输WebSocket帧(文本/二进制)\r
+```\r
+\r
+### 2. WebSocket帧格式\r
+```\r
+ 0                   1                   2                   3\r
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1\r
++-+-+-+-+-------+-+-------------+-------------------------------+\r
+|F|R|R|R| opcode|M| Payload len |    Extended payload length    |\r
+|I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |\r
+|N|V|V|V|       |S|             |   (if payload len==126/127)   |\r
+| |1|2|3|       |K|             |                               |\r
++-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +\r
+|     Extended payload length continued, if payload len == 127  |\r
++ - - - - - - - - - - - - - - - +-------------------------------+\r
+|                               |Masking-key, if MASK set to 1  |\r
++-------------------------------+-------------------------------+\r
+| Masking-key (continued)       |          Payload Data         |\r
++-------------------------------- - - - - - - - - - - - - - - - +\r
+:                     Payload Data continued ...                :\r
++ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +\r
+|                     Payload Data continued ...                |\r
++---------------------------------------------------------------+\r
+```\r
+\r
+关键点:\r
+- 需要按照WebSocket帧格式封装数据\r
+- 支持文本帧(opcode=0x1)和二进制帧(opcode=0x2)\r
+- 客户端发送的数据需要mask(MASK=1)\r
+- 服务器发送的数据不需要mask(MASK=0)\r
+\r
+## AT命令手册能力分析\r
+\r
+### ✅ 可用的指令\r
+\r
+#### 1. TCP连接管理\r
+- **AT+TCPSETUP** - 建立TCP连接(支持ws)\r
+  - 支持域名解析\r
+  - 支持0-5路连接\r
+  - 连接建立后返回OK\r
+\r
+- **AT+SSLTCPSETUP** - 建立SSL TCP连接(支持wss)\r
+  - 支持TLS/SSL加密\r
+  - 支持证书配置\r
+  - 支持0-5路连接\r
+\r
+#### 2. 数据发送能力\r
+- **AT+TCPSEND** - 发送TCP数据\r
+  - **Buffer模式**:\r
+    - ASCII模式:最大4096字节\r
+    - HEX模式:最大2048字节(二进制数据)\r
+  - **命令模式**:\r
+    - ASCII/HEX模式:最大512字节\r
+  - 支持二进制数据传输(HEX模式)\r
+\r
+- **AT+SSLTCPSEND** - 发送SSL TCP数据\r
+  - 最大4096字节\r
+  - 支持二进制数据传输\r
+\r
+#### 3. 数据接收能力\r
+- **+TCPRECV** - 接收TCP数据主动上报\r
+- **AT+TCPREAD** - 读取TCP数据\r
+- **+SSLTCPRECV** - 接收SSL TCP数据主动上报\r
+- **AT+SSLTCPREAD** - 读取SSL TCP数据\r
+- **AT+RECVMODE** - 设置接收模式(ASCII/HEX)\r
+\r
+#### 4. HTTP指令(用于握手)\r
+- **AT+HTTPSETUP/AT+HTTPSSETUP** - 建立HTTP/HTTPS连接\r
+- **AT+HTTPACTION/AT+HTTPSACTION** - 发送HTTP请求\r
+- 或者使用TCP指令手动发送HTTP握手请求\r
+\r
+### ⚠️ 限制和挑战\r
+\r
+#### 1. 数据长度限制\r
+- TCP buffer模式HEX:最大2048字节\r
+- SSL TCP:最大4096字节\r
+- WebSocket帧可能超过这些限制(需要分片)\r
+\r
+#### 2. WebSocket帧封装\r
+- **需要手动实现**WebSocket帧的编码/解码\r
+- 需要处理:\r
+  - 帧头构造(FIN, opcode, MASK, payload length)\r
+  - Masking-key生成和应用\r
+  - 扩展payload length(126/127字节)\r
+  - 分片处理(FIN=0的连续帧)\r
+\r
+#### 3. 握手处理\r
+- 需要生成WebSocket握手请求\r
+- 需要解析服务器握手响应\r
+- 需要验证Sec-WebSocket-Accept\r
+\r
+#### 4. 实时性要求\r
+- WebSocket需要及时处理接收到的数据\r
+- 需要轮询或使用主动上报机制\r
+\r
+## 实现方案\r
+\r
+### 方案1:使用TCP/SSL TCP指令(推荐)\r
+\r
+#### 优点\r
+- ✅ 完全控制数据传输\r
+- ✅ 支持二进制数据(HEX模式)\r
+- ✅ 支持长连接\r
+- ✅ 可以处理任意大小的数据(通过分片)\r
+\r
+#### 实现步骤\r
+\r
+**1. 建立连接(ws)**\r
+```\r
+AT+TCPSETUP=0,websocket.example.com,80\r
+```\r
+\r
+**2. 发送WebSocket握手请求**\r
+```\r
+AT+TCPSEND=0,<handshake_length>\r
+> GET /path HTTP/1.1\r\n\r
+  Host: websocket.example.com\r\n\r
+  Upgrade: websocket\r\n\r
+  Connection: Upgrade\r\n\r
+  Sec-WebSocket-Key: <base64_key>\r\n\r
+  Sec-WebSocket-Version: 13\r\n\r
+  \r\n\r
+```\r
+\r
+**3. 接收握手响应**\r
+```\r
++TCPRECV: 0,<length>\r
+HTTP/1.1 101 Switching Protocols\r
+Upgrade: websocket\r
+Connection: Upgrade\r
+Sec-WebSocket-Accept: <accept_key>\r
+```\r
+\r
+**4. 发送WebSocket帧(文本)**\r
+```\r
+// 构造WebSocket帧\r
+// FIN=1, opcode=0x1(文本), MASK=1, payload_len, masking_key, masked_payload\r
+AT+TCPSEND=0,<frame_length>,1  // HEX模式\r
+> <hex_encoded_frame>\r
+```\r
+\r
+**5. 发送WebSocket帧(二进制)**\r
+```\r
+// 构造WebSocket帧\r
+// FIN=1, opcode=0x2(二进制), MASK=1, payload_len, masking_key, masked_payload\r
+AT+TCPSEND=0,<frame_length>,1  // HEX模式\r
+> <hex_encoded_frame>\r
+```\r
+\r
+**6. 接收WebSocket帧**\r
+```\r
++TCPRECV: 0,<length>\r
+// 解析帧头,提取payload\r
+```\r
+\r
+### 方案2:使用HTTP指令(不推荐)\r
+\r
+#### 缺点\r
+- ❌ HTTP指令主要用于请求-响应模式\r
+- ❌ 不支持长连接的双向通信\r
+- ❌ 难以处理WebSocket帧\r
+\r
+## 关键实现点\r
+\r
+### 1. WebSocket帧编码(发送)\r
+\r
+```python\r
+def encode_websocket_frame(payload, opcode=0x1, fin=1):\r
+    """\r
+    编码WebSocket帧\r
+    opcode: 0x1=文本, 0x2=二进制\r
+    """\r
+    # 1. 构造帧头\r
+    frame = bytearray()\r
+    \r
+    # 第一个字节:FIN(1) + RSV(000) + opcode(4)\r
+    first_byte = (fin << 7) | opcode\r
+    frame.append(first_byte)\r
+    \r
+    # 2. 生成masking key(客户端必须mask)\r
+    masking_key = os.urandom(4)\r
+    \r
+    # 3. 第二个字节:MASK(1) + payload length\r
+    payload_len = len(payload)\r
+    if payload_len < 126:\r
+        second_byte = 0x80 | payload_len  # MASK=1\r
+        frame.append(second_byte)\r
+    elif payload_len < 65536:\r
+        frame.append(0x80 | 126)  # MASK=1, len=126\r
+        frame.extend(payload_len.to_bytes(2, 'big'))\r
+    else:\r
+        frame.append(0x80 | 127)  # MASK=1, len=127\r
+        frame.extend(payload_len.to_bytes(8, 'big'))\r
+    \r
+    # 4. 添加masking key\r
+    frame.extend(masking_key)\r
+    \r
+    # 5. 对payload进行mask\r
+    masked_payload = bytearray()\r
+    for i, byte in enumerate(payload):\r
+        masked_payload.append(byte ^ masking_key[i % 4])\r
+    \r
+    frame.extend(masked_payload)\r
+    return bytes(frame)\r
+```\r
+\r
+### 2. WebSocket帧解码(接收)\r
+\r
+```python\r
+def decode_websocket_frame(data):\r
+    """\r
+    解码WebSocket帧\r
+    """\r
+    if len(data) < 2:\r
+        return None, data\r
+    \r
+    # 解析第一个字节\r
+    first_byte = data[0]\r
+    fin = (first_byte >> 7) & 0x1\r
+    opcode = first_byte & 0x0F\r
+    \r
+    # 解析第二个字节\r
+    second_byte = data[1]\r
+    mask = (second_byte >> 7) & 0x1\r
+    payload_len = second_byte & 0x7F\r
+    \r
+    # 解析扩展payload length\r
+    offset = 2\r
+    if payload_len == 126:\r
+        if len(data) < 4:\r
+            return None, data\r
+        payload_len = int.from_bytes(data[2:4], 'big')\r
+        offset = 4\r
+    elif payload_len == 127:\r
+        if len(data) < 10:\r
+            return None, data\r
+        payload_len = int.from_bytes(data[2:10], 'big')\r
+        offset = 10\r
+    \r
+    # 解析masking key(服务器发送的不mask)\r
+    masking_key = None\r
+    if mask:\r
+        if len(data) < offset + 4:\r
+            return None, data\r
+        masking_key = data[offset:offset+4]\r
+        offset += 4\r
+    \r
+    # 读取payload\r
+    if len(data) < offset + payload_len:\r
+        return None, data\r
+    \r
+    payload = data[offset:offset+payload_len]\r
+    \r
+    # 如果masked,进行unmask\r
+    if mask and masking_key:\r
+        payload = bytearray(payload)\r
+        for i in range(len(payload)):\r
+            payload[i] ^= masking_key[i % 4]\r
+        payload = bytes(payload)\r
+    \r
+    # 返回帧信息和剩余数据\r
+    frame_info = {\r
+        'fin': fin,\r
+        'opcode': opcode,\r
+        'payload': payload,\r
+        'is_text': opcode == 0x1,\r
+        'is_binary': opcode == 0x2\r
+    }\r
+    \r
+    remaining = data[offset+payload_len:]\r
+    return frame_info, remaining\r
+```\r
+\r
+### 3. 握手请求生成\r
+\r
+```python\r
+import base64\r
+import hashlib\r
+import secrets\r
+\r
+def generate_websocket_handshake(host, path, port=80):\r
+    """\r
+    生成WebSocket握手请求\r
+    """\r
+    # 生成Sec-WebSocket-Key\r
+    key = base64.b64encode(secrets.token_bytes(16)).decode('utf-8')\r
+    \r
+    handshake = (\r
+        f"GET {path} HTTP/1.1\r\n"\r
+        f"Host: {host}:{port}\r\n"\r
+        f"Upgrade: websocket\r\n"\r
+        f"Connection: Upgrade\r\n"\r
+        f"Sec-WebSocket-Key: {key}\r\n"\r
+        f"Sec-WebSocket-Version: 13\r\n"\r
+        f"\r\n"\r
+    )\r
+    \r
+    return handshake, key\r
+```\r
+\r
+## 结论\r
+\r
+### ✅ **可以实现WebSocket(ws和wss)**\r
+\r
+**理由:**\r
+1. ✅ TCP/SSL TCP指令支持建立连接\r
+2. ✅ 支持二进制数据传输(HEX模式)\r
+3. ✅ 支持文本数据传输(ASCII模式)\r
+4. ✅ 支持长连接\r
+5. ✅ 可以手动发送HTTP握手请求\r
+\r
+### ⚠️ **需要额外实现**\r
+\r
+1. **WebSocket帧编码/解码** - 需要手动实现\r
+2. **握手处理** - 需要生成和解析HTTP握手\r
+3. **分片处理** - 处理大于2048/4096字节的数据\r
+4. **心跳/Ping-Pong** - 保持连接活跃\r
+5. **错误处理** - 处理连接断开、超时等\r
+\r
+### 📊 **数据长度限制**\r
+\r
+| 传输类型 | 最大长度 | 解决方案 |\r
+|---------|---------|---------|\r
+| TCP HEX模式 | 2048字节 | 分片发送(FIN=0) |\r
+| SSL TCP | 4096字节 | 分片发送(FIN=0) |\r
+| 文本数据 | 无限制 | 分片处理 |\r
+\r
+### 🎯 **推荐实现方式**\r
+\r
+**使用TCP/SSL TCP指令 + 手动实现WebSocket协议层**\r
+\r
+1. 使用 `AT+TCPSETUP` / `AT+SSLTCPSETUP` 建立连接\r
+2. 使用 `AT+TCPSEND` / `AT+SSLTCPSEND` 发送握手和帧数据(HEX模式)\r
+3. 使用 `+TCPRECV` / `+SSLTCPRECV` 接收数据\r
+4. 在应用层实现WebSocket帧的编码/解码\r
+\r
+### 📝 **注意事项**\r
+\r
+1. **数据分片**:超过2048/4096字节的数据需要分片发送\r
+2. **实时性**:需要及时处理接收到的数据,避免buffer溢出\r
+3. **错误恢复**:实现重连机制\r
+4. **性能优化**:合理使用buffer模式和命令模式\r
+\r
+## 总结\r
+\r
+### ✅ **结论:AT指令集中的TCP和SSL TCP指令足够实现WebSocket接口**\r
+\r
+**核心能力:**\r
+1. ✅ **连接建立**:`AT+TCPSETUP`(ws)和 `AT+SSLTCPSETUP`(wss)\r
+2. ✅ **数据发送**:`AT+TCPSEND` / `AT+SSLTCPSEND` 支持二进制(HEX模式)和文本(ASCII模式)\r
+3. ✅ **数据接收**:`+TCPRECV` / `+SSLTCPRECV` 主动上报,支持二进制和文本\r
+4. ✅ **长连接**:TCP/SSL TCP连接支持保持连接,满足WebSocket要求\r
+\r
+**需要手动实现的部分:**\r
+1. ⚠️ **WebSocket握手**:生成HTTP握手请求,解析服务器响应\r
+2. ⚠️ **帧封装/解析**:按照RFC 6455标准实现WebSocket帧的编码和解码\r
+3. ⚠️ **数据分片**:处理超过2048/4096字节的数据(使用FIN=0分片)\r
+4. ⚠️ **心跳机制**:实现Ping/Pong帧保持连接活跃\r
+\r
+**推荐实现方式:**\r
+- 使用 `AT+TCPSETUP` / `AT+SSLTCPSETUP` 建立连接\r
+- 使用 `AT+TCPSEND` / `AT+SSLTCPSEND` 发送握手和WebSocket帧(HEX模式)\r
+- 使用 `+TCPRECV` / `+SSLTCPRECV` 接收数据\r
+- 在应用层实现WebSocket协议栈(帧封装、握手、心跳等)\r
+\r
+**数据长度限制:**\r
+- TCP HEX模式:最大2048字节/帧(可通过分片解决)\r
+- SSL TCP:最大4096字节/帧(可通过分片解决)\r
+\r
+**总结:AT指令集提供了足够的底层传输能力,但需要在应用层实现WebSocket协议层。**\r
+\r
diff --git a/code/sdk/zhihui/ppp/doc/Neoway_功能模块_SSL_TLS_应用指南_V1.0.md b/code/sdk/zhihui/ppp/doc/Neoway_功能模块_SSL_TLS_应用指南_V1.0.md
new file mode 100644 (file)
index 0000000..f671036
--- /dev/null
@@ -0,0 +1,3378 @@
+# SSL/TLS应用指南
+
+### SSL/TLS 应用指南
+
+###### 版本 1.0 日期 2021-08-30
+
+Neoway SSL/TLS 应用指南
+
+版权声明
+
+深圳市有方科技股份有限公司版权所有 
+ii
+
+###### 版权声明
+
+版权所有 © 深圳市有方科技股份有限公司 2021。深圳市有方科技股份有限公司保留所有权利。
+
+未经深圳市有方科技股份有限公司书面同意,任何单位和个人不得擅自摘抄、复制本文档内容的部分或全部,
+并不得以任何形式传播。
+
+是深圳市有方科技股份有限公司所有商标。
+
+本文档中出现的其他商标,由商标所有者所有。
+
+###### 说明
+
+本文档对应产品为Neoway 模块。
+
+本文档的使用对象为系统工程师,开发工程师及测试工程师。
+
+本设计指南为客户产品设计提供支持,客户须按照本文中的规范和参数进行产品设计和调试。如因客户操作
+不当造成的人身伤害和财产损失,有方概不承担责任。
+
+由于产品版本升级或其它原因,本文档内容会在不预先通知的情况下进行必要的更新。
+
+除非另有约定,本文档中的所有陈述、信息和建议不构成任何明示或暗示的担保。
+
+深圳市有方科技股份有限公司为客户提供全方位的技术支持,任何垂询请直接联系您的客户经理或发送邮件
+至以下邮箱:
+
+Sales@neoway.com
+
+Support@neoway.com
+
+公司网址:http://www.neoway.com
+
+Neoway SSL/TLS 应用指南
+
+目录
+
+深圳市有方科技股份有限公司版权所有 
+iii
+
+#### 目 录
+
+###### 关于本文档 .................................................................................................... v
+
+范围 ..................................................................................................................................................... v
+
+读者对象 .............................................................................................................................................. v
+
+修订记录 .............................................................................................................................................. v
+
+符号约定 .............................................................................................................................................. v
+
+相关文档 .............................................................................................................................................. v
+
+###### 1 概述 ........................................................................................................... 6
+
+1.1 SSL/TLS 简介 ............................................................................................................................... 6
+
+1.2 SSL/TLS 使用流程 ........................................................................................................................ 7
+
+1.3 注意事项 ...................................................................................................................................... 8
+
+###### 2 HTTPS 应用流程........................................................................................ 9
+
+2.1 无需认证 ...................................................................................................................................... 9
+
+2.2 单向认证 ..................................................................................................................................... 11
+
+2.3 双向认证 .................................................................................................................................... 12
+
+2.4 示例 ........................................................................................................................................... 14
+
+2.4.1 无需认证 ........................................................................................................................... 14
+
+2.4.2 单向认证 ........................................................................................................................... 15
+
+2.4.3 双向认证 ........................................................................................................................... 15
+
+###### 3 MQTTS 应用流程 ..................................................................................... 17
+
+3.1 不加密 ........................................................................................................................................ 17
+
+3.2 加密 ........................................................................................................................................... 18
+
+3.2.1 单向认证 ........................................................................................................................... 18
+
+3.2.2 双向认证 ........................................................................................................................... 19
+
+3.3 示例 ........................................................................................................................................... 20
+
+3.3.1 单向认证 ........................................................................................................................... 20
+
+3.3.2 双向认证 ........................................................................................................................... 21
+
+###### 4 TCPS 应用流程 ........................................................................................ 23
+
+4.1 无需认证 .................................................................................................................................... 23
+
+4.2 单向认证 .................................................................................................................................... 23
+
+4.3 双向认证 .................................................................................................................................... 24
+
+4.4 示例 ........................................................................................................................................... 25
+
+4.4.1 无需认证 ........................................................................................................................... 25
+
+4.4.2 单向认证 ........................................................................................................................... 26
+
+4.4.3 双向认证 ........................................................................................................................... 26
+
+Neoway SSL/TLS 应用指南
+
+目录
+
+深圳市有方科技股份有限公司版权所有 
+iv
+
+###### A 相关AT 指令 ........................................................................................... 28
+
+A.1 HTTPS 相关指令 ........................................................................................................................ 28
+
+A.1.1 AT+CERTADD–添加SSL 证书 ......................................................................................... 28
+
+A.1.2 AT+CERTCHECK–SSL 证书确认 ................................................................................... 28
+
+A.1.3 AT+HTTPSCFG–配置HTTPS 参数 ................................................................................. 29
+
+A.1.4 AT+HTTPSPARA–设置HTTPS 参数 ............................................................................... 30
+
+A.1.5 AT+HTTPSSETUP–建立HTTPS 链路 ............................................................................ 31
+
+A.1.6 AT+HTTPSACTION–发送HTTPS 请求 ........................................................................... 32
+
+A.1.7 AT+HTTPSCLOSE–主动关闭HTTPS 链路 ..................................................................... 32
+
+A.2 MQTTS 相关指令 ....................................................................................................................... 33
+
+A.2.1 AT+CERTADD–添加SSL 证书 ......................................................................................... 33
+
+A.2.2 AT+CERTCHECK–SSL 证书确认 ................................................................................... 34
+
+A.2.3 AT+MQTTTLS - TLS 参数配置 .......................................................................................... 34
+
+A.2.4 AT+MQTTCONNPARAM–用户参数设置 ......................................................................... 35
+
+A.2.5 AT+MQTTWILLPARAM–遗嘱设置 ................................................................................... 36
+
+A.2.6 AT+MQTTCONN–连接命令 ............................................................................................. 37
+
+A.2.7 AT+MQTTSUB–订阅命令 ................................................................................................ 37
+
+A.2.8 AT+MQTTPUB–发布主题 ................................................................................................ 38
+
+A.2.9 AT+MQTTDISCONN–关闭MQTT 连接 ........................................................................... 39
+
+A.3 TCPS 相关指令 .......................................................................................................................... 39
+
+A.3.1 AT+CERTADD–添加SSL 证书 ......................................................................................... 39
+
+A.3.2 AT+CERTCHECK–SSL 证书确认 ................................................................................... 40
+
+A.3.3 AT+SSLTCPCFG–SSL TCP 配置参数 ............................................................................. 41
+
+A.3.4 AT+SSLTCPSETUP–建立SSL TCP 连接 ....................................................................... 42
+
+A.3.5 AT+SSLTCPSEND–SSL TCP 数据发送 ............................................................................ 43
+
+A.3.6 AT+SSLTCPCLOSE–SSL TCP 关闭连接指令 ................................................................. 44
+
+###### B 初始化流程 ............................................................................................. 45
+
+Neoway SSL/TLS 应用指南
+
+关于本文档
+
+深圳市有方科技股份有限公司版权所有 
+v
+
+#### 关于本文档
+
+##### 范围
+
+本文档对应产品为Neoway 模块,主要介绍了有方模块是如何使用AT 指令实现SSL/TLS 加密传
+
+输功能的。适用的模块有:N58、N77、N716、N720、N720V5 等,其他模块是否适用请参考具体AT
+
+指令集或者寻问对应的技术支持。
+
+##### 读者对象
+
+本文档的使用对象为系统工程师,开发工程师及测试工程师。
+
+##### 修订记录
+
+版本 
+日期 
+变更 
+作者
+
+1.0 
+2021-05 
+初始版本 
+Thea Chen
+
+##### 符号约定
+
+符号 
+含义
+
+危险或警告,用户必须遵从的规则,否则会造成模块或客户设备不可逆的故障损坏,
+
+甚至可能造成人员身体伤害。
+
+注意,警示用户使用模块时应该特别注意的地方,如不遵从,模块或客户设备可能
+
+出现故障。
+
+说明或提示,提供模块使用的意见或建议。
+
+##### 相关文档
+
+《Neoway_功能模块_HTTP 应用指南》
+
+《Neoway_功能模块_MQTT 应用指南》
+
+Neoway SSL/TLS 应用指南
+
+第1 章概述
+
+深圳市有方科技股份有限公司版权所有 
+6
+
+#### 1 概述
+
+本应用文档主要描述有方功能模块使用AT 指令实现SSL/TLS 加密传输功能的应用流程。在进行服
+
+务器-客户端之间的数据交互时,经常会有确保数据安全传输的需要,用户只需要使用有方AT 指令集提
+
+供的相应指令即可实现安全的数据交互,如:HTTPS、MQTTS、TCPS。
+
+##### 1.1 SSL/TLS 简介
+
+SSL(Secure Sockets Layer)安全套接字层及其继任者TLS(Transport Layer Security)传输层
+
+安全协议是为网络通信提供安全及数据完整性的一种安全协议。SSL 作用在端系统应用层和运输层之
+
+间,在TCP 之上建立起一个安全通道,为通过TCP 传输的应用层数据提供安全保障,如下图。
+
+图 1-1 SSL/TLS 所处TCP/IP 层次
+
+Application layer protocol
+
+TCP
+
+IP
+
+SSL/TLS
+
+SSL 协议是Netscape 公司在1994 年开发的安全协议,广泛应用于基于万维网的各种网络应用(但
+
+不限于万维网应用)。1995 年 Netscape 公司把SSL 转交给IETF,希望能够把SSL 进行标准化。于是
+
+IETF 在 SSL3.0 的基础上设计了TLS 协议,目前为了应对网络安全的变化,IETF 及时地对TLS 的版
+
+本进行升级,如2008 年8 月的互联网建议标准TLS1.2。
+
+目前有方模块支持的SSL/TLS 协议版本如下表所示:
+
+表 1-1 SSL/TLS 版本
+
+参数值 
+SSL/TLS 版本
+
+0 
+SSL3.0
+
+1 
+TLS1.0
+
+2 
+TLS1.1
+
+3 
+TLS1.2
+
+Neoway SSL/TLS 应用指南
+
+第1 章概述
+
+深圳市有方科技股份有限公司版权所有 
+7
+
+SSL/TLS 协议分两层,第一层是记录协议,主要用于传输数据的加密压缩;第二层是握手协议,它
+
+建立在第一层协议之上,主要用于数据传输前的双方身份认证、协商加密算法、交换密钥,以及警告协
+
+议,用于在协商出错时发送警告消息。SSL 验证过程具体分为单向认证和双向认证。
+
+图 1-2 SSL/TLS 协议
+
+SSL handshake protocol
+
+SSL alert protocol
+
+SSL change cipher spec
+
+protocol
+
+Application layer protocol
+
+SSL record protocol
+
+IP
+
+TCP
+
+单向认证和双向认证对客户端和服务器端都有不同的要求,如下表:
+
+表 1-2 认证模式
+
+参数值 
+认证模式 
+客户端要求 
+服务器端要求
+
+0 
+无需认证 
+无 
+无
+
+1 
+单向认证 
+ca.crt 
+server.crt、server.key
+
+2 
+双向认证 
+ca.crt、client.crt、client.key 
+ca.crt、server.crt、server.key
+
+##### 1.2 SSL/TLS 使用流程
+
+SSL/TLS 功能在Neoway 模块上的具体应用有HTTPS、MQTTS 和TCPS,以下是如何使用
+
+SSL/TLS 功能的具体步骤:
+
+步骤 1:模块进行初始化操作,具体流程请参考附录B 。
+
+步骤 2:模块进行拨号上网。
+
+步骤 3:配置加密参数。
+
+1. 
+选择SSL/TLS 协议版本。
+
+2. 
+选择安全认证模式。
+
+3. 
+配置相应证书文件,以便在握手过程中使用。
+
+Neoway SSL/TLS 应用指南
+
+第1 章概述
+
+深圳市有方科技股份有限公司版权所有 
+8
+
+根据所选安全认证模式不同添加证书内容也不同。
+
+当安全认证模式设置为无需认证时不需要此步骤。
+
+4. 
+确认各加密参数成功设置。
+
+步骤 4:设置服务器连接参数,进行相应的HTTPS 连接、MQTTS 连接或者TCPS 连接。
+
+##### 1.3 注意事项
+
+建立连接过程可能会失败,以下几点需要注意:
+
+⚫ 
+确保模块已经成功激活PDP,获取到IP 并且可以成功上网,一般是使用指令
+
+“AT+PING=<ip/url>”进行测试。
+
+⚫ 
+确保证书等文件已成功导入:
+
+指令“AT+CERTADD=<file_name>,<length>”中,“length”字段一定要和导入的证书长度一
+
+致,单位为字节(Byte)。可以右击证书文件,在“属性”查看证书文件的大小是多少字节。
+
+⚫ 
+确保证书文件已正确配置:
+
+无需认证不需要配置证书,单向认证需要配置CA 证书,双向认证需要配置CA 证书、客户端证
+
+书、客户端密钥。
+
+⚫ 
+确保加密参数是否正确配置:
+
+− 
+所配置的sslversion 服务器是否支持;
+
+− 
+所配置的authmode 是否和配置的证书文件匹配。
+
+⚫ 
+确保连接参数已正确配置:
+
+− 
+确认目标路径是否正确无误,目标路径可以是IP 地址或者域名。
+
+− 
+确认目标端口号是否与服务器所用端口号一致。
+
+⚫ 
+模块使用POST 方式进行HTTPS 请求时:
+
+− 
+确保POST 请求方式中所配置的请求数据类型与实际请求数据一致。例如,若请求为xml 格
+
+式,请求参数配置时就需要配置为3,若请求为html 格式则需要将请求参数配置为4。
+
+− 
+此外,还需要确认所配置的请求长度与实际请求长度一致。
+
+⚫ 
+模块进行MQTTS 请求时:
+
+需要确认连接参数clientdID、username、password 是否正确。
+
+Neoway SSL/TLS 应用指南
+
+HTTPS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+9
+
+#### 2 HTTPS 应用流程
+
+HTTPS (Secure Hypertext Transfer Protocol)安全超文本传输协议,HTTPS 是在HTTP 上建立
+
+SSL 加密层,并对传输数据进行加密,是HTTP 协议的安全版,简而言之就是:
+
+HTTPS = HTTP + 加密 + 身份认证 + 完整性保护
+
+图 2-1 HTTPS 协议
+
+###### HTTP
+
+###### TCP
+
+###### IP
+
+###### SSL/TLS
+
+###### HTTP
+
+###### TCP
+
+###### IP
+
+通常,HTTP 直接和TCP 通信,当使用SSL 时,则先和SSL 通信,再由SSL 和TCP 通信。在采
+
+用SSL 后,HTTP 就拥有了HTTPS 的加密、认证和完整性保护这些功能。
+
+在实际应用中,按照身份认证的不同需求,又可以分为无需认证,单向认证和双向认证。
+
+##### 2.1 无需认证
+
+配置无需认证时,HTTPS 的客户端或者服务器端就会接受不受信的证书,相当于不认证传输数据
+
+前通信双方的身份,而是直接进行之后的协商加密算法、交换密钥过程。虽然数据是经过加密进行传输,
+
+但是由于未进行身份认证,仍有风险,需谨慎使用。
+
+步骤 1:模块进行初始化操作,具体流程请参考附录B 。
+
+步骤 2:模块进行拨号上网。
+
+步骤 3:配置HTTPS 加密参数。
+
+1. 
+配置SSL/TLS 协议版本。
+
+例如,指令“AT+HTTPSCFG=sslversion,3”,表示配置SSL/TLS 协议版本为TLS1.2。
+
+2. 
+执行“AT+HTTPSCFG=authmode,0”指令,将SSL/TLS 认证方式配置为无需认证。
+
+Neoway SSL/TLS 应用指南
+
+HTTPS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+10
+
+当SSL/TLS 认证方式配置为无需认证时,不需要配置证书。
+
+步骤 4:配置HTTPS 连接参数。
+
+1. 
+配置目标路径。
+
+例如,“AT+HTTPSPARA=url, www.ip138.com”,表示目标路径为IP 地址查询网。
+
+2. 
+配置目标端口。
+
+例如,“AT+HTTPSPARA=port,443”,表示目标端口号为443(默认端口),若是自己搭建的
+
+HTTPS 服务器则需根据所设端口进行更改。
+
+3. 
+执行“AT+HTTPSPARA=keepalive,1”指令配置长连接(可选)。
+
+HTTPS 中长连接是默认关闭的,即每次发送完一次请求之后HTTPS 连接就会关闭,再次请求的
+
+话就需要重新建立连接。如果希望建立一次连接后可以多次请求,则可执行该操作。
+
+步骤 5:执行“AT+HTTPSSETUP”指令建立HTTPS 连接。
+
+步骤 6:发送HTTPS 请求。
+
+用户根据自身需求选择不同请求方式,当服务器收到请求并进行处理后,会返回响应的数据,客
+
+户端收到后会主动上报收到的内容。
+
+1. 
+使用指令“AT+HTTPSACTION=0”,表示使用GET 请求方式,读取步骤4 中所设URL 中的资源
+
+内容。
+
+2. 
+使用指令“AT+HTTPSACTION=2,446,3”,表示使用POST 请求方式,用于向指定URL 资源提交
+
+一大小为446 字节(需与之后POST 内容大小一致)的xml 格式数据进行处理请求。使用此指令
+
+后会出现一个尖括号“>”,这时上传POST 内容,上传完毕后服务器即会返回处理后的内容。
+
+Neoway 模块支持的HTTPS 请求方式有4 种,用户可根据自身需要进行选择:
+
+表 2-1 HTTPS 请求方式
+
+参数值 
+SSL/TLS 请求方式 
+意义
+
+0 
+GET 
+请求读取由URL 所标志的信息
+
+1 
+HEAD 
+请求读取由URL 所标志的信息首部
+
+2 
+POST 
+向指定资源提交数据进行处理请求
+
+99 
+OPEN_MODE 
+用户自己定义报文模式
+
+POST 请求的数据类型有5 种,用户根据自身请求内容类型进行选择:
+
+Neoway SSL/TLS 应用指南
+
+HTTPS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+11
+
+表 2-2 POST 请求的数据类型
+
+参数值 
+POST 请求的数据类型
+
+0 
+x-www-form-urlencoded
+
+1 
+text
+
+2 
+json
+
+3 
+xml
+
+4 
+html
+
+步骤 7:使用“AT+HTTPSCLOSE”指令关闭HTTPS 连接。
+
+##### 2.2 单向认证
+
+配置单向认证时,HTTPS 的客户端将会验证服务器的身份,验证通过后才会进行之后的协商加密
+
+算法以及交换密钥过程。此认证方式下,客户端需要CA 证书,服务器端需要服务器证书和服务器密钥。
+
+目前这一认证方式是HTTPS 使用最广泛的一种。
+
+步骤 1:模块进行初始化操作,具体流程请参考附录B 。
+
+步骤 2:模块进行拨号上网。
+
+步骤 3:配置HTTPS 加密参数。
+
+1. 
+配置SSL/TLS 协议版本。
+
+例如,指令“AT+HTTPSCFG=sslversion,3”,表示配置SSL/TLS 协议版本为TLS1.2。
+
+2. 
+执行“AT+HTTPSCFG=authmode,1”指令将SSL/TLS 认证方式配置为单向认证。
+
+3. 
+向模块写入CA 证书。
+
+例如,使用指令“AT+CERTADD=ca.crt,1196”,表示向模块写入一份名为ca.crt、长度为1196
+
+字节的CA 证书文件。
+
+当SSL/TLS 认证方式配置为单向认证时,需要向客户端(模块)配置CA 证书。
+
+4. 
+执行“AT+CERTCHECK=ca.crt”指令,确认证书是否成功写入。
+
+5. 
+执行“AT+HTTPSCFG=cacert,ca.crt”指令,将写入的CA 证书配置到客户端(模块端)。
+
+6. 
+执行“AT+HTTPSCFG?”指令,确认当前SSL/TLS 加密参数配置内容是否正确。
+
+步骤 4:配置HTTPS 连接参数。
+
+Neoway SSL/TLS 应用指南
+
+HTTPS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+12
+
+1. 
+配置目标路径。
+
+例如,“AT+HTTPSPARA=url, www.ip138.com”,表示目标路径为IP 地址查询网。
+
+2. 
+配置目标端口。
+
+例如,“AT+HTTPSPARA=port,443”,表示将目标端口号设置为443(默认端口),若是自己搭建
+
+的HTTPS 服务器则需根据所设端口进行更改。
+
+3. 
+执行“AT+HTTPSPARA=keepalive,1”指令配置长连接(可选)。
+
+HTTPS 中长连接是默认关闭的,即每次发送完一次请求之后HTTPS 连接就会关闭,再次请求的
+
+话就需要重新建立连接。如果希望建立一次连接后可以多次请求,则可执行该操作。
+
+步骤 5:执行“AT+HTTPSSETUP”指令建立HTTPS 连接。
+
+步骤 6:发送HTTPS 请求。
+
+用户根据自身需求选择不同请求方式,当服务器收到请求并进行处理后,会返回响应的数据,客
+
+户端收到后会主动上报收到的内容。
+
+1. 
+使用指令“AT+HTTPSACTION=0”,表示使用GET 请求方式,读取步骤4 中所设URL 中的资源
+
+内容。
+
+2. 
+使用指令“AT+HTTPSACTION=2,446,3”,表示使用POST 请求方式,用于向指定URL 资源提交
+
+一大小为446 字节(需与之后POST 内容大小一致)的xml 格式数据进行处理请求。使用此指令
+
+后会出现一个尖括号“>”,这时上传POST 内容,上传完毕后服务器即会返回处理后的内容。
+
+步骤 7:使用“AT+HTTPSCLOSE” 指令关闭HTTPS 连接。
+
+##### 2.3 双向认证
+
+双向认证和单向认证原理基本差不多,只是除了客户端需要认证服务端以外,增加了服务端对客户
+
+端的认证。在此认证模式下,客户端需要CA 证书、客户端证书与客户端密钥,服务器端需要CA 证书、
+
+服务器证书与服务器密钥。
+
+步骤 1:模块进行初始化操作,具体流程请参考附录B 。
+
+步骤 2:模块进行拨号上网。
+
+步骤 3:配置HTTPS 加密参数。
+
+1. 
+配置SSL/TLS 协议版本。
+
+例如,指令“AT+HTTPSCFG=sslversion,3”,表示配置SSL/TLS 协议版本为TLS1.2。
+
+2. 
+执行“AT+HTTPSCFG=authmode,2”指令将SSL/TLS 认证方式配置为双向认证。
+
+3. 
+向模块写入CA 证书。
+
+Neoway SSL/TLS 应用指南
+
+HTTPS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+13
+
+使用指令“AT+CERTADD=ca.crt,1196”,表示向模块写入一份名为ca.crt、长度为1196 字节的
+
+CA 证书文件。
+
+当认证方式配置为双向认证时,需要向客户端(模块)写入CA 证书、客户端证书与客户端密钥。
+
+4. 
+使用指令“AT+CERTCHECK=ca.crt”确认证书是否成功写入。
+
+5. 
+使用指令“AT+HTTPSCFG=cacert,ca.crt”,表示将写入的CA 证书配置到客户端(模块端)。
+
+6. 
+同理向客户端证书和客户端密钥,并进行确认。
+
+使用指令“AT+CERTADD=client.crt,1204”、“AT+CERTADD=client.key,1743”,表示向模块写入
+
+一份名为client.crt、长度为1204 字节的客户端证书文件和一份名为client.key、长度为1743 字
+
+节的客户端密钥文件。
+
+7. 
+使用指令“AT+CERTCHECK=client.crt”、“AT+CERTCHECK=client.key”确认证书和密钥是否成
+
+功写入。
+
+8. 
+使用指令“AT+HTTPSCFG=clientcacert,client.crt”、“AT+HTTPSCFG=clientkey,client.key”,表示
+
+将写入的客户端证书和客户端密钥配置到客户端(模块端)。
+
+9. 
+执行“AT+HTTPSCFG?”指令,确认当前SSL/TLS 加密参数配置内容是否正确。
+
+步骤 4:配置HTTPS 连接参数。
+
+1. 
+配置目标路径。
+
+例如,"AT+HTTPSPARA=url, www.ip138.com”,表示目标路径为IP 地址查询网。
+
+2. 
+配置端口。
+
+例如,“AT+HTTPSPARA=port,443”,表示目标端口号为443(默认端口),若是自己搭建的
+
+HTTPS 服务器则需根据所设端口进行更改。
+
+3. 
+执行“AT+HTTPSPARA=keepalive,1”指令配置长连接(可选)。
+
+HTTPS 中长连接是默认关闭的,即每次发送完一次请求之后HTTPS 连接就会关闭,再次请求的
+
+话就需要重新建立连接。如果希望建立一次连接后可以多次请求,则可执行该操作。
+
+步骤 5:执行“AT+HTTPSSETUP”指令建立HTTPS 连接。
+
+步骤 6:发送HTTPS 请求。
+
+用户根据自身需求选择不同请求方式,当服务器收到请求并进行处理后,会返回响应的数据,客
+
+户端收到后会主动上报收到的内容。
+
+⚫ 
+使用指令“AT+HTTPSACTION=0”,表示使用GET 请求方式,读取步骤4 中所设URL 中的资源
+
+内容。
+
+Neoway SSL/TLS 应用指南
+
+HTTPS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+14
+
+⚫ 
+使用指令“AT+HTTPSACTION=2,446,3”,表示使用POST 请求方式,用于向指定URL 资源提
+
+交一大小为446 字节(需与之后POST 内容大小一致)的xml 格式数据进行处理请求。使用此指
+
+令后会出现一个尖括号“>”,这时上传POST 内容,上传完毕后服务器即会返回处理后的内容。
+
+步骤 7:使用“AT+HTTPSCLOSE” 指令关闭HTTPS 连接。
+
+##### 2.4 示例
+
+以下流程示例在执行的过程中需首先对模块进行初始化后,才能进行。初始化流程,详见附录B 。
+
+###### 2.4.1 无需认证
+
+```
+AT+XIIC=1
+```
+
+OK
+
+```
+AT+XIIC?
+```
+
++XIIC: 1,100.77.55.140
+
+OK
+
+```
+AT+HTTPSCFG=sslversion,3
+```
+
+OK
+
+```
+AT+HTTPSCFG=authmode,0
+```
+
+OK
+
+```
+AT+HTTPSCFG?
+```
+
++HTTPSCFG: 3,0,,,
+
+OK
+
+```
+AT+HTTPSPARA=url,www.ip138.com
+```
+
+OK
+
+```
+AT+HTTPSPARA=port,443
+```
+
+OK
+
+```
+AT+HTTPSPARA=keepalive,1
+```
+
+OK
+
+```
+AT+HTTPSSETUP
+```
+
+OK
+
+```
+AT+HTTPSACTION=1
+```
+
+OK
+
++HTTPSRECV:
+
+HTTP/1.1 200 OK
+
+Date: Thu, 18 Mar 2021 03:48:38 GMT
+
+Content-Type: text/html
+
+Content-Length: 18544
+
+Connection: close
+
+Content-Location: http://www.ip138.com/index.htm
+
+Last-Modified: Thu, 11 Mar 2021 09:04:14 GMT
+
+Accept-Ranges: bytes
+
+ETag: "2ab018825516d71:218a"
+
+Server: Microsoft-IIS/6.0
+
+X-Powered-By: ASP.NET
+
+Age: 54728
+
+X-Via: 1.1 PSfjfzsx3ky59:9 (Cdn Cache Server V2.0), 1.1 PSjsczsxlm170:5 (Cdn Cache Server
+
+V2.0), 1.1 PS-TYN-011yu167:20 (Cdn Cache Server V2.0)
+
+X-Ws-Request-Id: 6052cd96_PS-TYN-011yu167_48200-28314
+
+```
+AT+HTTPSCLOSE
+```
+
+Neoway SSL/TLS 应用指南
+
+HTTPS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+15
+
+OK
+
++HTTPSCLOSE: HTTPS Link Closed
+
+###### 2.4.2 单向认证
+
+```
+AT+XIIC=1
+```
+
+OK
+
+```
+AT+XIIC?
+```
+
++XIIC: 1,100.77.55.140
+
+OK
+
+```
+AT+HTTPSCFG=sslversion,3
+```
+
+OK
+
+```
+AT+HTTPSCFG=authmode,1
+```
+
+OK
+
+```
+AT+CERTADD=ca.crt,1196
+```
+
+CONNECT
+
++CERTADD: 1196,OK
+
+```
+AT+HTTPSCFG=cacert,ca.crt
+```
+
+OK
+
+```
+AT+HTTPSCFG?
+```
+
++HTTPSCFG: 3,1,ca.crt,,
+
+OK
+
+```
+AT+HTTPSPARA=url,183.239.240.40
+```
+
+OK
+
+```
+AT+HTTPSPARA=port,12011
+```
+
+OK
+
+```
+AT+HTTPSPARA=keepalive,1
+```
+
+OK
+
+```
+AT+HTTPSSETUP
+```
+
+OK
+
+```
+AT+HTTPSACTION=1
+```
+
+OK
+
++HTTPSRECV:
+
+HTTP/1.1 200 OK
+
+Server: Apache-Coyote/1.1
+
+Content-Type: text/html;charset=UTF-8
+
+Content-Length: 0
+
+Date: Thu, 18 Mar 2021 07:05:47 GMT
+
+```
+AT+HTTPSCLOSE
+```
+
+OK
+
++HTTPSCLOSE: HTTPS Link Closed
+
+###### 2.4.3 双向认证
+
+```
+AT+XIIC=1
+```
+
+OK
+
+```
+AT+XIIC?
+```
+
++XIIC: 1,100.77.55.140
+
+OK
+
+```
+AT+HTTPSCFG=sslversion,3
+```
+
+OK
+
+```
+AT+HTTPSCFG=authmode,2
+```
+
+Neoway SSL/TLS 应用指南
+
+HTTPS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+16
+
+OK
+
+```
+AT+CERTADD=ca.crt,1196
+```
+
+CONNECT
+
++CERTADD: 1196,OK
+
+```
+AT+CERTADD=client.crt,1204
+```
+
+CONNECT
+
++CERTADD: 1204,OK
+
+```
+AT+CERTADD=client.key,1743
+```
+
+CONNECT
+
++CERTADD: 1743,OK
+
+```
+AT+HTTPSCFG=cacert,ca.crt
+```
+
+OK
+
+```
+AT+HTTPSCFG=clientcert,client.crt
+```
+
+OK
+
+```
+AT+HTTPSCFG=clientkey,client.key
+```
+
+OK
+
+```
+AT+HTTPSCFG?
+```
+
++HTTPSCFG: 3,2,ca.crt,client.crt,client.key
+
+OK
+
+```
+AT+HTTPSPARA=url,183.239.240.40
+```
+
+OK
+
+```
+AT+HTTPSPARA=port,12012
+```
+
+OK
+
+```
+AT+HTTPSPARA=keepalive,1
+```
+
+OK
+
+```
+AT+HTTPSSETUP
+```
+
+OK
+
+```
+AT+HTTPSACTION=1
+```
+
+OK
+
++HTTPSRECV:
+
+HTTP/1.1 200 OK
+
+Server: Apache-Coyote/1.1
+
+Content-Type: text/html;charset=UTF-8
+
+Content-Length: 0
+
+Date: Thu, 18 Mar 2021 07:19:09 GMT
+
+```
+AT+HTTPSCLOSE
+```
+
+OK
+
++HTTPSCLOSE: HTTPS Link Closed
+
+Neoway SSL/TLS 应用指南
+
+MQTTS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+17
+
+#### 3 MQTTS 应用流程
+
+Neoway 模块MQTT 也开发了SSL/TLS 加密功能,称为MQTTS。与HTTPS 略有不同的是,
+
+MQTTS 参数配置有些差异:MQTT 底层SSL/TLS 协议默认支持TLS1.2,所以在进行安全加密参数设
+
+置时只需要设置为加密和不加密。
+
+表 3-1 MQTTS 安全加密模式
+
+sslmode 
+安全加密模式
+
+0 
+不加密
+
+1 
+加密
+
+安全认证模式则分为两类:单向认证和双向认证,但是参数设置略有不同。
+
+表 3-2 MQTTS 安全认证模式
+
+authmode 
+认证模式 
+客户端要求 
+服务器端要求
+
+0 
+单向认证 
+ca.crt 
+server.crt、server.key
+
+1 
+双向认证 
+ca.crt、client.crt、client.key 
+ca.crt、server.crt、server.key
+
+##### 3.1 不加密
+
+配置不加密时,MQTTS 流程与MQTT 一致,最终的MQTT 数据通信不会进行加密。
+
+步骤 1:模块进行初始化操作,具体流程请参考附录B 。
+
+步骤 2:模块进行拨号上网。
+
+步骤 3:执行“AT+MQTTTLS=sslmode,0” 指令,将MQTTS 连接配置为不加密。
+
+连接配置为不加密时不需配置认证证书。
+
+步骤 4:配置MQTTS 连接参数。
+
+1. 
+执行“AT+MQTTCONNPARAM=<clientdID>,<username>, <password>”指令对用户设备ID、用
+
+户名和密码进行设置。
+
+2. 
+根据需要使用指令“AT+MQTTWILLPARAM=<retained>,<qos>, <topicname>,<message>”设置
+
+遗嘱主题(最大长度128Bytes)与遗嘱消息(最大长度1024Bytes)。
+
+Neoway SSL/TLS 应用指南
+
+MQTTS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+18
+
+步骤 5:执行“AT+MQTTCONN=<"host">,<clean>,<keep_alive>”指令建立MQTTS 连接。
+
+“host”为服务器地址(url:port);“clean”为是否清除session,默认为0,表示不清除;
+
+keep_alive 时间取值范围为[20,180],单位为s,默认为60s。
+
+步骤 6:执行“AT+MQTTPUB=<retained>,<qos>,<"topicname">,<"message">”指令 发布主题。
+
+其中,“retained”为保留标志,“qos”为服务质量,“topicname”为发布主题(最大长度
+
+128Bytes),“message”为发布消息(最大长度为1024Bytes)。
+
+步骤 7:执行“AT+MQTTSUB=<"topicname">,<qos>”指令进行主题的订阅。
+
+步骤 8:执行“AT+MQTTDISCONN”指令,使模块主动断开和服务端的连接,然后释放 MQTT 资源,
+
+参数设置也会释放,如果需要PUB 消息,需要重新发起参数设置再连接设备。
+
+##### 3.2 加密
+
+###### 3.2.1 单向认证
+
+配置单向认证时,MQTTS 的客户端将会验证服务器的身份,验证身份后进行之后的协商加密算法
+
+以及交换密钥过程,协商出通信密钥后进行加密通信。
+
+步骤 1:模块进行初始化操作,具体流程请参考附录B 。
+
+步骤 2:模块进行拨号上网。
+
+步骤 3:配置MQTTS 加密参数。
+
+1. 
+执行“AT+MQTTTLS=sslmode,1”指令,将MQTT 连接配置为安全加密模式。
+
+2. 
+执行“AT+MQTTTLS=authmode,0”指令,将SSL/TLS 认证方式配置为单向认证。
+
+3. 
+向模块写入CA 证书文件:
+
+例如,使用指令“AT+CERTADD= MQTTS-certificate.pem,1180 ”,表示向模块写入一份名为
+
+MQTTS-certificate.pem、长度为1180 字节的CA 证书文件。
+
+4. 
+使用指令“AT+CERTCHECK= MQTTS-certificate.pem”确认证书是否成功写入。
+
+5. 
+使用指令“AT+MQTTTLS=rootca, MQTTS-certificate.pem”,将写入的CA 证书配置到客户端(模
+
+块端)。
+
+CA 证书的参数配置与HTTPS 有所不同:
+
+⚫ 
+MQTTS 中指令AT+MQTTTLS=<type>,<type_name>中,<type>字段为rootca;
+
+⚫ 
+而HTTPS 中指令AT+HTTPSCFG=<type>,<type_name> 中,<type>字段为cacert。
+
+Neoway SSL/TLS 应用指南
+
+MQTTS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+19
+
+6. 
+执行“AT+MQTTTLS?”指令,确认当前SSL/TLS 加密参数配置内容是否正确。
+
+步骤 4:配置MQTTS 连接参数。
+
+1. 
+使用指令“AT+MQTTCONNPARAM=<clientdID>,<username>, <password>”对用户设备ID、用
+
+户名和密码进行设置。
+
+2. 
+根据需要使用指令“AT+MQTTWILLPARAM=<retained>,<qos>, <topicname>,<message>”设置
+
+遗嘱主题(最大长度128Bytes)与遗嘱消息(最大长度1024Bytes)。
+
+步骤 5:使用指令“AT+MQTTCONN=<"host">,<clean>,<keep_alive>”建立MQTTS 连接。
+
+其中,“host”为服务器地址(url:port);“clean”为是否清除session,默认为0,表示不清除;
+
+keep_alive 时间取值范围为[20,180],单位为s,默认为60s。
+
+步骤 6:执行“AT+MQTTPUB=<retained>,<qos>,<"topicname">,<"message">”指令发布主题。其中,
+
+“retained”为保留标志,“qos”为服务质量,“topicname”为发布主题(最大长度
+
+128Bytes),“message”为发布消息(最大长度为1024 Bytes)。
+
+步骤 7:执行“AT+MQTTSUB=<"topicname">,<qos>”指令进行主题的订阅。
+
+步骤 8:执行“AT+MQTTDISCONN”指令,使模块主动断开和服务端的连接,然后释放 MQTT 资源,
+
+参数设置也会释放。
+
+###### 3.2.2 双向认证
+
+双向认证和单向认证原理基本相差不大,只是除了客户端需要认证服务端以外,增加了服务端对客
+
+户端的认证。在此认证模式下,客户端需要CA 证书、客户端证书与客户端密钥,服务器端需要CA 证
+
+书、服务器证书与服务器密钥。
+
+步骤 1:模块进行初始化操作,具体流程请参考附录B 。
+
+步骤 2:模块进行拨号上网。
+
+步骤 3:配置MQTTS 加密参数。
+
+1. 
+执行“AT+MQTTTLS=sslmode,1”指令,将MQTT 连接配置为安全加密模式。
+
+2. 
+执行“AT+MQTTTLS=authmode,1”指令,再向模块写入CA 证书、客户端证书和客户端密钥。
+
+3. 
+向模块写入CA 文件:
+
+使用指令“AT+CERTADD= rootca.pem,1758”,表示向模块写入一份名为rootca.pem、长度为
+
+1758 字节的CA 证书文件。指令发送后会出现“CONNECT”字样,这时将CA 证书文件导入,
+
+导入成功后会有回码“+CERTADD:1758,OK”,表示证书已成功写入模块。
+
+当认证方式配置为双向认证时,需要向客户端(模块)写入CA 证书、客户端证书与客户端密钥。
+
+Neoway SSL/TLS 应用指南
+
+MQTTS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+20
+
+4. 
+使用指令“AT+CERTCHECK= rootca.pem”确认证书是否成功写入。
+
+5. 
+使用指令“AT+MQTTTLS=rootca, rootca.pem”,表示将写入的CA 证书配置到客户端(模块端)。
+
+6. 
+同理向客户端证书和客户端密钥,并进行确认。
+
+使用指令“AT+CERTADD= nwy_all.cert.pem,1220”、“AT+CERTADD=
+
+nwy_all.private.key,1675”,表示向模块写入一份名为nwy_all.cert.pem、长度为1220 字节的客
+
+户端证书文件和一份名为nwy_all.private.key、长度为1675 字节的客户端密钥文件。
+
+7. 
+使用指令“AT+CERTCHECK= nwy_all.cert.pem”、“AT+CERTCHECK= nwy_all.private.key”确
+
+认证书和密钥是否成功写入。
+
+8. 
+使用指令“ AT+MQTTTLS=clientcacert, nwy_all.cert.pem ”、“ AT+MQTTTLS=clientkey,
+
+nwy_all.private.key”,表示将写入的客户端证书和客户端密钥配置到客户端(模块端)。
+
+9. 
+执行“AT+MQTTTLS?”指令,确认当前SSL/TLS 加密参数配置内容是否正确。
+
+步骤 4:配置MQTTS 连接参数。
+
+1. 
+使用指令“AT+MQTTCONNPARAM=<clientdID>,<username>, <password>”对用户设备ID、用
+
+户名和密码进行设置。
+
+2. 
+根据需要使用指令“AT+MQTTWILLPARAM=<retained>,<qos>, <topicname>,<message>”设置
+
+遗嘱主题(最大长度128Bytes)与遗嘱消息(最大长度1024Bytes)。
+
+步骤 5:使用指令“AT+MQTTCONN=<"host">,<clean>,<keep_alive>”建立MQTTS 连接。
+
+其中,“host”为服务器地址(url:port);“clean”为是否清除session,默认为0,表示不清除;
+
+keep_alive 时间取值范围为[20,180],单位为s,默认为60s。
+
+步骤 6:执行“AT+MQTTPUB=<retained>,<qos>,<"topicname">,<"message">”指令发布主题。其中,
+
+“retained”为保留标志,“qos”为服务质量,“topicname”为发布主题(最大长度
+
+128Bytes),“message”为发布消息(最大长度为1024Bytes)。
+
+步骤 7:执行“AT+MQTTSUB=<"topicname">,<qos>”指令进行主题的订阅。
+
+步骤 8:执行“AT+MQTTDISCONN”指令,使模块主动断开和服务端的连接,然后释放 MQTT 资源,
+
+参数设置也会释放。
+
+##### 3.3 示例
+
+以下流程示例在执行的过程中需首先对模块进行初始化后,才能进行。初始化流程,详见附录B 。
+
+以下示例中,单向认证使用中国移动的OneNET 平台,双向认证使用亚马逊平台。
+
+###### 3.3.1 单向认证
+
+```
+AT+XIIC=1
+```
+
+Neoway SSL/TLS 应用指南
+
+MQTTS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+21
+
+OK
+
+```
+AT+XIIC?
+```
+
++XIIC: 1,10.193.121.203
+
+```
+AT+MQTTTLS=sslmode,1
+```
+
+OK
+
+```
+AT+MQTTTLS=authmode,0
+```
+
+OK
+
+```
+AT+CERTADD=MQTTS-certificate.pem,1180
+```
+
+CONNECT
+
++CERTADD: 1180,OK
+
+```
+AT+CERTCHECK=MQTTS-certificate.pem
+```
+
++CERTCHECK: MQTTS-certificate.pem,OK
+
+```
+AT+MQTTTLS=rootca,MQTTS-certificate.pem
+```
+
+OK
+
+```
+AT+MQTTTLS?
+```
+
++MQTTTLS: 1,0,MQTTS-certificate.pem,,
+
+OK
+
+```
+AT+MQTTCONNPARAM="N58","421872","version=2018-10-
+```
+
+31&res=products%2F421872%2Fdevices%2FN58&et=1650974923&method=md5&sign=WKGlErACNTMV5n4XIAxwtQ%3
+
+D%3D" OK
+
+```
+AT+MQTTCONN="183.230.40.16:8883",1,60
+```
+
+OK
+
+###### 3.3.2 双向认证
+
+```
+AT+XIIC=1
+```
+
+OK
+
+```
+AT+XIIC?
+```
+
++XIIC: 1,10.100.55.5
+
+OK
+
+```
+AT+MQTTTLS=sslmode,1
+```
+
+OK
+
+```
+AT+MQTTTLS=authmode,1
+```
+
+OK
+
+```
+AT+CERTADD=rootca.pem,1758
+```
+
+CONNECT
+
++CERTADD: 1758,OK
+
+```
+AT+CERTADD=nwy_all.cert.pem,1220
+```
+
+CONNECT
+
++CERTADD: 1220,OK
+
+```
+AT+CERTADD=nwy_all.private.key,1675
+```
+
+CONNECT
+
++CERTADD: 1675,OK
+
+```
+AT+SSLTCPCFG=cacert,rootca.pem
+```
+
+OK
+
+```
+AT+SSLTCPCFG=clientcert,nwy_all.cert.pem
+```
+
+OK
+
+```
+AT+SSLTCPCFG=clientkey,nwy_all.private.key
+```
+
+OK
+
+```
+AT+MQTTTLS=rootca,rootca.pem
+```
+
+OK
+
+```
+AT+MQTTTLS=clientcert,nwy_all.cert.pem
+```
+
+OK
+
+Neoway SSL/TLS 应用指南
+
+MQTTS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+22
+
+```
+AT+MQTTTLS=clientkey,nwy_all.private.key
+```
+
+OK
+
+```
+AT+MQTTTLS?
+```
+
++MQTTTLS: 1,1,rootca.pem,nwy_all.cert.pem,nwy_all.private.key
+
+OK
+
+```
+AT+MQTTCONNPARAM="a1epg1vh6w7hlk","",""
+```
+
+OK
+
+AT+MQTTCONN="a1epg1vh6w7hlk.iot.us-east-2.amazonaws.com:8883",1,60 
+OK
+
+Neoway SSL/TLS 应用指南
+
+TCPS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+23
+
+#### 4 TCPS 应用流程
+
+TCPS 与HTTPS 基本原理相差不大。TCP 协议加上SSL/TLS 协议,使得普通的TCP 连接变成了
+
+可以安全传输数据的TCPS 连接。
+
+和HTTPS 一样,按照身份认证可以分为无需认证,单向认证和双向认证。
+
+##### 4.1 无需认证
+
+配置无需认证时,TCPS 的客户端或者服务器端就会接受不受信的证书,相当于不认证传输数据前
+
+通信双方的身份,而是直接进行之后的协商加密算法、交换密钥过程。
+
+步骤 1:模块进行初始化操作,具体流程请参考附录B 。
+
+步骤 2:模块进行拨号上网。
+
+步骤 3:配置TCPS 加密参数。
+
+1. 
+配置SSL/TLS 协议版本。
+
+例如,指令“AT+SSLTCPCFG=sslversion,3”,表示配置SSL/TLS 协议版本为TLS1.2。
+
+2. 
+执行“AT+ SSLTCPCFG =authmode,0”指令将SSL/TLS 认证方式配置为无需认证。
+
+当SSL/TLS 认证方式配置无需认证时,不需要配置证书。
+
+步骤 4:执行“AT+SSLTCPSETUP= <n>,<ip>,<port>,<mode>”建立TCPS 连接。
+
+步骤 5:执行“AT+SSLTCPSEND=<n>,<data_length>”向服务器发送数据。
+
+当接收到服务器数据时,会主动上报收到的数据内容。
+
+步骤 6:当需要主动关闭连接时可执行“AT+SSLTCPCLOSE=<n>”关闭TCPS 连接。
+
+##### 4.2 单向认证
+
+配置单向认证时,TCPS 的客户端将会验证服务器的身份,验证通过后才会进行之后的协商加密算
+
+法以及交换密钥过程。此认证方式下,客户端需要CA 证书,服务器端需要服务器证书和服务器密钥。
+
+步骤 1:模块进行初始化操作,具体流程请参考附录B 。
+
+Neoway SSL/TLS 应用指南
+
+TCPS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+24
+
+步骤 2:模块进行拨号上网。
+
+步骤 3:配置TCPS 加密参数。
+
+1. 
+配置SSL/TLS 协议版本。
+
+例如,指令“AT+SSLTCPCFG=sslversion,3”,表示配置SSL/TLS 协议版本为TLS1.2。
+
+2. 
+执行“AT+SSLTCPCFG =authmode,1”指令将SSL/TLS 认证方式配置为单向认证。
+
+3. 
+向模块写入CA 证书。
+
+例如,使用指令“AT+CERTADD=ca.crt, 1196”,表示向模块写入一份名为ca.crt、长度为1196
+
+字节的CA 证书文件。
+
+当SSL/TLS 认证方设置为单向认证时,需要向客户端(模块)配置CA 证书。
+
+4. 
+执行“AT+CERTCHECK=ca.crt”指令,确认证书是否成功写入。
+
+5. 
+执行“AT+SSLTCPCFG =cacert,ca.crt”指令将写入的CA 证书配置到客户端(模块端)。
+
+6. 
+执行“AT+SSLTCPCFG?”指令查询当前SSL/TLS 参数配置,确认为所配置内容。
+
+步骤 4:执行“AT+SSLTCPETUP= <n>,<ip>,<port>,<mode>”建立TCPS 连接。
+
+步骤 5:执行“AT+SSLTCPSEND=<n>,<data_length>”向服务器发送数据。
+
+当接收到服务器数据时,会主动上报收到的数据内容。
+
+步骤 6:需要关闭连接时可执行“AT+SSLTCPCLOSE=<n>”关闭TCPS 连接。
+
+##### 4.3 双向认证
+
+双向认证和单向认证原理基本差不多,只是除了客户端需要认证服务端以外,增加了服务端对客户
+
+端的认证。在此认证模式下,客户端需要CA 证书、客户端证书与客户端密钥,服务器端需要CA 证书、
+
+服务器证书与服务器密钥。
+
+步骤 1:模块进行初始化操作,具体流程请参考附录B 。
+
+步骤 2:模块进行拨号上网。
+
+步骤 3:配置TCPS 加密参数。
+
+1. 
+配置SSL/TLS 协议版本。
+
+例如,指令“AT+ SSLTCPCFG =sslversion,3”,表示配置SSL/TLS 协议版本为TLS1.2。
+
+2. 
+使用指令“AT+ SSLTCPCFG =authmode,2”,表示配置的SSL/TLS 认证方式为双向认证。
+
+Neoway SSL/TLS 应用指南
+
+TCPS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+25
+
+3. 
+向模块写入CA 证书。
+
+使用指令“AT+CERTADD=ca.crt, 1196”,表示向模块写入一份名为ca.crt、长度为1196 字节的
+
+CA 证书文件。
+
+当SSL/TLS 认证方设置为双向认证时,所以需要向客户端(模块)写入CA 证书、客户端证书与客
+
+户端密钥。
+
+4. 
+使用指令“AT+CERTCHECK=ca.crt”确认证书是否成功写入。
+
+5. 
+使用指令“AT+SSLTCPCFG=cacert,ca.crt”,将写入的CA 证书配置到客户端(模块端)。
+
+6. 
+同理写入客户端证书和客户端密钥。
+
+使用指令“AT+CERTADD=client.crt,1204”、“AT+CERTADD=client.key,1743”,表示向模块写入
+
+一份名为client.crt、长度为1204 字节的客户端证书文件和一份名为client.key、长度为1743 字
+
+节的客户端密钥文件。
+
+7. 
+使用指令“AT+CERTCHECK=client.crt”、“AT+CERTCHECK=client.key”确认证书和密钥是否成
+
+功写入。
+
+8. 
+使用指令“AT+SSLTCPCFG=clientcacert,client.crt”、“AT+SSLTCPCFG=clientkey,client.key”,表
+
+示将写入的客户端证书和客户端密钥配置到客户端(模块端)。
+
+9. 
+执行“AT+TCPSCFG?”指令,确认当前SSL/TLS 加密参数配置内容是否正确。
+
+步骤 4:执行“AT+SSLTCPSETUP=<n>,<ip>,<port>,<mode>”建立TCPS 连接。
+
+步骤 5:执行“AT+SSLTCPSEND=<n>,<data_length>”向服务器发送数据。
+
+当接收到服务器数据时,会主动上报收到的数据内容。
+
+步骤 6:关闭TCPS 连接。
+
+需要主动关闭连接时可执行“AT+SSLTCPCLOSE=<n>”关闭TCPS 连接。
+
+##### 4.4 示例
+
+以下流程示例是使用我司自己搭建的SSLTCP 服务器,证书也是自己制作的,用户在使用前需自行
+
+搭建服务器及制作证书。且在执行的过程中需首先对模块进行初始化后,才能进行。初始化流程详见附
+
+录B 。
+
+###### 4.4.1 无需认证
+
+```
+AT+XIIC=1
+```
+
+OK
+
+```
+AT+XIIC?
+```
+
++XIIC: 1,10.100.101.229
+
+Neoway SSL/TLS 应用指南
+
+TCPS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+26
+
+OK
+
+```
+AT+SSLTCPCFG=sslversion,3
+```
+
+OK
+
+```
+AT+SSLTCPCFG=authmode,0
+```
+
+OK
+
+```
+AT+SSLTCPCFG?
+```
+
++SSLTCPCFG:3,0,,,
+
+OK
+
+```
+AT+SSLTCPSETUP=1,183.239.240.40,12013,0
+```
+
+OK
+
++SSLTCPSETUP: 1,OK
+
+```
+AT+SSLTCPSEND=1,10
+```
+
+>
+
++SSLTCPSEND: 1,OK
+
++SSLTCPRECV: 1,10,1234567890
+
+```
+AT+SSLTCPCLOSE=1
+```
+
++SSLTCPCLOSE: 1,OK
+
+###### 4.4.2 单向认证
+
+```
+AT+XIIC=1
+```
+
+OK
+
+```
+AT+XIIC?
+```
+
++XIIC: 1,10.100.101.229
+
+OK
+
+```
+AT+SSLTCPCFG=sslversion,3
+```
+
+OK
+
+```
+AT+SSLTCPCFG=authmode,1
+```
+
+OK
+
+```
+AT+CERTADD=ca.crt,1196
+```
+
+CONNECT
+
++CERTADD: 1196,OK
+
+```
+AT+CERTCHECK=ca.crt
+```
+
++CERTCHECK: ca.crt,OK
+
+```
+AT+SSLTCPCFG=cacert,ca.crt
+```
+
+OK
+
+```
+AT+SSLTCPCFG?
+```
+
++SSLTCPCFG:3,1,ca.crt,,
+
+OK
+
+```
+AT+SSLTCPSETUP=1,183.239.240.40,12013,0
+```
+
+OK
+
++SSLTCPSETUP: 1,OK
+
+```
+AT+SSLTCPSEND=1,10
+```
+
+>
+
++SSLTCPSEND: 1,OK
+
++SSLTCPRECV: 1,10,1234567890
+
+```
+AT+SSLTCPCLOSE=1
+```
+
++SSLTCPCLOSE: 1,OK
+
+###### 4.4.3 双向认证
+
+```
+AT+XIIC=1
+```
+
+OK
+
+Neoway SSL/TLS 应用指南
+
+TCPS 应用流程
+
+深圳市有方科技股份有限公司版权所有 
+27
+
+```
+AT+XIIC?
+```
+
++XIIC: 1,10.100.104.208
+
+OK
+
+```
+AT+SSLTCPCFG=sslversion,3
+```
+
+OK
+
+```
+AT+SSLTCPCFG=authmode,2
+```
+
+OK
+
+```
+AT+CERTADD=ca.crt,1196
+```
+
+CONNECT
+
++CERTADD: 1196,OK
+
+```
+AT+CERTADD=client.crt,1204
+```
+
+CONNECT
+
++CERTADD: 1204,OK
+
+```
+AT+CERTADD=client.key,1743
+```
+
+CONNECT
+
++CERTADD: 1743,OK
+
+```
+AT+CERTCHECK?
+```
+
+ca.crt
+
+client.crt
+
+client.key
+
+OK
+
+```
+AT+SSLTCPCFG=cacert,ca.crt
+```
+
+OK
+
+```
+AT+SSLTCPCFG=clientcert,client.crt
+```
+
+OK
+
+```
+AT+SSLTCPCFG=clientkey,client.key
+```
+
+OK
+
+```
+AT+SSLTCPCFG?
+```
+
++SSLTCPCFG:3,2,ca.crt,client.crt,client.key
+
+OK
+
+```
+AT+SSLTCPSETUP=1,183.239.240.40,12013,0
+```
+
+OK
+
++SSLTCPSETUP: 1,OK
+
+```
+AT+SSLTCPSEND=1,10
+```
+
+>
+
++SSLTCPSEND: 1,OK
+
++SSLTCPRECV: 1,10,1234567890
+
+```
+AT+SSLTCPCLOSE=1
+```
+
++SSLTCPCLOSE: 1,OK
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+28
+
+#### A 相关AT 指令
+
+##### A.1 HTTPS 相关指令
+
+###### A.1.1 AT+CERTADD–添加SSL 证书
+
+向模块写入证书。
+
+写入过程可以通过“+++”中断。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行 
+AT+CERTADD=<file_name>,<length
+><CR>
+
+<CR><LF>CONNECT<CR><LF> 
+<CR><LF>+CERTADD: <length>,OK<CR><LF> 
+Or 
+<CR><LF>+CERTADD: ERROR<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<file_name> 
+写入模块的证书名称
+
+<length> 
+写入的长度
+
+###### A.1.2 AT+CERTCHECK–SSL 证书确认
+
+检查证书。
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+29
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行 
+AT+CERTCHECK=<file_name><CR>
+
+<CR><LF>+CERTCHECK: <file_name>,OK 
+Or 
+<CR><LF>+CERTCHECK: ERROR
+
+查询 
+AT+CERTCHECK?<CR>
+
+<CR><LF><file_name> 
+[<CR><LF><file_name>] 
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<file_name> 
+要确认的证书名称。
+
+###### A.1.3 AT+HTTPSCFG–配置HTTPS 参数
+
+配置HTTPS 加密选项。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+设置 
+AT+HTTPSCFG=<type>,<type_na
+me><CR>
+
+<CR><LF>OK<CR><LF> 
+Or 
+<CR><LF>ERROR<CR><LF>
+
+查询 
+AT+HTTPSCFG?<CR>
+
+<CR><LF>+HTTPSCFG: <sslversion 
+type_name>,<authmode type_name>,<ciphersuite 
+type_name>,<cacert type_name>,<clientcert 
+type_name>,<clientkey type_name> 
+<CR><LF>OK<CR><LF>
+
+测试 
+AT+HTTPSCFG=?<CR> 
+<CR><LF>+HTTPSCFG: <type>,<type_name> 
+<CR><LF>OK<CR><LF>
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+30
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<type> 
+配置SSL 选项。
+
+sslversion: SSL 协议版本
+
+authmode: 安全认证模式
+
+ciphersuite: 加密控件
+
+cacert: 根证书
+
+clientcert: 客户端证书
+
+clientkey: 客户端密匙
+
+<type_name> 
+<type>和<type_name>参数的取值,对应关系如下
+
+sslversion 
+0:SSL3.0
+
+1:TLS1.0
+
+2:TLS1.1
+
+3:TLS1.2
+
+authmode
+
+0:No authentication
+
+1:Manage server authentication
+
+2:Manage server and client authentication if requested by the remote server
+
+ciphersuite: reserved. 
+cacert 根证书文件名
+
+clientcert 客户端证书文件名
+
+clientkey 客户端密匙文件名
+
+###### A.1.4 AT+HTTPSPARA–设置HTTPS 参数
+
+设置HTTPS 命令参数。
+
+新的HTTPS 请求需要设置新的HTTPS PARAMETER。若执行+HTTPSCLOSE,链路关闭的同时,
+
+HTTPS 参数会被清空。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+设置 
+AT+HTTPSPARA=<para>,<para_value><CR> 
+<CR><LF>OK<CR><LF> 
+Or
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+31
+
+<CR><LF>ERROR<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<para> 
+HTTPS 参数,支持两个参数设置
+
+url:目标路径
+
+port:目标端口号(未设置缺省值)
+
+keepalive:设置长连接,http 协议的长连接,默认值para_value=0,para_value
+
+为1 时打开长连接
+
+recvmode:接收模式,para_value=0 默认接收模式,一个HTTP 响应只包含一个
+
++HTTPRECV:头表示;para_value=1,数据以+HTTPRECV: <length>,<data>形
+
+式呈现
+
+<para_value> 
+对应<para>的值,其中url 参数值最大为1022 个字节,url 支持域名解析,url 需加
+
+双引号
+
+###### A.1.5 AT+HTTPSSETUP–建立HTTPS 链路
+
+建立HTTPS 链接。正确设置目标地址和端口号才能链接成功。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行 
+AT+HTTPSSETUP<CR>
+
+<CR><LF>OK<CR><LF> 
+Or 
+<CR><LF>ERROR<CR><LF>
+
+###### 响应时间
+
+最大响应时间60s。
+
+###### 参数
+
+无
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+32
+
+###### A.1.6 AT+HTTPSACTION–发送HTTPS 请求
+
+执行HTTPS 请求。
+
+用户自定义报文时需遵循HTTPS 协议;用户在自定义报文的时候注意使用的工具里头是否会自带
+
+回车和换行这些字符。
+
+###### 命令格式
+
+类型 
+命令 
+响应格式
+
+执行
+
+AT+HTTPSACTION=<mode>[,<length>
+[,<type>]<CR> 
+AT+HTTPSACTION=<mode>[,<offset>,
+<size>]<CR>
+
+<CR><LF>OK<CR><LF> 
+Or 
+<CR><LF>><post_content><CR><LF> 
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+响应超时时间为60 秒。
+
+###### 参数
+
+<mode> 
+HTTP 请求方式,可取值为0,1,2,99
+
+0:GET
+
+1:HEAD
+
+2:POST
+
+99:OPEN_MODE,用户自己定义报文模式
+
+<length> 
+POST 内容长度或自定义报文长度,在<mode>为POST 和OPEN_MODE 时必
+
+须设置,最大长度为2048
+
+<type> 
+POST 请求数据类型
+
+1:text
+
+2:json
+
+3:xml
+
+4:html
+
+<offset> 
+偏移量,通过GET 方式下载文件时,可以指定下载起始位置
+
+<size> 
+下载长度,通过GET 方式下载文件时,可以指定下载长度
+
+<post_content> 
+HTTPSPOST 发送的数据
+
+###### A.1.7 AT+HTTPSCLOSE–主动关闭HTTPS 链路
+
+关闭HTTPS 链接。
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+33
+
+执行+HTTPSCLOSE 命令,在关闭HTTPS 链路的同时,会清除+HTTPSPARA 命令设置的参数。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行 
+AT+HTTPCLOSE<CR> 
+<CR><LF>OK<CR><LF> 
+<CR><LF>+HTTPSCLOSE: HTTPS Link Closed<CR><LF>
+
+###### 响应时间
+
+最大响应时间60s。
+
+###### 参数
+
+无
+
+##### A.2 MQTTS 相关指令
+
+###### A.2.1 AT+CERTADD–添加SSL 证书
+
+向模块写入证书
+
+写入过程可以通过“+++”中断。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行 
+AT+CERTADD=<file_name>,<length
+><CR>
+
+<CR><LF>CONNECT<CR><LF> 
+<CR><LF>+CERTADD: <length>,OK<CR><LF> 
+Or 
+<CR><LF>+CERTADD: ERROR<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+34
+
+###### 参数
+
+<file_name> 
+写入模块的证书名称
+
+<length> 
+写入的长度
+
+###### A.2.2 AT+CERTCHECK–SSL 证书确认
+
+检查证书。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行 
+AT+CERTCHECK=<file_name><CR>
+
+<CR><LF>+CERTCHECK: <file_name>,OK 
+Or 
+<CR><LF>+CERTCHECK: ERROR
+
+查询 
+AT+CERTCHECK?<CR>
+
+<CR><LF><file_name> 
+[<CR><LF><file_name>] 
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<file_name> 
+要确认的证书名称。
+
+###### A.2.3 AT+MQTTTLS - TLS 参数配置
+
+MQTT TLS 参数配置。
+
+通过AT+CERTADD 导入证书。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+设置 
+AT+MQTTTLS=<type>,<val
+ue><CR>
+
+<CR><LF>OK<CR><LF> 
+Or 
+<CR><LF>ERROR<CR><LF>
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+35
+
+查询 
+AT+MQTTTLS?<CR>
+
+<CR><LF>+MQTTTLS: 
+<sslmode>,<authmode>,
+
+<rootca_name>,<clientcert_name>,<clientkey_name> 
+<CR><LF>OK<CR><LF>
+
+测试 
+AT+MQTTTLS=?<CR> 
+<CR><LF>+MQTTTLS: <type>,<value> 
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<type> 
+配置参数类型
+
+<value> 
+配置参数值
+
+<sslmode> 
+安全加密模式
+
+0:不加密
+
+1:加密
+
+<authmode> 
+安全认证模式
+
+0:verify optional
+
+1:verify required
+
+<rootca> 
+string,CA 证书
+
+<clientcert> 
+string,客户端证书
+
+<clientkey> 
+string,客户端密钥
+
+###### A.2.4 AT+MQTTCONNPARAM–用户参数设置
+
+设置ID、用户名、密码参数。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+设置 
+AT+MQTTCONNPARAM=<clientdID>,
+<username>,<password><CR>
+
+<CR><LF>OK<CR><LF> 
+Or 
+<CR><LF>ERROR<CR><LF>
+
+查询 
+AT+MQTTCONNPARAM?<CR>
+
+<CR><LF>+MQTTCONNPARAM:<"clientID"
+>,<"username">,<"password"> 
+<CR><LF>OK<CR><LF>
+
+测试 
+AT+MQTTCONNPARAM=?<CR> 
+<CR><LF>OK<CR><LF>
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+36
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<clientID> 
+设备ID,最大长度256。
+
+<username> 
+用户名,最大长度512。
+
+<password> 
+密码,最大长度256。
+
+<pubk> 
+建立连接的适配参数
+
+###### A.2.5 AT+MQTTWILLPARAM–遗嘱设置
+
+设置遗嘱信息。
+
+在MQTT 已经连接的情况下设置参数无效,掉电后参数需重新设置。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+设置 
+AT+MQTTWILLPARAM=<retained>,<q
+os>,<topicname>,<message><CR>
+
+<CR><LF>+GNSSSTATE: <status><CR><LF> 
+Or 
+<CR><LF>OK<CR><LF>
+
+查询 
+AT+MQTTWILLPARAM?<CR>
+
+<CR><LF>+MQTTWILLPARAM:<retained>,<qo
+s>,<"topicname">,<"message"> 
+<CR><LF>OK<CR><LF>
+
+测试 
+AT+MQTTWILLPARAM=?<CR> 
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<retained> 
+保留标志,数字类型。
+
+<qos> 
+服务质量,目前仅支持Qos=0 和1。
+
+<topicname> 
+遗嘱主题,最大长度128。
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+37
+
+<message> 
+遗嘱消息,最大长度1024。
+
+###### A.2.6 AT+MQTTCONN–连接命令
+
+连接MQTT 服务器。
+
+在连接的过程中等待连接返回值,没有返回值的情况下,不能再次进行连接操作。
+
+当连接成功后,没有主动断开情况下如果模块主动上报+MQTTDISCONNED: Link Closed,需手动
+
+进行连接。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行 
+AT+MQTTCONN=<"host">,<clean>,<ke
+ep_alive><CR>
+
+<CR><LF>OK<CR><LF> 
+Or 
+<CR><LF>ERROR<CR><LF>
+
+查询 
+AT+MQTTCONN?<CR>
+
+<CR><LF>+MQTTCONN:<"ip:port">,<clean>,
+<keep_alive> 
+<CR><LF>OK<CR><LF>
+
+测试 
+AT+MQTTCONN=?<CR> 
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间60s。
+
+###### 参数
+
+<"host"> 
+服务器地址(url:port)。
+
+<clean> 
+是否清除session,数字类型
+
+0-不清除(默认)
+
+1-清除
+
+<keep_alive> 
+keepAlive 时间设置,取值范围[20,180],单位s。
+
+###### A.2.7 AT+MQTTSUB–订阅命令
+
+订阅主题。
+
+当SUB 失败后,查询MQTT 和网络状态再进行操作。当网络不佳的情况下,返回值较慢。
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+38
+
+查询指令只有连接情况下才能查询到,只能查询到当前最后一次订阅的qos 和topic。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行 
+AT+MQTTSUB=<"topicname">,<qos><CR>
+
+<CR><LF>OK<CR><LF> 
+Or 
+<CR><LF>ERROR<CR><LF>
+
+查询 
+AT+MQTTSUB?<CR>
+
+<CR><LF>+MQTTSUB:<"topicname">,
+<qos> 
+<CR><LF>OK<CR><LF>
+
+测试 
+AT+MQTTSUB=?<CR> 
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间30s。
+
+###### 参数
+
+<"topicname"> 订阅的主题,最大长度128。
+
+<qos> 
+服务质量,目前支持Qos=0,1 和2
+
+###### A.2.8 AT+MQTTPUB–发布主题
+
+主题发布。
+
+当网络不佳的情况下,返回值较慢。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行 
+AT+MQTTPUB=<retained>,<qos>,<"topic
+name">,<"message"><CR>
+
+<CR><LF>OK<CR><LF> 
+Or 
+<CR><LF>ERROR<CR><LF>
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+39
+
+###### 响应时间
+
+最大响应时间30s。
+
+###### 参数
+
+<retained> 
+保留标志,数字类型,0 和1。
+
+<qos> 
+服务质量,目前仅支持Qos=0,1 和2。
+
+<"topicname"> 
+发布的主题,最大长度128。
+
+<"message"> 
+发布的消息,最大长度1024。
+
+###### A.2.9 AT+MQTTDISCONN–关闭MQTT 连接
+
+关闭MQTT 连接。
+
+终端主动断开和服务端的连接,然后释放MQTT 资源,参数设置也会释放,如果需要pub 消息,
+
+需要重新发起参数设置再连接设备。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行 
+AT+MQTTDISCONN<CR> 
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间30s。
+
+###### 参数
+
+无
+
+##### A.3 TCPS 相关指令
+
+###### A.3.1 AT+CERTADD–添加SSL 证书
+
+向模块写入证书
+
+写入过程可以通过“+++”中断。
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+40
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行 
+AT+CERTADD=<file_name>,<length
+><CR>
+
+<CR><LF>CONNECT<CR><LF> 
+<CR><LF>+CERTADD: <length>,OK<CR><LF> 
+Or 
+<CR><LF>+CERTADD: ERROR<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<file_name> 
+写入模块的证书名称
+
+<length> 
+写入的长度
+
+###### A.3.2 AT+CERTCHECK–SSL 证书确认
+
+检查证书。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行 
+AT+CERTCHECK=<file_name><CR>
+
+<CR><LF>+CERTCHECK: <file_name>,OK 
+Or 
+<CR><LF>+CERTCHECK: ERROR
+
+查询 
+AT+CERTCHECK?<CR>
+
+<CR><LF><file_name> 
+[<CR><LF><file_name>] 
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<file_name> 
+要确认的证书名称。
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+41
+
+###### A.3.3 AT+SSLTCPCFG–SSL TCP 配置参数
+
+配置SSL 加密选项。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+设置 
+AT+SSLTCPCFG=<type>,<typ
+e_name><CR>
+
+<CR><LF>OK<CR><LF> 
+Or 
+<CR><LF>ERROR<CR><LF>
+
+查询 
+AT+SSLTCPCFG?<CR>
+
+<CR><LF>+SSLTCPCFG: <sslversion 
+type_name>,<authmode type_name>,<ciphersuite 
+type_name>,<cacert type_name>,<clientcert 
+type_name>,<clientkey type_name> 
+<CR><LF>OK<CR><LF>
+
+测试 
+AT+SSLTCPCFG=?<CR> 
+<CR><LF>+SSLTCPCFG: <type>,<type_name> 
+<CR><LF>OK<CR><LF>
+
+###### 响应时间
+
+最大响应时间300ms。
+
+###### 参数
+
+<type> 
+配置SSL 选项。
+
+sslversion: SSL 协议版本
+
+authmode: 安全认证模式
+
+ciphersuite: 加密控件
+
+cacert: 根证书
+
+clientcert: 客户端证书
+
+clientkey: 客户端密匙
+
+<type_name> 
+<type>和<type_name>参数的取值,对应关系如下:
+
+⚫ 
+sslversion 
+0:SSL3.0
+
+1:TLS1.0
+
+2:TLS1.1
+
+3:TLS1.2
+
+⚫ 
+authmode
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+42
+
+0:No authentication,不需要设置cacert、clientcert、clientkey 等内容。
+
+1:Manage server authentication
+
+2:Manage server and client authentication if requested by the remote server
+
+⚫ 
+ciphersuite: reserved.
+
+⚫ 
+cacert 根证书文件名
+
+⚫ 
+clientcert 客户端证书文件名
+
+⚫ 
+clientkey 客户端密匙文件名
+
+###### A.3.4 AT+SSLTCPSETUP–建立SSL TCP 连接
+
+建立SSL TCP 连接。
+
+建立SSL TCP 连接之前,需要先激活PPP,申请到IP 地址。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行 
+AT+SSLTCPSETUP=<n>,<ip>,<por
+t>, <mode><CR>
+
+<CR><LF>OK<CR><LF> 
+<CR><LF>+SSLTCPSETUP: <n>,<status> 
+Or 
+<CR><LF>CONNECT 
+Or 
+<CR><LF>+SSLTCPSETUP: ERROR 
+Or 
+<CR><LF>+SSLTCPSETUP: GPRS 
+DISCONNECTION
+
+查询 
+AT+SSLTCPSETUP?
+
+<CR><LF>+SSLTCPSETUP: <socket_id>,<ip>, 
+<port>,<mode> 
+[<CR><LF>+SSLTCPSETUP: <socket_id>,<ip>, 
+<port>,<mode>]… 
+<CR><LF>OK<CR><LF>
+
+测试 
+AT+SSLTCPSETUP=? 
+<CR><LF>+SSLTCPSETUP: <socket_id>,<ip>, 
+<port>,<mode><CR><LF>
+
+###### 响应时间
+
+最大响应时间60s。
+
+###### 参数
+
+<n> 
+链路号,范围0-5,用来区分与服务器的连接
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+43
+
+<IP> 
+服务器的IP 地址/域名
+
+<Port> 
+服务器的端口
+
+<Mode> 
+传输模式
+
+0:非透传
+
+1:透传
+
+<status> 
+OK 
+ERROR1 
+AUTHFAIL 
+FAIL
+
+###### A.3.5 AT+SSLTCPSEND–SSL TCP 数据发送
+
+SSL TCP 数据发送。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行 
+AT+SSLTCPSEND=<n>,<data_length>
+
+<CR><LF>> 
+<CR><LF>+SSLTCPSEND: 
+<socket_id>,<result> 
+Or 
+<CR><LF>+SSLTCPSEND: Data length 
+error<CR><LF>
+
+测试 
+AT+SSLTCPSEND=?
+
+<CR><LF>+SSLTCPSEND: (value range 
+of<n>),(value range 
+of<data_length>)<CR><LF>
+
+###### 响应时间
+
+最大响应时间30s。
+
+###### 参数
+
+<n> 
+链路号 0~5
+
+<data_length> 要发送的数据长度,取值范围 1-4096
+
+<result> 
+OK 
+FAIL
+
+Neoway SSL/TLS 应用指南
+
+A 相关AT 指令
+
+深圳市有方科技股份有限公司版权所有 
+44
+
+###### A.3.6 AT+SSLTCPCLOSE–SSL TCP 关闭连接指令
+
+关闭SSL TCP 连接。
+
+###### 命令格式
+
+###### 类型 命令 响应格式
+
+执行 
+AT+SSLTCPCLOSE=<socket_id>
+
+<CR><LF>+SSLTCPCLOSE: 
+<socket_id>,<result> 
+Or 
+<CR><LF>+SSLTCPCLOSE: ERROR
+
+主动上报 
++SSLTCPCLOSE: <socket_id>,Link Closed
+
+###### 响应时间
+
+最大响应时间30s。
+
+###### 参数
+
+<n> 
+链路号 0~5
+
+<result> 
+OK 
+ERROR 
+Link Closed
+
+Neoway SSL/TLS 应用指南
+
+B 初始化流程
+
+深圳市有方科技股份有限公司版权所有 
+45
+
+#### B 初始化流程
+
+Yes
+
+No
+
+Yes
+
+Yes
+
+Yes
+
+Yes
+
+1. 上电开机
+
+延时
+
+OK?
+
+2. AT
+
+Yes
+
+return code?
+
+3. AT+CGSN
+
+Yes
+
+return code?
+
+4. AT+CGMR
+
+Yes
+
+5. AT+CPIN?
+
++CPIN: READY ?
+
+6. AT+CIMI
+
+OK?
+
+7. AT+CSQ
+
++CSQ: <signal>,<ber>
+ and 12≤signal≤31 ?
+
+8. AT+CREG=2
+
+return code?
+
+9. AT+CREG?
+
++CREG: 2,1 or
+
++CREG: 2,5?
+
+12. AT+CGATT?
+
++CGATT: 1?
+
+Yes
+
+Yes
+
+10. AT+CREG=0
+
+return code?
+
+11. AT+CGATT=1
+
+Yes
+
+No
+
+No
+
+AT$MYSYSINFO
+
+return code ?
+
+Yes
+
+```
+AT+XGAUTH=1,1,"gsm","1234"
+```
+
+OK?
+
+```
+AT+CGDCONT=1,"IP","CMNET"
+```
+
+OK?
+
+Yes
+
+Yes
+
+Yes
+
+查询<40次?
+
+查询<40次?
+
+Yes
+
+Yes
+
+No
+
+查询<15次?
+
+查询<30次?
+
+查询<3次?
+
+手动附着
+
+<30次?
+
+设置<3次?
+
+查询<3次?
+
+模块初始化完成
+
+No
+
+No
+
+No
+
+Yes
+
+Yes
+
+No
+
+No
+
+No
+
+No
+
+No
+
+No
+
+No
+
+No
+
+2G模块延时3s,4G模块延
+时10s。该过程中模块处于
+初始化阶段,可能出现未响
+应或响应出错情况。
+
+返回“+CGATT:1”则表示
+已注册上PS域业务,如果30
+s内仍未返回,建议手动附
+着CGATT流程。
+
+不设置主动上报的情况下,
+回码判断包含“+CREG:0,1
+或者+CREG:0,5”即可进入
+下一步,表示已经注册上CS
+域业务。
+60s内未注册上,建议执行
+重启模块动作。
+
+设置错误的APN可能会导致
+后续拨号动作失败,具体A
+PN信息需要和卡商确认。
+
+部分SIM卡识卡超过30s,
+建议出厂前对SIM卡作充分
+兼容性测试。
+
+Yes
+
+模块重启
+
+重启模块请参照
+《Neoway_标准模块_开关机流程操
+作指南》进行。24小时内过多的重
+启可能会导致FLASH及SIM卡不可
+逆的损坏。
+
+设置PPP拨号过程中鉴权类
+型和身份认证参数,设置错
+误的参数同样可能导致PPP
+拨号失败,CID要和设置A
+PN的参数保持一致。
diff --git a/code/sdk/zhihui/ppp/doc/Neoway_功能模块_SSL_TLS_应用指南_V1.0.pdf b/code/sdk/zhihui/ppp/doc/Neoway_功能模块_SSL_TLS_应用指南_V1.0.pdf
new file mode 100644 (file)
index 0000000..b6f955a
Binary files /dev/null and "b/code/sdk/zhihui/ppp/doc/Neoway_\345\212\237\350\203\275\346\250\241\345\235\227_SSL_TLS_\345\272\224\347\224\250\346\214\207\345\215\227_V1.0.pdf" differ
diff --git a/code/sdk/zhihui/ppp/doc/pdf_to_md.py b/code/sdk/zhihui/ppp/doc/pdf_to_md.py
new file mode 100644 (file)
index 0000000..a829b4f
--- /dev/null
@@ -0,0 +1,614 @@
+#!/usr/bin/env python3.9
+# -*- coding: utf-8 -*-
+"""
+PDF 转 Markdown 脚本
+功能:将 PDF 文件转换为 Markdown 格式
+
+支持两种模式:
+1. 结构化模式(默认):智能识别文档结构,优化层级和格式,适合大模型理解
+2. 原始模式(--raw):忠实还原 PDF 原始格式和布局,保留原始文本顺序
+
+特性:
+- 结构化模式:智能识别标题、段落、列表、表格、代码块
+- 原始模式:保留 PDF 原始布局和文本顺序
+- 可选的目录生成
+- 自动清理格式问题
+
+使用方法:
+    python3.9 pdf_to_md.py                    # 转换当前目录所有 PDF(结构化)
+    python3.9 pdf_to_md.py input.pdf          # 转换指定 PDF(结构化)
+    python3.9 pdf_to_md.py --raw input.pdf    # 原始格式转换
+    python3.9 pdf_to_md.py --toc input.pdf    # 转换并生成目录
+"""
+
+import os
+import sys
+import re
+import argparse
+from pathlib import Path
+from collections import defaultdict
+
+try:
+    import fitz  # PyMuPDF
+except ImportError:
+    print("错误: 需要安装 PyMuPDF 库")
+    print("请运行: pip3 install PyMuPDF")
+    sys.exit(1)
+
+
+class PDFStructureAnalyzer:
+    """PDF 结构分析器:分析文档的字体、大小等属性,识别结构"""
+    
+    def __init__(self, doc):
+        self.doc = doc
+        self.font_sizes = defaultdict(int)  # 统计字体大小出现频率
+        self.common_font_size = None  # 正文字体大小
+        self.heading_sizes = []  # 标题字体大小列表
+        
+    def analyze(self):
+        """分析整个文档的结构"""
+        # 统计字体大小
+        for page in self.doc:
+            blocks = page.get_text("dict")["blocks"]
+            for block in blocks:
+                if block.get("type") == 0:  # 文本块
+                    for line in block.get("lines", []):
+                        for span in line.get("spans", []):
+                            size = round(span.get("size", 12))
+                            self.font_sizes[size] += len(span.get("text", ""))
+        
+        # 确定正文字体大小(出现最频繁的)
+        if self.font_sizes:
+            self.common_font_size = max(self.font_sizes.items(), key=lambda x: x[1])[0]
+            
+            # 确定标题字体大小(大于正文的)
+            self.heading_sizes = sorted([s for s in self.font_sizes.keys() 
+                                        if s > self.common_font_size], reverse=True)
+        
+        return self
+    
+    def get_heading_level(self, font_size):
+        """根据字体大小确定标题级别"""
+        font_size = round(font_size)
+        
+        if font_size <= self.common_font_size:
+            return 0  # 不是标题
+        
+        # 根据字体大小确定标题级别 (H2-H6)
+        for i, size in enumerate(self.heading_sizes):
+            if font_size >= size:
+                return min(i + 2, 6)  # H2 到 H6
+        
+        return 2  # 默认 H2
+
+
+class MarkdownConverter:
+    """Markdown 转换器:将 PDF 内容转换为结构化的 Markdown"""
+    
+    def __init__(self, doc, generate_toc=False):
+        self.doc = doc
+        self.generate_toc = generate_toc
+        self.analyzer = PDFStructureAnalyzer(doc).analyze()
+        self.toc = []  # 目录
+        self.content = []  # 内容
+        
+    def convert(self):
+        """执行转换"""
+        # 添加文档标题
+        title = self.doc.metadata.get('title', '') or Path(self.doc.name).stem
+        self.content.append(f"# {title}\n")
+        
+        # 处理每一页
+        for page_num in range(len(self.doc)):
+            self._process_page(page_num)
+        
+        # 生成最终 Markdown
+        markdown_parts = []
+        
+        # 添加目录(如果需要)
+        if self.generate_toc and self.toc:
+            markdown_parts.append("## 目录\n")
+            for level, text in self.toc:
+                indent = "  " * (level - 2)
+                anchor = self._text_to_anchor(text)
+                markdown_parts.append(f"{indent}- [{text}](#{anchor})")
+            markdown_parts.append("\n---\n")
+        
+        # 添加内容
+        markdown_parts.extend(self.content)
+        
+        # 清理多余空行
+        markdown_text = '\n'.join(markdown_parts)
+        markdown_text = re.sub(r'\n{3,}', '\n\n', markdown_text)
+        
+        return markdown_text
+    
+    def _process_page(self, page_num):
+        """处理单个页面"""
+        page = self.doc[page_num]
+        blocks = page.get_text("dict")["blocks"]
+        
+        # 添加页面注释(可选)
+        # self.content.append(f"\n<!-- Page {page_num + 1} -->\n")
+        
+        for block in blocks:
+            if block.get("type") == 0:  # 文本块
+                self._process_text_block(block)
+    
+    def _process_text_block(self, block):
+        """处理文本块"""
+        lines = block.get("lines", [])
+        if not lines:
+            return
+        
+        # 获取块的主要字体大小
+        all_spans = []
+        for line in lines:
+            all_spans.extend(line.get("spans", []))
+        
+        if not all_spans:
+            return
+        
+        # 按字符数统计主要字体大小
+        size_chars = defaultdict(int)
+        for span in all_spans:
+            size = round(span.get("size", 12))
+            size_chars[size] += len(span.get("text", ""))
+        
+        main_size = max(size_chars.items(), key=lambda x: x[1])[0] if size_chars else 12
+        
+        # 提取文本
+        text = ""
+        for line in lines:
+            line_text = ""
+            for span in line.get("spans", []):
+                line_text += span.get("text", "")
+            text += line_text + "\n"
+        
+        text = text.strip()
+        if not text:
+            return
+        
+        # 判断文本类型并格式化
+        self._format_text(text, main_size)
+    
+    def _format_text(self, text, font_size):
+        """格式化文本为 Markdown"""
+        # 检查是否是标题
+        heading_level = self.analyzer.get_heading_level(font_size)
+        
+        if heading_level > 0:
+            # 是标题
+            self._add_heading(text, heading_level)
+        elif self._is_at_command(text):
+            # AT 命令
+            self._add_code_block(text, "")
+        elif self._is_table(text):
+            # 表格
+            self._add_table(text)
+        elif self._is_list(text):
+            # 列表
+            self._add_list(text)
+        else:
+            # 普通段落
+            self._add_paragraph(text)
+    
+    def _add_heading(self, text, level):
+        """添加标题"""
+        # 清理标题文本
+        text = re.sub(r'\s+', ' ', text).strip()
+        
+        # 记录到目录
+        if self.generate_toc:
+            self.toc.append((level, text))
+        
+        # 添加标题
+        self.content.append(f"\n{'#' * level} {text}\n")
+    
+    def _add_paragraph(self, text):
+        """添加段落"""
+        # 清理文本
+        text = self._clean_text(text)
+        if text:
+            self.content.append(f"{text}\n")
+    
+    def _add_code_block(self, text, lang=""):
+        """添加代码块"""
+        self.content.append(f"```{lang}\n{text}\n```\n")
+    
+    def _add_table(self, text):
+        """添加表格"""
+        # 简单的表格处理:将制表符转换为 Markdown 表格
+        lines = text.split('\n')
+        table_lines = []
+        
+        for i, line in enumerate(lines):
+            if '\t' in line or '|' in line:
+                # 处理表格行
+                if '\t' in line:
+                    cells = [cell.strip() for cell in line.split('\t') if cell.strip()]
+                    line = '| ' + ' | '.join(cells) + ' |'
+                table_lines.append(line)
+                
+                # 添加表头分隔符(第一行后)
+                if i == 0 and '\t' in lines[0]:
+                    num_cols = len(cells)
+                    separator = '| ' + ' | '.join(['---'] * num_cols) + ' |'
+                    table_lines.append(separator)
+        
+        if table_lines:
+            self.content.append('\n'.join(table_lines) + '\n')
+        else:
+            self._add_paragraph(text)
+    
+    def _add_list(self, text):
+        """添加列表"""
+        lines = text.split('\n')
+        for line in lines:
+            line = line.strip()
+            if line:
+                # 检查是否已经是列表格式
+                if re.match(r'^[\d]+\.?\s', line) or re.match(r'^[•\-\*]\s', line):
+                    # 规范化列表格式
+                    if re.match(r'^[\d]+\.?\s', line):
+                        # 数字列表
+                        self.content.append(line)
+                    else:
+                        # 无序列表
+                        line = re.sub(r'^[•\-\*]\s', '- ', line)
+                        self.content.append(line)
+                else:
+                    self.content.append(f"- {line}")
+        self.content.append("")
+    
+    def _is_at_command(self, text):
+        """判断是否是 AT 命令"""
+        lines = text.split('\n')
+        at_lines = sum(1 for line in lines if line.strip().startswith(('AT+', 'AT^', 'AT*')))
+        return at_lines > 0 and at_lines / len(lines) > 0.5
+    
+    def _is_table(self, text):
+        """判断是否是表格"""
+        lines = text.split('\n')
+        tab_lines = sum(1 for line in lines if '\t' in line or line.count('|') > 2)
+        return tab_lines > 0 and tab_lines / len(lines) > 0.5
+    
+    def _is_list(self, text):
+        """判断是否是列表"""
+        lines = [line.strip() for line in text.split('\n') if line.strip()]
+        if len(lines) < 2:
+            return False
+        
+        list_lines = sum(1 for line in lines 
+                        if re.match(r'^[\d]+\.?\s', line) or re.match(r'^[•\-\*]\s', line))
+        return list_lines / len(lines) > 0.6
+    
+    def _clean_text(self, text):
+        """清理文本"""
+        # 移除软连字符
+        text = text.replace('\u00ad', '')
+        
+        # 修复单词断行
+        text = re.sub(r'(\w)-\s*\n\s*(\w)', r'\1\2', text)
+        
+        # 规范化空白字符
+        text = re.sub(r'[ \t]+', ' ', text)
+        text = re.sub(r'\n{2,}', '\n\n', text)
+        
+        return text.strip()
+    
+    def _text_to_anchor(self, text):
+        """将文本转换为 Markdown 锚点"""
+        # 简化为小写,移除特殊字符
+        anchor = text.lower()
+        anchor = re.sub(r'[^\w\s\u4e00-\u9fff-]', '', anchor)
+        anchor = re.sub(r'\s+', '-', anchor)
+        return anchor
+
+
+class RawMarkdownConverter:
+    """原始 Markdown 转换器:忠实还原 PDF 原始格式"""
+    
+    def __init__(self, doc, preserve_pages=True):
+        self.doc = doc
+        self.preserve_pages = preserve_pages
+        self.content = []
+        
+    def convert(self):
+        """执行转换"""
+        # 添加文档标题
+        title = self.doc.metadata.get('title', '') or Path(self.doc.name).stem
+        self.content.append(f"# {title}\n")
+        
+        # 处理每一页
+        total_pages = len(self.doc)
+        for page_num in range(total_pages):
+            self._process_page(page_num)
+            
+            # 添加页面分隔
+            if self.preserve_pages and page_num < total_pages - 1:
+                self.content.append(f"\n---\n")
+                self.content.append(f"<!-- Page {page_num + 2} -->\n")
+        
+        # 生成最终 Markdown
+        markdown_text = '\n'.join(self.content)
+        
+        # 基本清理:移除3个以上的连续空行
+        markdown_text = re.sub(r'\n{4,}', '\n\n\n', markdown_text)
+        
+        return markdown_text
+    
+    def _process_page(self, page_num):
+        """处理单个页面 - 保留原始格式"""
+        page = self.doc[page_num]
+        
+        # 添加页面标记
+        if self.preserve_pages:
+            self.content.append(f"\n<!-- Page {page_num + 1} -->\n")
+        
+        # 获取原始文本块,按位置排序
+        blocks = page.get_text("dict")["blocks"]
+        
+        # 按垂直位置排序(从上到下)
+        text_blocks = [b for b in blocks if b.get("type") == 0]
+        text_blocks.sort(key=lambda b: (b["bbox"][1], b["bbox"][0]))
+        
+        for block in text_blocks:
+            text = self._extract_block_text(block)
+            if text.strip():
+                # 检测是否是 AT 命令行
+                if self._is_at_command_line(text):
+                    self.content.append(f"```")
+                    self.content.append(text.strip())
+                    self.content.append(f"```\n")
+                else:
+                    # 保留原始文本,只做基本清理
+                    self.content.append(text)
+                    self.content.append("")  # 块之间空行
+    
+    def _extract_block_text(self, block):
+        """提取文本块内容,保留原始格式"""
+        lines = block.get("lines", [])
+        text_lines = []
+        
+        for line in lines:
+            line_text = ""
+            spans = line.get("spans", [])
+            
+            # 按水平位置排序
+            spans.sort(key=lambda s: s["bbox"][0])
+            
+            for span in spans:
+                span_text = span.get("text", "")
+                line_text += span_text
+            
+            if line_text.strip():
+                text_lines.append(line_text)
+        
+        # 保留行内换行
+        return '\n'.join(text_lines)
+    
+    def _is_at_command_line(self, text):
+        """判断是否是 AT 命令(单行)"""
+        text = text.strip()
+        # 单行且以 AT 开头
+        return text.startswith(('AT+', 'AT^', 'AT*', 'AT')) and '\n' not in text
+
+
+def pdf_to_markdown(pdf_path, output_path=None, generate_toc=False, raw_mode=False, preserve_pages=True):
+    """
+    将 PDF 文件转换为 Markdown
+    
+    Args:
+        pdf_path: PDF 文件路径
+        output_path: 输出 Markdown 文件路径(可选)
+        generate_toc: 是否生成目录(默认 False,仅结构化模式)
+        raw_mode: 是否使用原始模式(默认 False,使用结构化模式)
+        preserve_pages: 是否保留页面标记(默认 True,仅原始模式)
+        
+    Returns:
+        转换是否成功
+    """
+    try:
+        mode_str = "原始格式" if raw_mode else "结构化"
+        print(f"正在处理: {pdf_path} [{mode_str}模式]")
+        
+        # 打开 PDF
+        doc = fitz.open(pdf_path)
+        total_pages = len(doc)
+        
+        print(f"  文档信息: {total_pages} 页")
+        
+        # 根据模式选择转换器
+        if raw_mode:
+            # 原始格式转换
+            converter = RawMarkdownConverter(doc, preserve_pages=preserve_pages)
+            markdown_text = converter.convert()
+            heading_count = 0
+        else:
+            # 结构化转换
+            converter = MarkdownConverter(doc, generate_toc=generate_toc)
+            markdown_text = converter.convert()
+            heading_count = len(converter.toc) if generate_toc else markdown_text.count('\n## ')
+        
+        doc.close()
+        
+        print(f"  转换完成")
+        
+        # 确定输出路径
+        if output_path is None:
+            output_path = pdf_path.rsplit('.', 1)[0] + '.md'
+        
+        # 保存 Markdown 文件
+        with open(output_path, 'w', encoding='utf-8') as f:
+            f.write(markdown_text)
+        
+        # 统计信息
+        file_size_kb = os.path.getsize(output_path) / 1024
+        line_count = markdown_text.count('\n')
+        
+        print(f"✓ 转换成功: {output_path}")
+        print(f"  文件大小: {file_size_kb:.2f} KB")
+        print(f"  行数: {line_count}")
+        if not raw_mode:
+            print(f"  标题数: {heading_count}")
+        
+        return True
+        
+    except Exception as e:
+        print(f"✗ 转换失败: {e}")
+        import traceback
+        traceback.print_exc()
+        return False
+
+
+def process_directory(directory, generate_toc=False, raw_mode=False, preserve_pages=True):
+    """
+    处理目录下的所有 PDF 文件
+    
+    Args:
+        directory: 目录路径
+        generate_toc: 是否生成目录
+        raw_mode: 是否使用原始模式
+        preserve_pages: 是否保留页面标记
+    """
+    pdf_files = list(Path(directory).glob('*.pdf'))
+    
+    if not pdf_files:
+        print(f"在 {directory} 目录下没有找到 PDF 文件")
+        return
+    
+    print(f"找到 {len(pdf_files)} 个 PDF 文件\n")
+    
+    success_count = 0
+    for pdf_file in pdf_files:
+        if pdf_to_markdown(str(pdf_file), generate_toc=generate_toc, 
+                          raw_mode=raw_mode, preserve_pages=preserve_pages):
+            success_count += 1
+        print()
+    
+    print(f"\n总结: {success_count}/{len(pdf_files)} 个文件转换成功")
+
+
+def main():
+    """主函数"""
+    parser = argparse.ArgumentParser(
+        description='将 PDF 文件转换为 Markdown 格式',
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+示例:
+  %(prog)s                           # 转换当前目录所有 PDF(结构化)
+  %(prog)s input.pdf                 # 转换指定 PDF 文件(结构化)
+  %(prog)s --raw input.pdf           # 原始格式转换
+  %(prog)s --toc input.pdf           # 转换并生成目录(结构化)
+  %(prog)s -o output.md input.pdf    # 指定输出文件名
+  %(prog)s /path/to/docs/            # 转换指定目录下的所有 PDF
+
+模式说明:
+  结构化模式(默认):智能识别文档结构,优化层级和格式,适合大模型理解
+  原始模式(--raw):忠实还原 PDF 原始格式和布局,保留原始文本顺序
+
+注意:
+  - 请确保输入的 PDF 文件已经去除了水印
+  - 使用 pdf_watermark_remover.py 可以预先清理 PDF 水印
+        """
+    )
+    
+    parser.add_argument(
+        'input',
+        nargs='?',
+        help='输入 PDF 文件或目录路径(默认为脚本所在目录)'
+    )
+    
+    parser.add_argument(
+        '-o', '--output',
+        help='输出 Markdown 文件路径(仅当输入为单个文件时有效)'
+    )
+    
+    parser.add_argument(
+        '--toc',
+        action='store_true',
+        help='生成文档目录(仅结构化模式有效)'
+    )
+    
+    parser.add_argument(
+        '--raw',
+        action='store_true',
+        help='使用原始格式模式,忠实还原 PDF 布局'
+    )
+    
+    parser.add_argument(
+        '--no-pages',
+        action='store_true',
+        help='不添加页面标记(仅原始模式有效)'
+    )
+    
+    args = parser.parse_args()
+    
+    # 确定输入路径
+    if args.input:
+        input_path = args.input
+    else:
+        # 默认为脚本所在目录
+        input_path = os.path.dirname(os.path.abspath(__file__))
+    
+    # 检查路径是否存在
+    if not os.path.exists(input_path):
+        print(f"错误: 路径 '{input_path}' 不存在")
+        sys.exit(1)
+    
+    # 确定模式和参数
+    mode_name = "原始格式模式" if args.raw else "结构化模式"
+    preserve_pages = not args.no_pages
+    
+    # 打印工具信息
+    print("=" * 70)
+    print(f"PDF 转 Markdown 工具 - {mode_name}".center(70))
+    print("=" * 70)
+    
+    # 判断是文件还是目录
+    if os.path.isfile(input_path):
+        # 处理单个文件
+        if not input_path.lower().endswith('.pdf'):
+            print(f"错误: '{input_path}' 不是 PDF 文件")
+            sys.exit(1)
+        
+        print(f"输入文件: {input_path}")
+        if args.output:
+            print(f"输出文件: {args.output}")
+        print(f"转换模式: {mode_name}")
+        if args.toc and not args.raw:
+            print("生成目录: 是")
+        if args.no_pages and args.raw:
+            print("页面标记: 否")
+        print("=" * 70)
+        print()
+        
+        success = pdf_to_markdown(input_path, args.output, args.toc, 
+                                 args.raw, preserve_pages)
+        sys.exit(0 if success else 1)
+    
+    elif os.path.isdir(input_path):
+        # 处理目录
+        print(f"处理目录: {input_path}")
+        print(f"转换模式: {mode_name}")
+        if args.toc and not args.raw:
+            print("生成目录: 是")
+        if args.no_pages and args.raw:
+            print("页面标记: 否")
+        if args.output:
+            print("警告: 处理目录时 -o/--output 参数将被忽略")
+        print("=" * 70)
+        print()
+        
+        process_directory(input_path, args.toc, args.raw, preserve_pages)
+        sys.exit(0)
+    
+    else:
+        print(f"错误: '{input_path}' 既不是文件也不是目录")
+        sys.exit(1)
+
+
+if __name__ == '__main__':
+    main()
+
diff --git a/code/sdk/zhihui/ppp/doc/pdf_watermark_remover.py b/code/sdk/zhihui/ppp/doc/pdf_watermark_remover.py
new file mode 100644 (file)
index 0000000..35cb884
--- /dev/null
@@ -0,0 +1,451 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+PDF 水印去除工具 - 真正工作的版本
+
+核心理念:
+- 旋转的文本 = 水印(默认:旋转>0.5度即判定为水印)
+- 正常文本都是水平的(角度=0)
+
+核心实现:
+1. 使用PyMuPDF识别旋转文本(水印)
+2. 使用pikepdf解析和修改PDF内容流
+3. 检测Tm和cm矩阵的旋转角度
+4. 删除包含旋转矩阵的整个BT...ET文本块
+5. 重建内容流
+
+依赖:
+    pip3 install pikepdf PyMuPDF
+
+使用方法:
+    # 分析水印
+    python3 pdf_watermark_remover_working.py --mode analyze --input file.pdf
+    
+    # 删除水印(默认:旋转>0.5度)
+    python3 pdf_watermark_remover_working.py --mode remove --input file.pdf --output clean.pdf
+    
+    # 删除所有旋转文本(角度阈值=0)
+    python3 pdf_watermark_remover_working.py --mode remove --input file.pdf --output clean.pdf -a 0
+"""
+
+import os
+import sys
+import argparse
+import math
+import re
+from collections import defaultdict
+
+try:
+    import pikepdf
+except ImportError:
+    print("错误: 需要安装 pikepdf 库")
+    print("请运行: pip3 install pikepdf")
+    sys.exit(1)
+
+try:
+    import fitz  # PyMuPDF
+except ImportError:
+    print("错误: 需要安装 PyMuPDF 库")
+    print("请运行: pip3 install PyMuPDF")
+    sys.exit(1)
+
+
+class PDFContentStreamParser:
+    """简化的PDF内容流解析器"""
+    
+    def __init__(self, angle_threshold=0.5):
+        self.angle_threshold = angle_threshold  # 默认0.5度,任何旋转都会被检测
+    
+    def parse_content_stream(self, stream_data):
+        """
+        解析PDF内容流,识别并删除旋转文本
+        
+        Returns:
+            修改后的内容流数据
+        """
+        try:
+            content = stream_data.decode('latin-1')
+        except:
+            # 如果解码失败,尝试其他编码
+            try:
+                content = stream_data.decode('utf-8')
+            except:
+                return stream_data  # 无法解码,返回原始数据
+        
+        # 按行分割
+        lines = content.split('\n')
+        
+        # 过滤后的行
+        filtered_lines = []
+        
+        # 状态追踪
+        in_text_block = False
+        skip_until_et = False
+        bt_start_line = -1
+        has_rotated_cm = False  # 追踪是否有旋转的cm
+        
+        i = 0
+        while i < len(lines):
+            line = lines[i].strip()
+            
+            # 检查cm(坐标变换矩阵)- Draft使用的是cm而不是Tm!
+            if ' cm' in line and not in_text_block:
+                parts = line.split()
+                try:
+                    # cm也是6个参数:a b c d e f cm
+                    cm_idx = -1
+                    for idx, part in enumerate(parts):
+                        if part == 'cm':
+                            cm_idx = idx
+                            break
+                    
+                    if cm_idx >= 6:
+                        a = float(parts[cm_idx-6])
+                        b = float(parts[cm_idx-5])
+                        c = float(parts[cm_idx-4])
+                        d = float(parts[cm_idx-3])
+                        
+                        angle = math.degrees(math.atan2(b, a))
+                        
+                        if abs(angle) > self.angle_threshold:
+                            # 有旋转的cm,标记状态
+                            has_rotated_cm = True
+                            # 不添加这行
+                            i += 1
+                            continue
+                except (ValueError, IndexError):
+                    pass
+            
+            # 检查BT(Begin Text)
+            if line.endswith('BT') or line == 'BT':
+                in_text_block = True
+                # 如果之前有旋转的cm,标记跳过整个文本块
+                if has_rotated_cm:
+                    skip_until_et = True
+                else:
+                    skip_until_et = False
+                bt_start_line = len(filtered_lines)
+                
+                if not skip_until_et:
+                    filtered_lines.append(lines[i])
+                i += 1
+                continue
+            
+            # 检查ET(End Text)
+            if line.endswith('ET') or line == 'ET':
+                if skip_until_et:
+                    # 删除从BT到ET的所有行
+                    filtered_lines = filtered_lines[:bt_start_line]
+                else:
+                    filtered_lines.append(lines[i])
+                
+                in_text_block = False
+                skip_until_et = False
+                bt_start_line = -1
+                has_rotated_cm = False  # 重置cm状态
+                i += 1
+                continue
+            
+            # 检查Q(恢复图形状态)- 重置cm状态
+            if line == 'Q':
+                has_rotated_cm = False
+            
+            # 在文本块内,检查Tm操作符
+            if in_text_block and 'Tm' in line:
+                # 尝试解析Tm矩阵:a b c d e f Tm
+                # 可能在一行中,也可能与其他操作符混合
+                tm_match = line
+                
+                # 查找最后的6个数字 + Tm
+                parts = tm_match.split()
+                
+                # 尝试找到Tm操作符的位置
+                try:
+                    tm_idx = -1
+                    for idx, part in enumerate(parts):
+                        if part == 'Tm' or part.endswith('Tm'):
+                            tm_idx = idx
+                            break
+                    
+                    if tm_idx >= 6:
+                        # 获取Tm前的6个参数
+                        a = float(parts[tm_idx-6])
+                        b = float(parts[tm_idx-5])
+                        c = float(parts[tm_idx-4])
+                        d = float(parts[tm_idx-3])
+                        e = float(parts[tm_idx-2])
+                        f = float(parts[tm_idx-1])
+                        
+                        # 计算旋转角度
+                        angle = math.degrees(math.atan2(b, a))
+                        
+                        if abs(angle) > self.angle_threshold:
+                            # 这是旋转文本(水印),标记跳过整个BT...ET块
+                            skip_until_et = True
+                            i += 1
+                            continue
+                except (ValueError, IndexError):
+                    pass  # 解析失败,保留这行
+            
+            # 如果不在跳过状态,保留这行
+            if not skip_until_et:
+                filtered_lines.append(lines[i])
+            
+            i += 1
+        
+        # 重建内容流
+        modified_content = '\n'.join(filtered_lines)
+        
+        try:
+            return modified_content.encode('latin-1')
+        except:
+            return modified_content.encode('utf-8')
+
+
+class WorkingWatermarkRemover:
+    """真正工作的水印去除器"""
+    
+    def __init__(self, angle_threshold=0.5):
+        self.angle_threshold = angle_threshold  # 默认0.5度,任何旋转都会被检测
+        self.parser = PDFContentStreamParser(angle_threshold)
+        
+    def _is_rotated_text(self, line_dir):
+        """判断文本是否旋转"""
+        angle = math.degrees(math.atan2(line_dir[1], line_dir[0]))
+        is_rotated = abs(angle) > self.angle_threshold
+        return is_rotated, angle
+    
+    def analyze(self, pdf_path):
+        """分析PDF中的水印"""
+        print(f"\n正在分析 PDF: {pdf_path}")
+        print("=" * 80)
+        
+        doc = fitz.open(pdf_path)
+        watermark_info = {}
+        total_rotated = 0
+        
+        for page_num in range(len(doc)):
+            print(f"  扫描第 {page_num + 1}/{len(doc)} 页...", end='\r')
+            page = doc[page_num]
+            blocks = page.get_text("dict")["blocks"]
+            
+            page_watermarks = []
+            
+            for block in blocks:
+                if "lines" not in block:
+                    continue
+                
+                for line in block["lines"]:
+                    line_dir = line.get("dir", (1, 0))
+                    is_rotated, angle = self._is_rotated_text(line_dir)
+                    
+                    if is_rotated:
+                        for span in line["spans"]:
+                            text = span["text"].strip()
+                            if text:
+                                page_watermarks.append({
+                                    'text': text,
+                                    'font': span["font"],
+                                    'angle': angle
+                                })
+                                total_rotated += 1
+            
+            if page_watermarks:
+                watermark_info[page_num] = page_watermarks
+        
+        doc.close()
+        
+        print(f"\n\n分析完成!")
+        print(f"\n发现的水印:")
+        print(f"  总数: {total_rotated}")
+        print(f"  分布页数: {len(watermark_info)}")
+        
+        # 显示示例
+        if watermark_info:
+            print(f"\n水印示例(前10个):")
+            sample_count = 0
+            for page_num, watermarks in list(watermark_info.items())[:5]:
+                for wm in watermarks[:3]:
+                    if sample_count >= 10:
+                        break
+                    sample_count += 1
+                    print(f"  {sample_count}. 第{page_num + 1}页: \"{wm['text'][:40]}\"")
+                    print(f"     字体: {wm['font']}, 角度: {wm['angle']:+.1f}°")
+        
+        return watermark_info
+    
+    def remove(self, input_pdf, output_pdf):
+        """删除水印(真正的实现)"""
+        print(f"\n正在删除水印: {input_pdf}")
+        print(f"输出文件: {output_pdf}")
+        print(f"方法: 内容流操作(删除旋转文本对象)")
+        print("=" * 80)
+        
+        # 先分析
+        watermark_info = self.analyze(input_pdf)
+        
+        if not watermark_info:
+            print("\n未发现水印,直接复制文件")
+            import shutil
+            shutil.copy2(input_pdf, output_pdf)
+            return 0
+        
+        print(f"\n开始处理内容流...")
+        print("-" * 80)
+        
+        # 打开PDF
+        pdf = pikepdf.open(input_pdf)
+        
+        modified_pages = 0
+        processed_blocks = 0
+        
+        for page_num, page in enumerate(pdf.pages):
+            if page_num not in watermark_info:
+                continue
+            
+            try:
+                # 获取页面内容流
+                if '/Contents' not in page:
+                    continue
+                
+                contents = page.Contents
+                
+                # 处理单个或多个内容流
+                if isinstance(contents, pikepdf.Array):
+                    # 多个内容流对象
+                    for i, content_obj in enumerate(contents):
+                        if hasattr(content_obj, 'read_bytes'):
+                            stream_data = content_obj.read_bytes()
+                            
+                            # 修改内容流
+                            modified_data = self.parser.parse_content_stream(stream_data)
+                            
+                            # 更新内容流
+                            content_obj.write(modified_data)
+                            processed_blocks += 1
+                else:
+                    # 单个内容流对象
+                    if hasattr(contents, 'read_bytes'):
+                        stream_data = contents.read_bytes()
+                        
+                        # 修改内容流
+                        modified_data = self.parser.parse_content_stream(stream_data)
+                        
+                        # 更新内容流
+                        contents.write(modified_data)
+                        processed_blocks += 1
+                
+                modified_pages += 1
+                print(f"  ✓ 第{page_num + 1}页: 处理了 {len(watermark_info[page_num])} 个水印")
+                
+            except Exception as e:
+                print(f"  ✗ 第{page_num + 1}页处理失败: {e}")
+                continue
+        
+        print(f"\n保存修改后的PDF...")
+        print("-" * 80)
+        
+        # 保存
+        pdf.save(output_pdf)
+        pdf.close()
+        
+        print(f"\n完成!")
+        print(f"  修改页数: {modified_pages}")
+        print(f"  处理的内容流块: {processed_blocks}")
+        print(f"  标识的水印数: {sum(len(wms) for wms in watermark_info.values())}")
+        
+        # 文件大小对比
+        original_size = os.path.getsize(input_pdf)
+        final_size = os.path.getsize(output_pdf)
+        print(f"\n文件大小:")
+        print(f"  原始: {original_size / 1024 / 1024:.2f} MB")
+        print(f"  清理后: {final_size / 1024 / 1024:.2f} MB")
+        
+        return modified_pages
+
+
+def main():
+    """主函数"""
+    parser = argparse.ArgumentParser(
+        description='PDF 水印去除工具 - 真正工作的版本',
+        formatter_class=argparse.RawDescriptionHelpFormatter,
+        epilog="""
+特点:
+  - 真正从内容流中删除旋转文本对象
+  - 不是覆盖,是删除操作符
+  - 基于Tm和cm矩阵识别旋转文本
+  - 任何旋转的文本都会被识别为水印
+  - 删除整个BT...ET文本块
+
+工作原理:
+  1. 解析PDF内容流
+  2. 识别包含旋转矩阵的Tm或cm操作符
+  3. 删除从BT到ET的整个文本块
+  4. 重建内容流
+
+示例:
+  # 分析
+  %(prog)s --mode analyze --input file.pdf
+  
+  # 删除(默认:旋转>0.5度即删除)
+  %(prog)s --mode remove --input file.pdf --output clean.pdf
+  
+  # 只删除旋转超过10度的(更保守)
+  %(prog)s --mode remove --input file.pdf --output clean.pdf --angle-threshold 10.0
+  
+  # 删除所有旋转文本(设为0)
+  %(prog)s --mode remove --input file.pdf --output clean.pdf --angle-threshold 0
+        """
+    )
+    
+    parser.add_argument('--mode', '-m',
+                        choices=['analyze', 'remove'],
+                        required=True,
+                        help='操作模式')
+    
+    parser.add_argument('--input', '-i',
+                        required=True,
+                        help='输入 PDF 文件路径')
+    
+    parser.add_argument('--output', '-o',
+                        help='输出 PDF 文件路径(remove模式必需)')
+    
+    parser.add_argument('--angle-threshold', '-a',
+                        type=float,
+                        default=0.5,
+                        help='旋转角度阈值(度),默认0.5,任何旋转都会被检测为水印')
+    
+    args = parser.parse_args()
+    
+    if not os.path.exists(args.input):
+        print(f"错误: 文件不存在: {args.input}")
+        sys.exit(1)
+    
+    if args.mode == 'remove' and not args.output:
+        print("错误: remove 模式需要指定 --output 参数")
+        sys.exit(1)
+    
+    print("=" * 80)
+    print("PDF 水印去除工具 - 真正工作的版本")
+    print("=" * 80)
+    
+    remover = WorkingWatermarkRemover(angle_threshold=args.angle_threshold)
+    
+    if args.mode == 'analyze':
+        remover.analyze(args.input)
+        
+    elif args.mode == 'remove':
+        if os.path.exists(args.output):
+            response = input(f"\n输出文件已存在: {args.output}\n是否覆盖? [y/N]: ").strip().lower()
+            if response not in ['y', 'yes']:
+                print("操作已取消")
+                sys.exit(0)
+        
+        remover.remove(args.input, args.output)
+        print(f"\n✓ 完成! 输出文件: {args.output}")
+        print("\n💡 建议:打开PDF检查效果,确认正常内容是否完整")
+
+
+if __name__ == '__main__':
+    main()
+