1. 浮标卫星链路丢包事件记录与初步分类

任务对象与日志结构

浮标端通信日志通常由通信模块(如铱星9603、北斗短报文终端或VSAT调制解调器)按事件写入,每条记录至少包含时间戳、卫星过顶窗口标识、链路方向、包序号、包长、发送/确认状态和重传次数。要做丢包分类,第一步不是算丢包率,而是把原始日志整理成"一次过顶—一个方向—一个包"的规范事件表,再按过顶前、过顶中、过顶后三个时段切片。

本案例使用一份教学示例日志(假设数据,非实测),字段定义如下:

字段含义示例值
ts事件时间戳(UTC,秒级)2024-06-11T02:14:07Z
pass_id过顶窗口编号P021
dir链路方向:UL上行 / DL下行UL
seq包序号(每方向独立递增)1043
bytes包长(字节)96
event事件类型:TX_SENT / ACK_RX / TX_FAIL / NACK_RXTX_SENT
retry该包累计重传次数0

过顶窗口本身由星历预报给出,日志中另存一张窗口表:pass_id, aos_ts(升交点/可见开始), los_ts(可见结束)。时段划分规则为:过顶前 = 首次TX_SENT早于aos_ts的记录;过顶中 = aos_ts ≤ ts ≤ los_ts;过顶后 = 晚于los_ts的补发或确认。这条规则必须写死在脚本里,否则同一批日志不同人统计会得到不同结果。

从日志到丢包事件清单

判定一个包"丢失"的口径要明确:上行包发出后,在窗口结束前未收到对应ACK_RX,且重传次数已达上限(示例设为3),记为一次上行丢包;下行包以浮标侧是否收到该seq为准,缺失seq即记为下行丢包。下面脚本完成读取、窗口对齐、时段标注和分类统计,输出可直接用于后续分析的清单。

import csv
from datetime import datetime

def parse_ts(s):
    return datetime.strptime(s, "%Y-%m-%dT%H:%M:%SZ")

# 1. 读入过顶窗口表
windows = {}
with open("pass_windows.csv") as f:
    for r in csv.DictReader(f):
        windows[r["pass_id"]] = (parse_ts(r["aos_ts"]), parse_ts(r["los_ts"]))

# 2. 读入通信日志,按 (pass_id, dir, seq) 聚合
pkts = {}
with open("link_log.csv") as f:
    for r in csv.DictReader(f):
        key = (r["pass_id"], r["dir"], int(r["seq"]))
        p = pkts.setdefault(key, {"sent": None, "acked": False,
                                  "retry": 0, "bytes": int(r["bytes"])})
        ts = parse_ts(r["ts"])
        if r["event"] == "TX_SENT" and p["sent"] is None:
            p["sent"] = ts
        elif r["event"] == "ACK_RX":
            p["acked"] = True
        elif r["event"] in ("TX_FAIL", "NACK_RX"):
            p["retry"] = max(p["retry"], int(r["retry"]))

# 3. 时段标注 + 丢包判定
MAX_RETRY = 3
events = []
for (pid, d, seq), p in pkts.items():
    aos, los = windows[pid]
    t = p["sent"]
    if t is None:
        continue
    if t < aos:
        phase = "过顶前"
    elif t > los:
        phase = "过顶后"
    else:
        phase = "过顶中"
    lost = (not p["acked"]) and p["retry"] >= MAX_RETRY
    events.append({"pass_id": pid, "dir": d, "seq": seq,
                   "phase": phase, "bytes": p["bytes"],
                   "retry": p["retry"], "lost": lost})

# 4. 分类统计
from collections import Counter
stat = Counter((e["phase"], e["dir"]) for e in events if e["lost"])
total = Counter((e["phase"], e["dir"]) for e in events)
for k in sorted(total):
    print(k, "丢包", stat[k], "/ 总包", total[k])

# 5. 输出丢包事件清单
with open("loss_events.csv", "w", newline="") as f:
    w = csv.DictWriter(f, fieldnames=["pass_id","dir","seq","phase","bytes","retry"])
    w.writeheader()
    for e in events:
        if e["lost"]:
            w.writerow({k: e[k] for k in w.fieldnames})

运行后得到示例输出(基于假设日志):过顶前上行丢包2/18、过顶前下行丢包0/12;过顶中上行丢包7/140、过顶中下行丢包3/96;过顶后上行丢包4/22、过顶后下行丢包1/9。可以看到过顶后上行丢包比例明显偏高,这提示补发阶段的重传窗口可能不足,值得在后续案例中结合RSSI和SNR进一步定位。

时段边界建议留1~2秒保护带:卫星可见开始瞬间链路尚未稳定,若严格按aos_ts切分,容易把建链握手包误判为"过顶中丢包"。可在脚本中把aos_ts+2s作为过顶中起点,并在清单中保留原始时间戳以便复核。

分类结果的可视化判读

下图把一次完整过顶的时序结构与三个时段的丢包分布放在同一张图上:上半部分是过顶窗口与包流方向,下半部分是分类统计的对比条,便于快速看出哪一时段、哪一方向是主要丢包来源。

浮标卫星过顶窗口与丢包时段分类 时间 过顶前 建链准备 / 缓存排队 过顶中(aos_ts ~ los_ts) 主数据窗口,吞吐最高 过顶后 补发 / 确认收尾 aos_ts los_ts 上行 UL(浮标→卫星) 下行 DL(卫星→浮标) 丢包事件分类统计(示例数据) 上行丢包 下行丢包 包数 0 5 10 2 0 过顶前 7 3 过顶中 4 1 过顶后 过顶后上行丢包占比偏高

清单复核与常见口径问题

生成的 loss_events.csv 应逐条抽查:随机取3~5个丢包记录,回到原始日志确认该seq确实无ACK_RX、重传次数达到上限,且时段标注与窗口表一致。常见口径问题有三类:一是同一包在过顶前发出、过顶中重传成功,按"首次发送时间"归入过顶前,但实际是在过顶中恢复的,清单中应保留retry字段以便区分;二是下行缺失seq可能只是浮标未记录而非真丢包,需与地面站侧日志交叉核对;三是跨窗口的补发包若pass_id写错,会被错误归入相邻过顶,建议在脚本中加入pass_id与时间戳的一致性校验。

不要直接用"发送数减确认数"作为丢包数:重传成功、NACK后补发、窗口外确认都会让这个差值失真。必须按包序号逐条判定,并明确重传上限这一判定条件。