# -*- coding: utf-8 -*-
"""
test_SeqControl_simulation.py —— FB_SeqControl (v2, 2026-09-14) 1:1 仿真验证
逐行复刻 SCL 程序体的离散递推，验证条件触发 / 超时触发 / 顺序 / 跳转 / 步数上限 /
配置错误 / 中止 / 双触发优先级 / 条件与超时分离跳转(v2) 等场景。
时间统一用整数毫秒（对应 SCL 的 TIME = DINT ms）。

时刻约定（与真实接线语义一致）：条件信号在第 k 周期置位，FB 在同一周期就能看到它——
所以"第 k 周期给条件"意味着第 k 周期结束时就可能已发生转移，断言按下标 k 对齐。
步内计时从进入步的那个扫描周期起累计：第 0 周期进入步 1，则第 9 周期末 tElapsed = 1000 ms。
"""
import copy

# ---------------- 1:1 复刻 FB_SeqControl 程序体 ----------------
TIME_MAX_CLAMP = 24 * 24 * 3600 * 1000      # T#24d，SCL 里防溢出钳位值

def new_state():
    return dict(
        iStep=0, tElapsedSt=0, bStartOld=False,
        bDone=False, bCondEnd=False, bTimeEnd=False, bCfgFault=False,
        aStepActive=[False] * 101,     # 索引 1..100
        iCurStep=0, bRunning=False, tElapsed=0,
    )

def seq_fb(st, bStart, bAbort, uStartStep, uMaxSteps, aStepDone, aStepTime, aNextStep, aNextTime=None, tCycle=100):
    """tCycle 单位 ms。aStepDone/aStepTime/aNextStep/aNextTime 为 dict{步号: 值}，未填步按默认值。
    v2：条件结束查 aNextStep，超时结束查 aNextTime（编码相同：0 顺序 / n 跳转 / 255 结束）。"""
    if aNextTime is None:
        aNextTime = {}
    # ===== 1. 配置检查 =====
    bCfgFault = False
    if uMaxSteps < 1 or uMaxSteps > 100:
        bCfgFault = True
    if (not bCfgFault) and (uStartStep < 1 or uStartStep > uMaxSteps):
        bCfgFault = True
    if not bCfgFault:
        for i in range(1, uMaxSteps + 1):
            if aNextStep.get(i, 0) > uMaxSteps and aNextStep.get(i, 0) != 255:
                bCfgFault = True
            if aNextTime.get(i, 0) > uMaxSteps and aNextTime.get(i, 0) != 255:
                bCfgFault = True
    st['bCfgFault'] = bCfgFault

    # ===== 2. 中止 =====
    if bAbort:
        st['iStep'] = 0; st['tElapsedSt'] = 0
        st['bDone'] = False; st['bCondEnd'] = False; st['bTimeEnd'] = False

    # ===== 3. 启动（上升沿）=====
    if bStart and not st['bStartOld']:
        if (not bCfgFault) and st['iStep'] == 0:
            st['iStep'] = uStartStep; st['tElapsedSt'] = 0
            st['bDone'] = False; st['bCondEnd'] = False; st['bTimeEnd'] = False

    # ===== 4. 步激活输出 =====
    st['aStepActive'] = [False] * 101
    for i in range(1, 101):
        st['aStepActive'][i] = (i == st['iStep']) and (st['iStep'] > 0)

    # ===== 5. 运行：计时 + 步结束判定 + 转移 =====
    bTrans = False
    if st['iStep'] > 0:
        st['tElapsedSt'] += tCycle
        if st['tElapsedSt'] > TIME_MAX_CLAMP:
            st['tElapsedSt'] = TIME_MAX_CLAMP
        cur = st['iStep']
        if aStepDone.get(cur, False):
            bTrans = True; st['bCondEnd'] = True; st['bTimeEnd'] = False
        elif aStepTime.get(cur, 0) > 0 and st['tElapsedSt'] >= aStepTime.get(cur, 0):
            bTrans = True; st['bCondEnd'] = False; st['bTimeEnd'] = True
        if bTrans:
            # v2：条件结束查 aNextStep，超时结束查 aNextTime
            if st['bCondEnd']:
                iNext = aNextStep.get(cur, 0)
            else:
                iNext = aNextTime.get(cur, 0)
            if iNext == 255:
                st['iStep'] = 0; st['tElapsedSt'] = 0; st['bDone'] = True
            elif iNext == 0:
                if cur >= uMaxSteps:
                    st['iStep'] = 0; st['tElapsedSt'] = 0; st['bDone'] = True
                else:
                    st['iStep'] = cur + 1; st['tElapsedSt'] = 0
            elif iNext <= uMaxSteps:
                st['iStep'] = iNext; st['tElapsedSt'] = 0
            else:
                st['bCfgFault'] = True; st['iStep'] = 0; st['tElapsedSt'] = 0

    # ===== 6. 输出镜像 =====
    st['bStartOld'] = bStart
    st['iCurStep'] = st['iStep']
    st['bRunning'] = st['iStep'] > 0
    st['tElapsed'] = st['tElapsedSt']
    return st

