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: 4 additions & 1 deletion src/components/about-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { useReadOnce } from "@/lib/bus/hooks";
import { modelName } from "@/lib/descriptor";
import { formatVersion, hex16 } from "@/lib/format";
import { features, identityFrom, IDENTITY_REGISTERS } from "@/lib/identity";
import { useSession } from "@/lib/session";

function Row({ label, children }: { label: string; children: ReactNode }) {
return (
Expand All @@ -19,7 +20,9 @@ function Row({ label, children }: { label: string; children: ReactNode }) {
}

export function AboutCard({ id, uid }: { id: number; uid: string }) {
const { snapshot, error } = useReadOnce(id, IDENTITY_REGISTERS);
const { descriptor, descriptorError } = useSession();
const { snapshot, error: readError } = useReadOnce(id, IDENTITY_REGISTERS, [descriptor]);
const error = (descriptor === undefined ? undefined : readError) ?? descriptorError;
const identity = snapshot === undefined ? undefined : identityFrom(snapshot.read);

const name = identity === undefined ? undefined : modelName(identity.model);
Expand Down
12 changes: 7 additions & 5 deletions src/lib/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ class Controller {
private readonly cards = new Map<string, Partial<CardValues>>();
private readonly subscriptions = new Map<string, () => void>();
private readonly reading = new Set<string>();
private readonly fetching = new Set<string>();
private readonly fetching = new Map<string, Promise<void>>();
private readonly listeners = new Set<() => void>();

readonly subscribe = (listener: () => void): (() => void) => {
Expand Down Expand Up @@ -197,8 +197,7 @@ class Controller {
if (ping === undefined) continue;
const key = modelKey(ping);
if (this.snap.models.has(key) || this.fetching.has(key)) continue;
this.fetching.add(key);
fetchDescriptor(ping.model, ping.fw)
const fetched = fetchDescriptor(ping.model, ping.fw)
.then(
(descriptor) => {
if (this.snap.models.has(key)) {
Expand All @@ -224,6 +223,7 @@ class Controller {
.finally(() => {
this.fetching.delete(key);
});
this.fetching.set(key, fetched);
}
}

Expand Down Expand Up @@ -425,10 +425,12 @@ class Controller {
}

/** Test hook: a table into a simulated servo, the way `osc lut write` puts one there. */
writePosLut(id: number, points: number[]): Promise<void> {
async writePosLut(id: number, points: number[]): Promise<void> {
const servo = this.snap.state.servos.find((s) => s.id === id);
// The servo answers before its descriptor arrives; the write needs the descriptor.
if (servo?.ping !== undefined) await this.fetching.get(modelKey(servo.ping));
const descriptor = servo === undefined ? undefined : this.modelFor(servo)?.descriptor;
if (descriptor === undefined) return Promise.reject(new Error(`no descriptor for ID ${id}`));
if (descriptor === undefined) throw new Error(`no descriptor for ID ${id}`);
return this.bus.command((c) => c.writePosLut(id, descriptor, points));
}

Expand Down
13 changes: 12 additions & 1 deletion tests/e2e/control.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,21 @@ async function openLive(page: Page, opts: { debug?: boolean } = {}): Promise<voi
await expect(page.getByRole("combobox", { name: "Mode" })).toBeEnabled();
}

async function pickMode(page: Page, mode: string): Promise<void> {
/** Each mode's goal unit, which the goal readout takes from the servo's read-back of `mode`. */
const GOAL_UNIT = {
Position: / deg$/,
Velocity: / deg\/s$/,
Current: / mA$/,
"Open loop": / %$/,
} as const;

async function pickMode(page: Page, mode: keyof typeof GOAL_UNIT): Promise<void> {
await page.getByRole("combobox", { name: "Mode" }).click();
await page.getByRole("option", { name: mode }).click();
await expect(page.getByRole("combobox", { name: "Mode" })).toHaveText(mode);
// The combobox shows the pick at once, but the goal control stays on the
// servo's old mode until the read-back lands: a goal typed sooner goes there.
await expect(page.getByLabel("Goal readout")).toHaveText(GOAL_UNIT[mode]);
}

async function setGoal(page: Page, value: string): Promise<void> {
Expand Down
13 changes: 13 additions & 0 deletions tests/e2e/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,19 @@ export async function writeTable(page: Page, id: number, points: number[]): Prom
] as const);
}

/** Holds every descriptor request until the returned release is called. */
export async function holdDescriptors(page: Page): Promise<() => void> {
let release!: () => void;
const released = new Promise<void>((resolve) => {
release = resolve;
});
await page.route("**/descriptors/**", async (route) => {
await released;
await route.continue();
});
return release;
}

/** Lands on the home page with a simulated fleet of `ids` already scanned. */
export async function gotoSim(
page: Page,
Expand Down
14 changes: 13 additions & 1 deletion tests/e2e/pos-lut.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test, type Page } from "@playwright/test";
import { errorSeries, hoverWords } from "../../src/lib/pos-lut";
import { gotoSim, MG90A, writeTable } from "./helpers";
import { gotoSim, holdDescriptors, MG90A, writeTable } from "./helpers";

/** The simulated servo's angle map: stops 5..4095 read 0..202 deg. */
const SIM = {
Expand Down Expand Up @@ -84,3 +84,15 @@ test("the mg90-a table reads live, graded A, its error in degrees and percent of
page.getByRole("region", { name: "Position calibration" }).getByLabel("Table state"),
).toHaveText("No table: the sensor is read as-is.");
});

test("a table written while the descriptor is still on its way lands live", async ({ page }) => {
const land = await holdDescriptors(page);
await gotoSim(page, [1, 2]);
const written = writeTable(page, 1, MG90A.points);
land();
await written;
await openServo(page, 1);
await expect(
page.getByRole("region", { name: "Position calibration" }).getByLabel("Table state"),
).toHaveText("Live: the servo corrects every sensor sample through this table.");
});
13 changes: 12 additions & 1 deletion tests/e2e/servo.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect, test } from "@playwright/test";
import { CORE_VERSION, gotoSim } from "./helpers";
import { CORE_VERSION, gotoSim, holdDescriptors } from "./helpers";

const UID_1 = "c94b8419d1092aec87de5fd151ce290f";

Expand All @@ -19,6 +19,17 @@ test("the servo page reports the selected servo's identity and health", async ({
await expect(health.getByText("No faults.")).toBeVisible();
});

test("the about card reads the identity once a late descriptor lands", async ({ page }) => {
const land = await holdDescriptors(page);
await gotoSim(page, [1, 2]);
await page.getByRole("button", { name: "ID 1" }).click();
const about = page.getByRole("region", { name: "About" });
await expect(about).toBeVisible();
land();
await expect(about.getByText(CORE_VERSION)).toBeVisible();
await expect(about.getByText(UID_1)).toBeVisible();
});

test("the servo page asks for a selection while none is made", async ({ page }) => {
await page.goto("/servo?sim=1,2");
await expect(page.getByText("Pick a servo in the left pane.")).toBeVisible();
Expand Down
Loading