v2-on-ui #4
@@ -240,8 +240,8 @@ def _prune_out_dir(out_dir, keep):
|
||||
return removed
|
||||
|
||||
|
||||
def _fetch_selftest_log(device_ip, out_dir, stamp):
|
||||
"""从设备 FTP 抓取 /log/selftest.log(lftpd :21 匿名,根目录 = 卷根)。
|
||||
def _fetch_selftest_log(device_ip, out_dir, stamp, ftp_user="admin", ftp_pass="admin"):
|
||||
"""从设备 FTP 抓取 /log/selftest.log(lftpd :21 需鉴权,根目录 = 卷根)。
|
||||
|
||||
成功返回本地路径字符串;失败(网络不可达 / FTP 错误 / 设备侧文件缺失)返回 ""
|
||||
(按 S7 语义视为无故障记录;此处打 WARN,调用方不判整体 FAIL)。
|
||||
@@ -256,7 +256,9 @@ def _fetch_selftest_log(device_ip, out_dir, stamp):
|
||||
ftp = ftplib.FTP()
|
||||
try:
|
||||
ftp.connect(device_ip, 21, timeout=10)
|
||||
ftp.login() # 匿名(lftpd 接受 USER anonymous / 空 PASS)
|
||||
# lftpd 按 /config.ini 的 [ftp] 校验账号(user / pwd_hash),出厂 admin/admin;
|
||||
# 设备没有匿名通道——匿名登录一律 530,所以这里必须带凭据。
|
||||
ftp.login(ftp_user, ftp_pass)
|
||||
except (OSError, ftplib.Error) as e:
|
||||
print("RUN_TEST_WARN: selftest fetch failed: {} "
|
||||
"(按 S7 语义视为无记录;设备网络/FTP 需另行排查)".format(e))
|
||||
@@ -315,7 +317,11 @@ def main(argv=None):
|
||||
ap.add_argument("--selftest-log", default=None,
|
||||
help="local copy of /log/selftest.log (skips device-side fetch when given)")
|
||||
ap.add_argument("--device-ip", default=None,
|
||||
help="device IP for fetching /log/selftest.log via FTP (anonymous)")
|
||||
help="device IP for fetching /log/selftest.log via FTP")
|
||||
ap.add_argument("--ftp-user", default=None,
|
||||
help="FTP user for the selftest fetch (config 'ftp_user'; default admin)")
|
||||
ap.add_argument("--ftp-pass", default=None,
|
||||
help="FTP password for the selftest fetch (config 'ftp_pass'; default admin)")
|
||||
ap.add_argument("--out-dir", default=None)
|
||||
ap.add_argument("--show-tail-lines", type=int, default=None)
|
||||
ap.add_argument("--fw-hex", default=None,
|
||||
@@ -388,6 +394,8 @@ def main(argv=None):
|
||||
"require_selftest_pass", default=False))
|
||||
args.selftest_log = str(_pick(args.selftest_log, "selftest_log", default="") or "")
|
||||
args.device_ip = str(_pick(args.device_ip, "device_ip", default="") or "")
|
||||
args.ftp_user = str(_pick(args.ftp_user, "ftp_user", default="admin") or "admin")
|
||||
args.ftp_pass = str(_pick(args.ftp_pass, "ftp_pass", default="admin") or "admin")
|
||||
args.out_dir = str(_pick(args.out_dir, "out_dir",
|
||||
default=str(Path(tempfile.gettempdir()) / "serial-boot-test")))
|
||||
args.show_tail_lines = int(_pick(args.show_tail_lines, "show_tail_lines", default=15))
|
||||
@@ -596,7 +604,8 @@ def main(argv=None):
|
||||
print("RUN_TEST_WARN: require_selftest_pass but no device_ip/selftest_log "
|
||||
"-> treated as no record (S7 PASS path)")
|
||||
if args.require_selftest_pass and not selftest_local and args.device_ip:
|
||||
selftest_local = _fetch_selftest_log(args.device_ip, out_dir, stamp)
|
||||
selftest_local = _fetch_selftest_log(args.device_ip, out_dir, stamp,
|
||||
args.ftp_user, args.ftp_pass)
|
||||
|
||||
# 6. 标记校验(含固件身份戳、启动锚点计数、可选的自检记录联合判定)
|
||||
# 启动锚点(config "boot_marker",如本仓库的 'reset cause:')未配置 =
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
## 1. 概述
|
||||
|
||||
- FTP Server(`lftpd`)在**默认固件构建中即常驻运行**:`Src/freertos.c` 的 `StartFtpTask`(默认 `__weak` 实现)直接调用 `lftpd_start("/", 21, &ftp)`,与测试套件选择无关。
|
||||
- 监听端口 **21**,根目录 `/`(即 NAND 上的 FatFS 卷),**匿名登录**(`USER anonymous` / `PASS` 任意)。
|
||||
- 监听端口 **21**,根目录 `/`(即 NAND 上的 FatFS 卷)。**需要账号鉴权,设备没有匿名通道**:账号取
|
||||
`/config.ini` 的 `[ftp]` 段(`user` / `pwd_hash` = SHA-256(user‖password) / `write`),出厂默认
|
||||
**admin/admin**(用默认口令时启动日志有告警);**连续 5 次失败锁定 60 秒**。完整口径见
|
||||
`../子系统设计/网络配置说明.md`。
|
||||
- 仅支持 **1 个并发客户端**(控制通道 Socket0 + PASV 数据通道动态 Socket2,backlog=1)。
|
||||
- 测试分两类:
|
||||
- **PC 端手测(推荐,覆盖真实 FTP 行为)**:用 `test/` 下 Python 脚本或 GUI 客户端。
|
||||
@@ -48,7 +51,8 @@
|
||||
|
||||
## 4. PC 端脚本手测(主要手段)
|
||||
|
||||
所有脚本支持 `--host`(默认 `192.168.1.100`)、`--port`(默认 21),在 `test/` 目录运行;`ftp_up_dl_test.py` / `ftp_speed_test.py` 内置**连接重试**(单客户端:旧会话未释放时自动等待重试,默认 6 次 × 3s)。
|
||||
所有脚本支持 `--host`(默认 `192.168.1.100`)、`--port`(默认 21)、`--user` / `--password`
|
||||
(默认出厂账号 `admin` / `admin`;设备口令改过就要显式传,**没有匿名通道**),在 `test/` 目录运行;`ftp_up_dl_test.py` / `ftp_speed_test.py` 内置**连接重试**(单客户端:旧会话未释放时自动等待重试,默认 6 次 × 3s)。
|
||||
推荐入口:`& '.venv\Scripts\python.exe' test/host/ftp_up_dl_test.py`(功能闭环:登录/TYPE I/PASV/STOR/LIST/RETR/比对/DELE/QUIT)与 `& '.venv\Scripts\python.exe' test/host/ftp_speed_test.py --size 128`(大文件 + 吞吐);后者同时是 **ftpTask 栈水位测量**的负载(见 `docs/内存管理/RAM使用分析与优化方案.md` §4.0.6)。
|
||||
|
||||
### 4.1 冒烟测试:`ftp_test.py`
|
||||
@@ -57,7 +61,7 @@
|
||||
- 命令:`python ./test/host/ftp_test.py --host 192.168.1.100`
|
||||
- 预期:
|
||||
- 连接成功并读到 `220` 欢迎;
|
||||
- `USER anonymous` / `PASS` 返回 `230`;
|
||||
- `USER admin` 返回 `331`(需要口令)、`PASS admin` 返回 `230`;
|
||||
- `PASV` 返回 `227` 并解析出数据端口(或回退 `EPSV`);
|
||||
- `LIST` 返回目录列表;
|
||||
- `QUIT` 正常关闭。
|
||||
@@ -88,7 +92,7 @@
|
||||
|
||||
## 5. GUI 客户端手测(可选)
|
||||
|
||||
- **FileZilla**:主机 `192.168.1.100`,端口 21,用户 `anonymous`,密码任意;可拖拽上传/下载、浏览目录。
|
||||
- **FileZilla**:主机 `192.168.1.100`,端口 21,用户 `admin`、密码 `admin`(**匿名会被 530 拒**);可拖拽上传/下载、浏览目录。
|
||||
- 要点:使用二进制模式传输;文件名遵守 8.3;**一次只连一个客户端**。
|
||||
|
||||
---
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"""
|
||||
FTP 速度测试 — 上传/下载大文件,测量传输速度
|
||||
用法:python ftp_speed_test.py --host 192.168.1.100 [--size 512] [--port 21]
|
||||
[--user admin] [--password admin]
|
||||
"""
|
||||
|
||||
import socket
|
||||
@@ -13,6 +14,10 @@ import os
|
||||
|
||||
HOST = "192.168.1.100"
|
||||
PORT = 21
|
||||
# 设备侧 FTP 鉴权走 /config.ini 的 [ftp](user / pwd_hash),出厂默认 admin/admin;
|
||||
# **没有匿名通道**(USER anonymous 一律回 530)。
|
||||
USER = "admin"
|
||||
PASSWORD = "admin"
|
||||
FILE_SIZE_KB = 512 # 默认 512KB 测试文件
|
||||
|
||||
def log(msg, *args):
|
||||
@@ -71,7 +76,7 @@ def parse_pasv(resp):
|
||||
return f"{a}.{b}.{c}.{d}", e * 256 + f
|
||||
return None, None
|
||||
|
||||
def test_speed(host, port, size_kb):
|
||||
def test_speed(host, port, size_kb, user, password):
|
||||
total_bytes = size_kb * 1024
|
||||
test_data = os.urandom(total_bytes)
|
||||
test_name = "SPEEDTST.DAT" # 8.3 format
|
||||
@@ -113,15 +118,20 @@ def test_speed(host, port, size_kb):
|
||||
check("Connect", False, str(err))
|
||||
return finish(None)
|
||||
recv_line(sock) # 220
|
||||
resp = send_cmd(sock, "USER anonymous")
|
||||
if not (resp and resp.startswith(b"230")):
|
||||
check("Login USER 230", False,
|
||||
# 需要口令的服务器在 USER 之后回 331,直接放行的才回 230——两种都要接受
|
||||
resp = send_cmd(sock, "USER " + user)
|
||||
if not (resp and (resp.startswith(b"230") or resp.startswith(b"331"))):
|
||||
check("Login USER (230/331)", False,
|
||||
resp.decode(errors='replace').rstrip() if resp else "no resp")
|
||||
print(" 提示:lftpd 按 /config.ini 的 [ftp] 段校验(user / pwd_hash),出厂默认 admin/admin;"
|
||||
"设备没有匿名通道。口令被改过就用 --user/--password 传;连续 5 次失败会锁定 60 秒。")
|
||||
return finish(sock)
|
||||
resp = send_cmd(sock, "PASS test@test.com")
|
||||
resp = send_cmd(sock, "PASS " + password)
|
||||
if not (resp and resp.startswith(b"230")):
|
||||
check("Login PASS 230", False,
|
||||
resp.decode(errors='replace').rstrip() if resp else "no resp")
|
||||
print(" 提示:lftpd 按 /config.ini 的 [ftp] 段校验(user / pwd_hash),出厂默认 admin/admin;"
|
||||
"设备没有匿名通道。口令被改过就用 --user/--password 传;连续 5 次失败会锁定 60 秒。")
|
||||
return finish(sock)
|
||||
check("Login (USER/PASS 230)", True)
|
||||
send_cmd(sock, "TYPE I")
|
||||
@@ -250,8 +260,11 @@ def main():
|
||||
parser.add_argument("--port", type=int, default=PORT)
|
||||
parser.add_argument("--size", type=int, default=FILE_SIZE_KB,
|
||||
help=f"File size in KB (default {FILE_SIZE_KB})")
|
||||
parser.add_argument("--user", default=USER, help=f"FTP user (default: {USER})")
|
||||
parser.add_argument("--password", default=PASSWORD,
|
||||
help="FTP password (default: the factory password)")
|
||||
args = parser.parse_args()
|
||||
success = test_speed(args.host, args.port, args.size)
|
||||
success = test_speed(args.host, args.port, args.size, args.user, args.password)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+15
-5
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
FTP Server 测试脚本 — 连接 STM32F4 上的 lftpd 服务器
|
||||
用法:python ftp_test.py [--host 192.168.1.100] [--port 21]
|
||||
用法:python ftp_test.py [--host 192.168.1.100] [--port 21] [--user admin] [--password admin]
|
||||
"""
|
||||
|
||||
import socket
|
||||
@@ -12,6 +12,10 @@ import argparse
|
||||
|
||||
HOST = "192.168.1.100"
|
||||
PORT = 21
|
||||
# 设备侧 FTP 鉴权走 /config.ini 的 [ftp](user / pwd_hash),出厂默认 admin/admin;
|
||||
# **没有匿名通道**(USER anonymous 一律回 530)。
|
||||
USER = "admin"
|
||||
PASSWORD = "admin"
|
||||
TIMEOUT = 10.0
|
||||
|
||||
def recv_line(sock):
|
||||
@@ -53,7 +57,7 @@ def parse_pasv(response):
|
||||
return ip, port
|
||||
return None, None
|
||||
|
||||
def test_ftp(host, port):
|
||||
def test_ftp(host, port, user, password):
|
||||
print(f"=== FTP Test: {host}:{port} ===\n")
|
||||
checks = [] # (label, ok, detail)
|
||||
|
||||
@@ -90,13 +94,13 @@ def test_ftp(host, port):
|
||||
|
||||
# 3. Login
|
||||
print("\n[3] Login...")
|
||||
resp = send_cmd(sock, "USER anonymous")
|
||||
resp = send_cmd(sock, "USER " + user)
|
||||
if not resp:
|
||||
sock.close()
|
||||
check("Login USER", False, "no response")
|
||||
return False
|
||||
|
||||
resp = send_cmd(sock, "PASS test@test.com")
|
||||
resp = send_cmd(sock, "PASS " + password)
|
||||
if not resp:
|
||||
sock.close()
|
||||
check("Login PASS", False, "no response")
|
||||
@@ -104,6 +108,9 @@ def test_ftp(host, port):
|
||||
login_ok = b"230" in resp
|
||||
check("Login (USER/PASS 230)", login_ok,
|
||||
resp.decode(errors='replace').rstrip())
|
||||
if not login_ok:
|
||||
print(" 提示:lftpd 按 /config.ini 的 [ftp] 段校验(user / pwd_hash),出厂默认 admin/admin;"
|
||||
"设备没有匿名通道。口令被改过就用 --user/--password 传;连续 5 次失败会锁定 60 秒。")
|
||||
|
||||
# 4. PWD
|
||||
print("\n[4] PWD...")
|
||||
@@ -199,9 +206,12 @@ def main():
|
||||
parser = argparse.ArgumentParser(description="FTP Server Test")
|
||||
parser.add_argument("--host", default=HOST, help=f"FTP server IP (default: {HOST})")
|
||||
parser.add_argument("--port", type=int, default=PORT, help=f"FTP server port (default: {PORT})")
|
||||
parser.add_argument("--user", default=USER, help=f"FTP user (default: {USER})")
|
||||
parser.add_argument("--password", default=PASSWORD,
|
||||
help="FTP password (default: the factory password)")
|
||||
args = parser.parse_args()
|
||||
|
||||
success = test_ftp(args.host, args.port)
|
||||
success = test_ftp(args.host, args.port, args.user, args.password)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
FTP 上传+下载测试脚本
|
||||
用法:python ftp_up_dl_test.py --host 192.168.1.100
|
||||
用法:python ftp_up_dl_test.py --host 192.168.1.100 [--user admin] [--password admin]
|
||||
"""
|
||||
|
||||
import socket
|
||||
@@ -14,6 +14,10 @@ import os
|
||||
|
||||
HOST = "192.168.1.100"
|
||||
PORT = 21
|
||||
# 设备侧 FTP 鉴权走 /config.ini 的 [ftp](user / pwd_hash = SHA-256(user‖password)),
|
||||
# 出厂默认 admin/admin;**没有匿名通道**(USER anonymous 一律回 530)。
|
||||
USER = "admin"
|
||||
PASSWORD = "admin"
|
||||
TIMEOUT = 15.0
|
||||
|
||||
TEST_DATA = b"Hello from Python FTP test!\r\nThis is line 2.\r\nLine 3: " + b"ABCDEFGH" * 50 + b"\r\n"
|
||||
@@ -83,8 +87,8 @@ def parse_pasv(resp):
|
||||
return f"{a}.{b}.{c}.{d}", e * 256 + f
|
||||
return None, None
|
||||
|
||||
def test_upload_download(host, port):
|
||||
print(f"=== FTP Upload/Download Test: {host}:{port} ===\n")
|
||||
def test_upload_download(host, port, user, password):
|
||||
print(f"=== FTP Upload/Download Test: {host}:{port} user={user} ===\n")
|
||||
checks = []
|
||||
|
||||
def check(label, ok, detail=""):
|
||||
@@ -120,11 +124,20 @@ def test_upload_download(host, port):
|
||||
return finish(None)
|
||||
recv_line(sock) # 220
|
||||
|
||||
if not send_cmd(sock, "USER anonymous", 230):
|
||||
check("Login USER 230", False)
|
||||
# 需要口令的服务器在 USER 之后回 331,直接放行的才回 230——两种都要接受
|
||||
resp = send_cmd(sock, "USER " + user)
|
||||
if not (resp and (resp.startswith(b"230") or resp.startswith(b"331"))):
|
||||
check("Login USER (230/331)", False,
|
||||
resp.decode(errors="replace").rstrip() if resp else "no resp")
|
||||
print(" 提示:lftpd 按 /config.ini 的 [ftp] 段校验(user / pwd_hash),出厂默认 admin/admin;"
|
||||
"设备没有匿名通道。口令被改过就用 --user/--password 传;连续 5 次失败会锁定 60 秒。")
|
||||
return finish(sock)
|
||||
if not send_cmd(sock, "PASS test@test.com", 230):
|
||||
check("Login PASS 230", False)
|
||||
resp = send_cmd(sock, "PASS " + password)
|
||||
if not (resp and resp.startswith(b"230")):
|
||||
check("Login PASS 230", False,
|
||||
resp.decode(errors="replace").rstrip() if resp else "no resp")
|
||||
print(" 提示:lftpd 按 /config.ini 的 [ftp] 段校验(user / pwd_hash),出厂默认 admin/admin;"
|
||||
"设备没有匿名通道。口令被改过就用 --user/--password 传;连续 5 次失败会锁定 60 秒。")
|
||||
return finish(sock)
|
||||
check("Login (USER/PASS 230)", True)
|
||||
|
||||
@@ -271,8 +284,11 @@ def main():
|
||||
parser = argparse.ArgumentParser(description="FTP Upload/Download Test")
|
||||
parser.add_argument("--host", default=HOST)
|
||||
parser.add_argument("--port", type=int, default=PORT)
|
||||
parser.add_argument("--user", default=USER, help=f"FTP user (default: {USER})")
|
||||
parser.add_argument("--password", default=PASSWORD,
|
||||
help="FTP password (default: the factory password)")
|
||||
args = parser.parse_args()
|
||||
success = test_upload_download(args.host, args.port)
|
||||
success = test_upload_download(args.host, args.port, args.user, args.password)
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user