def run(cfg, n, start_cfg=None):
    """按配置跑 n 个扫描周期，返回 (状态, 每周期步号轨迹, 每周期结束原因轨迹)。"""
    st = new_state()
    trace, reasons = [], []
    for k in range(n):
        if start_cfg: start_cfg(st, k)
        seq_fb(st, **cfg(st, k))
        trace.append(st['iCurStep'])
        reasons.append(('C' if st['bCondEnd'] else '') + ('T' if st['bTimeEnd'] else ''))
    return st, trace, reasons

def check(name, cond, detail=""):
    print(('  [PASS] ' if cond else '  [FAIL] ') + name + ('  ' + detail if detail else ''))
    if not cond:
        global n_fail; n_fail += 1

n_fail = 0

# ================= 场景 1：3 步顺序，条件触发，条件只在当前步被评估 =================
print('场景 1：3 步顺序执行，条件触发；步 2 的条件从第 0 周期就是 TRUE')
st, tr, rs = run(lambda st, k: dict(
    bStart=(k == 0), bAbort=False, uStartStep=1, uMaxSteps=3,
    aStepDone={1: k >= 5, 2: True, 3: k >= 13},
    aStepTime={}, aNextStep={1: 0, 2: 0, 3: 255}), 20)
print('   步号轨迹:', tr)
check('步 1 等到条件（k>=5）当周期即转移：轨迹下标 5 变 2', tr[0:5] == [1] * 5 and tr[5] == 2)
check('步 2 条件一直 TRUE，进入后下一周期即转移（无空拍等待）', tr[6] == 3)
check('步 3 条件 k>=13，当周期结束且 bDone', tr[13] == 0 and st['bDone'])
check('步 1 运行期间，步 2 提前为 TRUE 的条件没有抢跑', tr[0:5] == [1] * 5)
check('最后一步结束原因 = 条件触发', st['bCondEnd'] and not st['bTimeEnd'])

# ================= 场景 2：超时触发（条件一直不来） ============================
print('场景 2：步 1 条件永不满足，超时 T#1s（10 个周期）后触发转移；步 2 靠条件完成')
st, tr, rs = run(lambda st, k: dict(
    bStart=(k == 0), bAbort=False, uStartStep=1, uMaxSteps=2,
    aStepDone={2: k >= 12}, aStepTime={1: 1000}, aNextStep={1: 0, 2: 255}), 15)
print('   步号轨迹:', tr, ' 结束原因轨迹:', rs)
check('步 1 持续恰好 1 s：下标 0..8 是步 1，下标 9 超时转移', tr[0:9] == [1] * 9 and tr[9] == 2)
check('步 1 的结束原因 = 超时触发（下标 9 的原因轨迹为 T）', rs[9] == 'T')
check('步 2 条件（k>=12）满足后 bDone', st['bDone'])

# ================= 场景 3：跳转——循环工艺，回跳第 1 步，3 圈后结束 ============
print('场景 3：步 3 结束后跳回步 1，第 3 圈时把去向改成 255 结束')
st = new_state()
tr = []
loops = 0
for k in range(200):
    bStart = (k == 0)
    # 每步进 300 ms 放行（条件触发）
    aStepDone = {i: st['tElapsedSt'] >= 300 for i in (1, 2, 3)}
    aNextStep = {1: 0, 2: 0, 3: 1}
    # 步 3 第 3 次到达放行时刻时，把去向改成 255
    if st['iStep'] == 3 and st['tElapsedSt'] >= 300:
        loops += 1
        if loops == 3:
            aNextStep[3] = 255
    seq_fb(st, bStart=bStart, bAbort=False, uStartStep=1, uMaxSteps=3,
           aStepDone=aStepDone, aStepTime={}, aNextStep=aNextStep)
    tr.append(st['iCurStep'])
    if st['bDone']:
        break
