fix(k1): simplify response-gated launch UX

This commit is contained in:
DCCONSTRUCTIONS
2026-07-18 18:17:16 +03:00
parent 7b7b5d6cad
commit d7a2c22faf
10 changed files with 306 additions and 161 deletions
@@ -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,