Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/components/calibration-card.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Field } from "@openservocore/client";
import { Check, CircleQuestionMark, Ruler } from "lucide-react";
import { useId } from "react";
import { StampNotice, useStampNotice } from "@/components/stamp-notice";
import { ValueEditor } from "@/components/value-editor";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
Expand All @@ -10,6 +11,7 @@ import { Skeleton } from "@/components/ui/skeleton";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useBus, useClientOnce } from "@/lib/bus/hooks";
import { CALIBRATION_REGISTERS, editReason, type CalibrationRegister } from "@/lib/calibration";
import { STAMP_MISMATCH } from "@/lib/data-state";
import { hex16, hexAddr } from "@/lib/format";
import { useSession } from "@/lib/session";
import { stampWords } from "@/lib/stamp";
Expand All @@ -34,12 +36,14 @@ export function CalibrationCard({ id, uid }: { id: number; uid: string }) {
(c) => (descriptor === undefined ? Promise.resolve(undefined) : c.plantStamp(id, descriptor)),
[id, descriptor, cal, card?.data.flags],
);
const notice = useStampNotice(((card?.data.flags ?? 0) & STAMP_MISMATCH) !== 0);

async function apply(field: Field, raw: number) {
await bus.write(id, field.name, { kind: field.kind === "int" ? "int" : "uint", value: raw });
// The write dirties the CALIB span; the session re-reads the constants
// every card on the page shows.
refreshConstants(uid);
notice.onEdit();
}

return (
Expand Down Expand Up @@ -67,6 +71,7 @@ export function CalibrationCard({ id, uid }: { id: number; uid: string }) {
)}
</CardHeader>
<CardContent>
{notice.shown && <StampNotice id={id} onStamped={notice.onStamped} />}
{descriptorError !== undefined && <p className="text-danger">{descriptorError}</p>}
{cal === undefined || fields === undefined ? (
<div className="grid grid-cols-2 gap-x-6 gap-y-3">
Expand Down
14 changes: 14 additions & 0 deletions src/components/stamp-notice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,20 @@ import { useBus } from "@/lib/bus/hooks";
import { useSession } from "@/lib/session";
import { saveAndStamp } from "@/lib/stamp";

/** Whether the notice shows: an edit landed while the stamp matched, so a mismatch now is that edit's doing. */
export function useStampNotice(mismatch: boolean) {
const [edited, setEdited] = useState(false);
return {
shown: edited && mismatch,
onEdit: () => {
if (!mismatch) setEdited(true);
},
onStamped: () => {
setEdited(false);
},
};
}

/** Why closed loop went off after a covered edit, and the one way back the editor offers. */
export function StampNotice({ id, onStamped }: { id: number; onStamped: () => void }) {
const bus = useBus();
Expand Down
19 changes: 4 additions & 15 deletions src/routes/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/component
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { StampNotice } from "@/components/stamp-notice";
import { StampNotice, useStampNotice } from "@/components/stamp-notice";
import { TableSearch } from "@/components/table-search";
import { ValueEditor } from "@/components/value-editor";
import { useBus, useReadOnce, useRegisters } from "@/lib/bus/hooks";
Expand Down Expand Up @@ -159,21 +159,12 @@ function Table({
}) {
const { servos, values } = useSession();
const [refresh, setRefresh] = useState(0);
/** An edit landed while the stamp matched, so a mismatch now is that edit's doing. */
const [edited, setEdited] = useState(false);
const servo = servos.find((s) => s.id === id);
const flags = (servo === undefined ? undefined : values.get(servo.uid)?.data.flags) ?? 0;
const mismatch = (flags & STAMP_MISMATCH) !== 0;
const notice = useStampNotice((flags & STAMP_MISMATCH) !== 0);
return (
<>
{edited && mismatch && (
<StampNotice
id={id}
onStamped={() => {
setEdited(false);
}}
/>
)}
{notice.shown && <StampNotice id={id} onStamped={notice.onStamped} />}
<Tabs
value={tab}
onValueChange={(v) => {
Expand Down Expand Up @@ -211,9 +202,7 @@ function Table({
open={open}
onToggle={onToggle}
jump={jump?.tab === t.name ? jump : undefined}
onEdit={() => {
if (!mismatch) setEdited(true);
}}
onEdit={notice.onEdit}
/>
</TabsContent>
))}
Expand Down
37 changes: 36 additions & 1 deletion tests/e2e/stamp.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test, type Locator, type Page } from "@playwright/test";
import { openTable, tableRow } from "./helpers";
import { gotoSim, openTable, tableRow } from "./helpers";

/** Opens the editor of velocity_limit_cps, a covered row low on the page, and lowers its value. */
async function editLowRow(page: Page): Promise<Locator> {
Expand Down Expand Up @@ -73,3 +73,38 @@ test("a stamp notice that mounts while the editor closes stays in view", async (
await expect(editor).toHaveCount(0);
await expectInView(page, notice);
});

test("a calibration card edit raises the same notice, and save and stamp makes the stamp match again", async ({
page,
}) => {
await gotoSim(page, [1]);
await page.getByRole("button", { name: "ID 1" }).click();
const card = page.getByRole("region", { name: "Calibration" });
const stamp = card.getByRole("group", { name: "Plant stamp" });
await expect(stamp).toContainText("Stamped: the calibration");

await card
.getByRole("group", { name: "Sensor highest" })
.getByRole("button", { name: "4095 counts" })
.click();
await page.getByRole("textbox").fill("4000");
await page.getByRole("button", { name: "Apply" }).click();
await expect(stamp).toContainText("Changed since it was stamped");

const notice = page.getByRole("status", { name: "Stamp" });
await expect(notice).toContainText("Closed loop is off: the stamp no longer matches.");
await notice.getByRole("button", { name: "Save and stamp" }).click();
await expect(notice).toHaveCount(0);

await expect(stamp).toContainText("Stamped: the calibration");
const [, stored, computed] =
/= (0x[0-9a-f]{4}), computed (0x[0-9a-f]{4})/i.exec((await stamp.textContent()) ?? "") ?? [];
expect(stored).toBeDefined();
expect(stored).toBe(computed);
await expect(
card
.getByRole("group", { name: "Sensor highest" })
.getByRole("button", { name: "4000 counts" }),
).toBeVisible();
await expect(page.getByRole("status", { name: "Data state" })).toHaveCount(0);
});
Loading