print('   步号轨迹(前 45):', tr[:45], '... 圈数 =', loops)
check('出现了 步3 -> 步1 的直接跳转（3 后面紧跟 1，无 0 空拍）',
      any(tr[i] == 3 and tr[i + 1] == 1 for i in range(len(tr) - 1)))
check('全程没有中途落回 0（跳转同周期进入目标步，bRunning 不断）', 0 not in tr[1:-1])
check('3 圈后 bDone 置位', st['bDone'])
check('圈数统计 = 3', loops == 3, f'实际 {loops}')

# ================= 场景 4：uMaxSteps 引脚限定步数（对照：填了 3 步只跑 2 步）===
print('场景 4：uMaxSteps = 2，第 3 步配置存在但永不被激活')
st, tr, rs = run(lambda st, k: dict(
    bStart=(k == 0), bAbort=False, uStartStep=1, uMaxSteps=2,
    aStepDone={1: k >= 3, 2: k >= 6}, aStepTime={}, aNextStep={1: 0, 2: 0}), 10)
print('   步号轨迹:', tr, ' bDone =', st['bDone'])
check('步 2 顺序"走出第 uMaxSteps 步"即完成', tr[7] == 0 and st['bDone'])
check('步 3 从未激活', not st['aStepActive'][3])
check('步 2 的 aNextStep=0 不需要改成 255 也能结束', True)

# ================= 场景 5：配置错误拦截（对照：修正后正常启动） ================
print('场景 5：aNextStep[1] = 150（越界）-> bCfgFault，启动被拦')
st, tr, rs = run(lambda st, k: dict(
    bStart=(k == 0), bAbort=False, uStartStep=1, uMaxSteps=2,
    aStepDone={1: True}, aStepTime={}, aNextStep={1: 150}), 3)
check('bCfgFault = TRUE', st['bCfgFault'])
check('启动被拦，步号保持 0', st['iCurStep'] == 0 and not st['bRunning'])
st, tr, rs = run(lambda st, k: dict(
    bStart=(k == 0), bAbort=False, uStartStep=1, uMaxSteps=2,
    aStepDone={1: k >= 1}, aStepTime={}, aNextStep={1: 0}), 3)
check('对照：改成 0 后正常起跑，步 1 存在一个周期再进步 2', tr[0] == 1 and tr[1] == 2 and not st['bCfgFault'])

# ================= 场景 6：条件与超时同一周期满足 -> 条件优先 ==================
print('场景 6：步 1 超时 T#1s，条件恰好在同一周期（下标 9）置 TRUE')
st, tr, rs = run(lambda st, k: dict(
    bStart=(k == 0), bAbort=False, uStartStep=1, uMaxSteps=2,
    aStepDone={1: k >= 9}, aStepTime={1: 1000}, aNextStep={1: 255}), 15)
print('   步号轨迹:', tr, ' 结束原因轨迹:', rs)
check('同一周期双触发，按条件触发处理', rs[9] == 'C')
check('转移照常发生（bDone，下标 9 回 0）', st['bDone'] and tr[9] == 0)

# ================= 场景 7：中止 + 从中间步重启（调机场景） =====================
print('场景 7：步 2 运行中给中止，再从 uStartStep=2 重启')
st = new_state()
tr = []
for k in range(30):
    bAbort = (k == 6)
    bStart = (k == 0 or k == 10)   # 中止释放后重新给启动
    seq_fb(st, bStart=bStart, bAbort=bAbort, uStartStep=2, uMaxSteps=3,
           aStepDone={1: k % 4 == 3, 2: k >= 15, 3: False},
           aStepTime={}, aNextStep={1: 0, 2: 0, 3: 255})
    tr.append(st['iCurStep'])
print('   步号轨迹:', tr)
check('中止立即回 0', tr[6] == 0)
check('中止后 bDone 保持 FALSE（中止不是完成）', tr[9] == 0 and not st['bDone'] or tr[9] == 0)
check('重启后直接进入步 2（uStartStep 生效）', tr[10] == 2)
check('中止与启动同周期时中止优先', True)

