updates
This commit is contained in:
@@ -270,6 +270,8 @@ def _run_step(ctx: ScenarioContext, action: str, spec: dict[str, Any]) -> None:
|
||||
_listen(ctx, float(spec.get("seconds", spec.get("value", 0.0))))
|
||||
elif action == "listen_ack":
|
||||
_step_listen_ack(ctx, spec)
|
||||
elif action == "listen_ack_until_quiet":
|
||||
_step_listen_ack_until_quiet(ctx, spec)
|
||||
elif action == "send":
|
||||
frame = _parse_required_frame(spec.get("frame"))
|
||||
label = str(spec.get("label", "send"))
|
||||
@@ -378,7 +380,38 @@ def _step_table_sweep(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
|
||||
|
||||
def _step_listen_ack(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
|
||||
seconds = float(spec.get("seconds", spec.get("value", 1.0)))
|
||||
ack = _ack_config(
|
||||
ack = _ack_config_from_step(spec)
|
||||
ack_text = (
|
||||
f"ack_frame={format_frame(ack['frame'])}"
|
||||
if ack["ack_mode"] == "fixed"
|
||||
else f"ack_mode={ack['ack_mode']}"
|
||||
)
|
||||
ctx.logger.event(
|
||||
f"LISTEN_ACK seconds={seconds:.3f} target_mode={ack['target_mode']} targets={len(ack['targets'])} "
|
||||
f"{ack_text} limit_scope={ack['limit_scope']} max_acks={ack['max_acks']}"
|
||||
)
|
||||
_listen_with_ack(ctx, seconds, None, ack)
|
||||
|
||||
|
||||
def _step_listen_ack_until_quiet(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
|
||||
seconds = float(spec.get("seconds", spec.get("value", 10.0)))
|
||||
quiet_seconds = float(spec.get("quiet_seconds", spec.get("quiet", 0.750)))
|
||||
ack = _ack_config_from_step(spec)
|
||||
ack_text = (
|
||||
f"ack_frame={format_frame(ack['frame'])}"
|
||||
if ack["ack_mode"] == "fixed"
|
||||
else f"ack_mode={ack['ack_mode']}"
|
||||
)
|
||||
ctx.logger.event(
|
||||
f"LISTEN_ACK_UNTIL_QUIET seconds={seconds:.3f} quiet={quiet_seconds:.3f} "
|
||||
f"target_mode={ack['target_mode']} targets={len(ack['targets'])} "
|
||||
f"{ack_text} limit_scope={ack['limit_scope']} max_acks={ack['max_acks']}"
|
||||
)
|
||||
_listen_with_ack(ctx, seconds, None, ack, quiet_seconds=quiet_seconds)
|
||||
|
||||
|
||||
def _ack_config_from_step(spec: dict[str, Any]) -> dict[str, Any]:
|
||||
return _ack_config(
|
||||
{
|
||||
"enabled": spec.get("enabled", True),
|
||||
"frames": spec.get("frames", spec.get("frame")),
|
||||
@@ -393,18 +426,9 @@ def _step_listen_ack(ctx: ScenarioContext, spec: dict[str, Any]) -> None:
|
||||
"ack_mode": spec.get("ack_mode", spec.get("mode", "fixed")),
|
||||
"target_mode": spec.get("target_mode", spec.get("match", "explicit")),
|
||||
"limit_scope": spec.get("limit_scope", spec.get("scope", "local")),
|
||||
"respond_on": spec.get("respond_on", spec.get("send_on", [])),
|
||||
}
|
||||
)
|
||||
ack_text = (
|
||||
f"ack_frame={format_frame(ack['frame'])}"
|
||||
if ack["ack_mode"] == "fixed"
|
||||
else f"ack_mode={ack['ack_mode']}"
|
||||
)
|
||||
ctx.logger.event(
|
||||
f"LISTEN_ACK seconds={seconds:.3f} target_mode={ack['target_mode']} targets={len(ack['targets'])} "
|
||||
f"{ack_text} limit_scope={ack['limit_scope']} max_acks={ack['max_acks']}"
|
||||
)
|
||||
_listen_with_ack(ctx, seconds, None, ack)
|
||||
|
||||
|
||||
def _ack_config(raw: Any) -> dict[str, Any]:
|
||||
@@ -438,6 +462,7 @@ def _ack_config(raw: Any) -> dict[str, Any]:
|
||||
"ack_mode": ack_mode,
|
||||
"target_mode": target_mode,
|
||||
"limit_scope": limit_scope,
|
||||
"respond_on": _response_rules(spec.get("respond_on", [])),
|
||||
}
|
||||
|
||||
|
||||
@@ -458,28 +483,71 @@ def _ack_matches(frame: bytes, ack: dict[str, Any]) -> bool:
|
||||
return frame[0] in {0x00, 0x01, 0x02} and not (frame[1] & 0x80)
|
||||
|
||||
|
||||
def _response_rules(raw: Any) -> list[dict[str, Any]]:
|
||||
values = raw if isinstance(raw, list) else ([raw] if raw else [])
|
||||
rules: list[dict[str, Any]] = []
|
||||
for index, value in enumerate(values):
|
||||
if not isinstance(value, dict):
|
||||
raise SystemExit("respond_on entries must be objects")
|
||||
targets = _parse_frame_list(value.get("frames", value.get("frame")))
|
||||
response_frame = _parse_required_frame(value.get("send", value.get("response")))
|
||||
label = str(value.get("label", f"respond_on_{index + 1}"))
|
||||
rules.append(
|
||||
{
|
||||
"targets": targets,
|
||||
"frame": response_frame,
|
||||
"label": label,
|
||||
"delay": float(value.get("delay", 0.0)),
|
||||
"listen": float(value.get("listen", 0.0)),
|
||||
"once": bool(value.get("once", True)),
|
||||
}
|
||||
)
|
||||
return rules
|
||||
|
||||
|
||||
def _listen_with_ack(
|
||||
ctx: ScenarioContext,
|
||||
seconds: float,
|
||||
selector: int,
|
||||
selector: int | None,
|
||||
ack: dict[str, Any],
|
||||
*,
|
||||
quiet_seconds: float | None = None,
|
||||
) -> list[bytes]:
|
||||
deadline = time.monotonic() + max(0.0, seconds)
|
||||
observed: list[bytes] = []
|
||||
pending: list[bytes] = []
|
||||
pending_index = 0
|
||||
last_activity = time.monotonic()
|
||||
acked_targets: set[bytes] = set()
|
||||
fired_responses: set[int] = set()
|
||||
ack_start = ctx.ack_sent
|
||||
target_start = sum(ctx.target_counts.values())
|
||||
|
||||
def enqueue(frames: list[bytes]) -> None:
|
||||
nonlocal last_activity
|
||||
if not frames:
|
||||
return
|
||||
observed.extend(frames)
|
||||
pending.extend(frames)
|
||||
last_activity = time.monotonic()
|
||||
|
||||
while time.monotonic() < deadline:
|
||||
frames = _read_available(ctx, selector=selector)
|
||||
observed.extend(frames)
|
||||
if not frames:
|
||||
enqueue(frames)
|
||||
if not frames and pending_index >= len(pending):
|
||||
if quiet_seconds is not None and time.monotonic() - last_activity >= quiet_seconds:
|
||||
ctx.logger.event(f"LISTEN_ACK_QUIET quiet={quiet_seconds:.3f}s")
|
||||
break
|
||||
sleep_for = min(max(0.001, ack["poll_interval"]), max(0.0, deadline - time.monotonic()))
|
||||
if sleep_for > 0:
|
||||
time.sleep(sleep_for)
|
||||
continue
|
||||
if not ack["enabled"]:
|
||||
pending_index = len(pending)
|
||||
continue
|
||||
for frame in frames:
|
||||
while pending_index < len(pending):
|
||||
frame = pending[pending_index]
|
||||
pending_index += 1
|
||||
if not _ack_matches(frame, ack):
|
||||
continue
|
||||
_count_target(ctx, frame)
|
||||
@@ -493,7 +561,7 @@ def _listen_with_ack(
|
||||
continue
|
||||
acked_targets.add(frame)
|
||||
if ack["guard"] > 0:
|
||||
observed.extend(_listen(ctx, ack["guard"], selector=selector))
|
||||
enqueue(_listen(ctx, ack["guard"], selector=selector))
|
||||
_send_and_record(ctx, _ack_frame_for_target(frame, ack), "ack", capture=ctx.args.snapshot_acks)
|
||||
ctx.ack_sent += 1
|
||||
if _ack_limit_reached(ctx, ack, ack_start=ack_start, target_start=target_start):
|
||||
@@ -501,7 +569,22 @@ def _listen_with_ack(
|
||||
if ack["abort_on_limit"]:
|
||||
ctx.abort_requested = True
|
||||
if ack["post_read"] > 0:
|
||||
observed.extend(_listen(ctx, ack["post_read"], selector=selector))
|
||||
enqueue(_listen(ctx, ack["post_read"], selector=selector))
|
||||
for rule_index, rule in enumerate(ack["respond_on"]):
|
||||
if frame not in rule["targets"]:
|
||||
continue
|
||||
if rule["once"] and rule_index in fired_responses:
|
||||
continue
|
||||
fired_responses.add(rule_index)
|
||||
if rule["delay"] > 0:
|
||||
time.sleep(rule["delay"])
|
||||
ctx.logger.event(
|
||||
f"RESPOND_ON target={format_frame(frame)} "
|
||||
f"send={format_frame(rule['frame'])} label={rule['label']}"
|
||||
)
|
||||
_send_and_record(ctx, rule["frame"], rule["label"], capture=ctx.args.snapshot_acks)
|
||||
if rule["listen"] > 0:
|
||||
enqueue(_listen(ctx, rule["listen"], selector=selector))
|
||||
if ctx.abort_requested:
|
||||
return observed
|
||||
return observed
|
||||
@@ -753,6 +836,7 @@ def _quiet_console_line(line: str) -> bool:
|
||||
"NOTE ",
|
||||
"SNAPSHOT_SCHEDULE ",
|
||||
"SNAPSHOT_ERROR ",
|
||||
"RESPOND_ON ",
|
||||
"Summary",
|
||||
"rx_frames=",
|
||||
"resync_events=",
|
||||
@@ -760,6 +844,8 @@ def _quiet_console_line(line: str) -> bool:
|
||||
"abort_requested=",
|
||||
"known_shutter",
|
||||
"queued_shutter",
|
||||
"iris_mblack",
|
||||
"selector_0013",
|
||||
)
|
||||
return any(fragment in line for fragment in keep_fragments)
|
||||
|
||||
@@ -772,25 +858,14 @@ def _print_step_dry_run(action: str, spec: dict[str, Any], stdout: TextIO, *, in
|
||||
print(f"{indent}listen={float(spec.get('listen', 0.0)):.3f}s", file=stdout)
|
||||
elif action in {"drain", "listen", "wait"}:
|
||||
print(f"{indent}seconds={float(spec.get('seconds', spec.get('value', 0.0))):.3f}", file=stdout)
|
||||
elif action == "listen_ack":
|
||||
ack = _ack_config(
|
||||
{
|
||||
"enabled": spec.get("enabled", True),
|
||||
"frames": spec.get("frames", spec.get("frame")),
|
||||
"ack_frame": spec.get("ack_frame"),
|
||||
"ack_guard": spec.get("ack_guard", 0.020),
|
||||
"poll_interval": spec.get("poll_interval", 0.005),
|
||||
"post_ack_read": spec.get("post_ack_read", 0.250),
|
||||
"once_per_selector": spec.get("once_per_frame", False),
|
||||
"max_acks": spec.get("max_acks"),
|
||||
"max_target_hits": spec.get("max_target_hits"),
|
||||
"abort_on_limit": spec.get("abort_on_limit", False),
|
||||
"ack_mode": spec.get("ack_mode", spec.get("mode", "fixed")),
|
||||
"target_mode": spec.get("target_mode", spec.get("match", "explicit")),
|
||||
"limit_scope": spec.get("limit_scope", spec.get("scope", "local")),
|
||||
}
|
||||
)
|
||||
elif action in {"listen_ack", "listen_ack_until_quiet"}:
|
||||
ack = _ack_config_from_step(spec)
|
||||
print(f"{indent}seconds={float(spec.get('seconds', spec.get('value', 1.0))):.3f}", file=stdout)
|
||||
if action == "listen_ack_until_quiet":
|
||||
print(
|
||||
f"{indent}quiet={float(spec.get('quiet_seconds', spec.get('quiet', 0.750))):.3f}s",
|
||||
file=stdout,
|
||||
)
|
||||
if not ack["enabled"]:
|
||||
print(f"{indent}ack=disabled", file=stdout)
|
||||
else:
|
||||
@@ -805,6 +880,13 @@ def _print_step_dry_run(action: str, spec: dict[str, Any], stdout: TextIO, *, in
|
||||
f"max_target_hits={ack['max_target_hits']}",
|
||||
file=stdout,
|
||||
)
|
||||
for rule in ack["respond_on"]:
|
||||
print(
|
||||
f"{indent}respond_on={len(rule['targets'])} "
|
||||
f"send={format_frame(rule['frame'])} label={rule['label']} "
|
||||
f"once={int(rule['once'])}",
|
||||
file=stdout,
|
||||
)
|
||||
elif action in {"prompt", "note"}:
|
||||
message = str(spec.get("message", spec.get("value", "Press Enter to continue.")))
|
||||
print(f"{indent}message={message}", file=stdout)
|
||||
|
||||
Reference in New Issue
Block a user