fix(k1): simplify response-gated launch UX
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
type OperatorPresenceConfirmation,
|
||||
type PrepareAcquisitionRequest,
|
||||
type ReplayRequest,
|
||||
type XgridsApplicationControlPhase,
|
||||
type XgridsK1State,
|
||||
} from "./api";
|
||||
import {
|
||||
@@ -35,6 +36,50 @@ export type PendingAction =
|
||||
| "camera"
|
||||
| "viewer";
|
||||
|
||||
export interface CanonicalLiveStartRequest {
|
||||
control: OpenApplicationControlSessionRequest;
|
||||
acquisition: PrepareAcquisitionRequest;
|
||||
physicalAcceptance: OperatorPresenceConfirmation;
|
||||
}
|
||||
|
||||
const CONTROL_STATE_READ_INTERVAL_MS = 250;
|
||||
|
||||
function controlPhase(state: XgridsK1State): XgridsApplicationControlPhase {
|
||||
return state.application_control_session?.state ?? "idle";
|
||||
}
|
||||
|
||||
function controlFailure(state: XgridsK1State): ApiError {
|
||||
const failure = state.application_control_session?.failure;
|
||||
return new ApiError(
|
||||
failure?.message
|
||||
? `Канонический диалог K1 остановлен: ${failure.message}`
|
||||
: "Канонический диалог K1 остановлен до запуска сканирования.",
|
||||
);
|
||||
}
|
||||
|
||||
async function waitForControlPhase(
|
||||
expected: XgridsApplicationControlPhase,
|
||||
acceptState: (state: XgridsK1State) => void,
|
||||
): Promise<XgridsK1State> {
|
||||
for (;;) {
|
||||
const nextState = await xgridsK1Api.getState();
|
||||
acceptState(nextState);
|
||||
const phase = controlPhase(nextState);
|
||||
if (phase === expected) return nextState;
|
||||
if (phase === "failed") throw controlFailure(nextState);
|
||||
if (["idle", "closed", "completed"].includes(phase)) {
|
||||
throw new ApiError(
|
||||
`Управляющая сессия K1 завершилась до ожидаемого этапа «${expected}».`,
|
||||
);
|
||||
}
|
||||
// This cadence only reads local server state. It never schedules, retries,
|
||||
// or times a K1 command; every next write remains gated by device response.
|
||||
await new Promise<void>((resolve) => {
|
||||
window.setTimeout(resolve, CONTROL_STATE_READ_INTERVAL_MS);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function messageFor(error: unknown): string {
|
||||
if (error instanceof ApiError) {
|
||||
const message = localizeRuntimeMessage(error.message) ?? error.message;
|
||||
@@ -189,6 +234,102 @@ export function useXgridsK1Runtime(enabled: boolean) {
|
||||
[run],
|
||||
);
|
||||
|
||||
const startCanonicalAcquisition = useCallback(
|
||||
(request: CanonicalLiveStartRequest) =>
|
||||
run("live", async () => {
|
||||
let nextState = await xgridsK1Api.getState();
|
||||
acceptState(nextState);
|
||||
const plan = liveStartPlan(nextState);
|
||||
if (plan === "blocked") {
|
||||
throw new ApiError("Сначала завершите текущий приём или повтор записи.");
|
||||
}
|
||||
if (plan === "already-running") return nextState;
|
||||
|
||||
for (;;) {
|
||||
const phase = controlPhase(nextState);
|
||||
const acquisition = nextState.acquisition;
|
||||
|
||||
if (["idle", "closed", "completed"].includes(phase)) {
|
||||
if (acquisition && !isTerminalAcquisitionState(acquisition.state)) {
|
||||
throw new ApiError(
|
||||
"Незавершённая подготовка не привязана к открытой control-сессии. Отмените её перед новым запуском.",
|
||||
);
|
||||
}
|
||||
nextState = await xgridsK1Api.openApplicationControlSession(request.control);
|
||||
acceptState(nextState);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (phase === "failed") {
|
||||
if (nextState.application_control_session?.can_open !== true) {
|
||||
throw controlFailure(nextState);
|
||||
}
|
||||
nextState = await xgridsK1Api.openApplicationControlSession(request.control);
|
||||
acceptState(nextState);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (phase === "connecting") {
|
||||
nextState = await waitForControlPhase("connection-ready", acceptState);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (phase === "connection-ready") {
|
||||
nextState = await xgridsK1Api.enterApplicationWorkspace({
|
||||
operator_confirmed: true,
|
||||
});
|
||||
acceptState(nextState);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (phase === "workspace-requested") {
|
||||
nextState = await waitForControlPhase("workspace-ready", acceptState);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (phase === "workspace-ready") {
|
||||
if (!acquisition || isTerminalAcquisitionState(acquisition.state)) {
|
||||
nextState = await xgridsK1Api.prepareAcquisition(request.acquisition);
|
||||
acceptState(nextState);
|
||||
continue;
|
||||
}
|
||||
if (acquisition.state !== "prepared" || acquisition.control_mode !== "plugin-commanded") {
|
||||
throw new ApiError(
|
||||
"Текущая подготовка не принадлежит канонической control-сессии K1.",
|
||||
);
|
||||
}
|
||||
nextState = await waitForControlPhase("project-ready", acceptState);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (phase === "project-requested") {
|
||||
nextState = await waitForControlPhase("project-ready", acceptState);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (phase === "project-ready") {
|
||||
if (!acquisition || acquisition.state !== "prepared") {
|
||||
throw new ApiError("Локальный приём не подготовлен к каноническому START.");
|
||||
}
|
||||
nextState = await xgridsK1Api.startAcquisition({
|
||||
acquisition_id: acquisition.acquisition_id,
|
||||
expected_state_revision: acquisition.state_revision,
|
||||
physical_acceptance: request.physicalAcceptance,
|
||||
});
|
||||
acceptState(nextState);
|
||||
return nextState;
|
||||
}
|
||||
|
||||
if (["start-requested", "initializing", "scanning"].includes(phase)) {
|
||||
return nextState;
|
||||
}
|
||||
|
||||
throw new ApiError(`Запуск K1 недоступен из состояния «${phase}».`);
|
||||
}
|
||||
}),
|
||||
[acceptState, run],
|
||||
);
|
||||
|
||||
const prepareAcquisition = useCallback(
|
||||
(request: PrepareAcquisitionRequest) =>
|
||||
run("live", async () => {
|
||||
@@ -417,6 +558,7 @@ export function useXgridsK1Runtime(enabled: boolean) {
|
||||
openApplicationControlSession,
|
||||
enterApplicationWorkspace,
|
||||
closeApplicationControlSession,
|
||||
startCanonicalAcquisition,
|
||||
prepareAcquisition,
|
||||
startPreparedAcquisition,
|
||||
startReplay,
|
||||
|
||||
Reference in New Issue
Block a user