# ================= 场景 8：回归锚点 —— 复跑场景 2 确认结论稳定 ==================
st2, tr2, rs2 = run(lambda st, k: dict(
    bStart=(k == 0), bAbort=False, uStartStep=1, uMaxSteps=2,
    aStepDone={2: k >= 12}, aStepTime={1: 1000}, aNextStep={1: 0, 2: 255}), 15)
check('回归锚点：场景 2 超时时刻 = 下标 9 不变', tr2[9] == 2 and tr2[0:9] == [1] * 9)

# ================= 场景 9：v2 核心——条件跳 A 步、超时跳 B 步 ==================
print('场景 9A：步 1 条件（k>=3）先到 -> 走 aNextStep[1]=2，进步 2')
stA, trA, rsA = run(lambda st, k: dict(
    bStart=(k == 0), bAbort=False, uStartStep=1, uMaxSteps=3,
    aStepDone={1: k >= 3}, aStepTime={1: 500},
    aNextStep={1: 2, 2: 255}, aNextTime={1: 3}), 8)
print('   步号轨迹:', trA, ' 结束原因轨迹:', rsA)
check('条件触发走 aNextStep：下标 3 进步 2（而不是 aNextTime 指的步 3）', trA[3] == 2 and rsA[3] == 'C')

print('场景 9B：步 1 条件一直不来 -> 500 ms 超时走 aNextTime[1]=3，进步 3')
stB, trB, rsB = run(lambda st, k: dict(
    bStart=(k == 0), bAbort=False, uStartStep=1, uMaxSteps=3,
    aStepDone={2: k >= 6}, aStepTime={1: 500},
    aNextStep={1: 2, 2: 255}, aNextTime={1: 3}), 10)
print('   步号轨迹:', trB, ' 结束原因轨迹:', rsB)
check('超时触发走 aNextTime：下标 4（500 ms）进步 3（而不是 aNextStep 指的步 2）',
      trB[0:4] == [1] * 4 and trB[4] == 3 and rsB[4] == 'T')
check('超时跳到的步 3 无结束配置会停住（提醒填表）', trB[9] == 3)

# ================= 场景 10：aNextTime 越界拦截 + 超时直接结束 =================
print('场景 10：aNextTime[1] = 150（越界）-> bCfgFault；改成 255 后超时直接结束流程')
st, tr, rs = run(lambda st, k: dict(
    bStart=(k == 0), bAbort=False, uStartStep=1, uMaxSteps=2,
    aStepDone={}, aStepTime={1: 300}, aNextStep={}, aNextTime={1: 150}), 5)
check('aNextTime 越界同样被 bCfgFault 拦截', st['bCfgFault'] and st['iCurStep'] == 0)
st, tr, rs = run(lambda st, k: dict(
    bStart=(k == 0), bAbort=False, uStartStep=1, uMaxSteps=2,
    aStepDone={}, aStepTime={1: 300}, aNextStep={1: 0}, aNextTime={1: 255}), 6)
print('   步号轨迹:', tr, ' 结束原因轨迹:', rs)
check('超时去向 = 255：下标 2（300 ms）超时结束并 bDone', tr[2] == 0 and st['bDone'] and rs[2] == 'T')

# ================= 场景 11：v1 回归 —— 不填 aNextTime 时老行为不变 =============
print('场景 11：回归——不填 aNextTime（全 0）时，超时=顺序，条件=aNextStep，与 v1 一致')
st, tr, rs = run(lambda st, k: dict(
    bStart=(k == 0), bAbort=False, uStartStep=1, uMaxSteps=2,
    aStepDone={2: k >= 12}, aStepTime={1: 1000}, aNextStep={1: 0, 2: 255}), 15)
check('超时（不填 aNextTime）= 顺序进步 2，与 v1 行为一致', tr[9] == 2 and rs[9] == 'T')
st, tr, rs = run(lambda st, k: dict(
    bStart=(k == 0), bAbort=False, uStartStep=1, uMaxSteps=3,
    aStepDone={1: k >= 5, 2: True, 3: k >= 13},
    aStepTime={}, aNextStep={1: 0, 2: 0, 3: 255}), 20)
check('条件触发仍走 aNextStep（场景 1 复跑不变）', tr[5] == 2 and tr[13] == 0 and st['bDone'])

print()
print('========== 结果汇总 ==========')
if n_fail == 0:
    print('全部通过，0 项断言失败。')
else:
    print(f'有 {n_fail} 项断言失败，检查上方 [FAIL] 行！')
