diff --git a/src/lib/bus/spans.test.ts b/src/lib/bus/spans.test.ts index 597d86c..6f2ecb9 100644 --- a/src/lib/bus/spans.test.ts +++ b/src/lib/bus/spans.test.ts @@ -233,10 +233,16 @@ test("the fleet card reads the live sensors and the health block as one span", ( }); test("faultText names the latched bits, lowest first", () => { - expect(faultText(0)).toBeUndefined(); - expect(faultText(1 << 2)).toBe("Stall detected"); - expect(faultText((1 << 0) | (1 << 5))).toBe("Overcurrent, Undervoltage"); - expect(faultText(1 << 7)).toBe("Fault 0x80"); + expect(faultText(0, 0)).toBeUndefined(); + expect(faultText(1 << 2, 3)).toBe("Stall detected"); + expect(faultText((1 << 0) | (1 << 5), 6)).toBe("Overcurrent, Undervoltage"); + expect(faultText(1 << 7, 0)).toBe("Fault 0x80"); +}); + +test("faultText names the data bit by its fault code", () => { + expect(faultText(1 << 6, 7)).toBe("Closed loop refused"); + expect(faultText(1 << 6, 8)).toBe("Drive reversed"); + expect(faultText((1 << 2) | (1 << 6), 8)).toBe("Stall detected, Drive reversed"); }); test("limitText names what governs, lowest first", () => { diff --git a/src/lib/bus/spans.ts b/src/lib/bus/spans.ts index 39cf0f7..d08ab1c 100644 --- a/src/lib/bus/spans.ts +++ b/src/lib/bus/spans.ts @@ -3,7 +3,7 @@ import type { Descriptor, Field, Health, Value } from "@openservocore/client"; import { dutyPercent } from "../control"; -import type { DataState } from "../data-state"; +import { FAULT_DATA_BIT, FAULT_DRIVE_REVERSED, type DataState } from "../data-state"; import { biasesFromTable, calibrationFromTable, @@ -247,8 +247,12 @@ const FAULTS: readonly string[] = [ "Closed loop refused", ]; -export function faultText(faultFlags: number): string | undefined { - const set = FAULTS.filter((_, bit) => (faultFlags & (1 << bit)) !== 0); +export function faultText(faultFlags: number, faultCode: number): string | undefined { + const set = FAULTS.flatMap((name, bit) => { + if ((faultFlags & (1 << bit)) === 0) return []; + const reversed = bit === FAULT_DATA_BIT && faultCode === FAULT_DRIVE_REVERSED; + return [reversed ? "Drive reversed" : name]; + }); if (set.length === 0) return faultFlags === 0 ? undefined : `Fault 0x${faultFlags.toString(16)}`; return set.join(", "); } diff --git a/src/lib/data-state.ts b/src/lib/data-state.ts index b59db42..5bbc984 100644 --- a/src/lib/data-state.ts +++ b/src/lib/data-state.ts @@ -19,6 +19,8 @@ export interface Reason { export const FAULT_DATA = 7; /** `fault_flags` bit of the same fault. */ export const FAULT_DATA_BIT = 6; +/** `fault_code` of a shaft that turned against the drive, on the same bit. */ +export const FAULT_DRIVE_REVERSED = 8; const CONFIG_CORRUPT = 1 << 1; /** The stored plant stamp differs from the set the servo holds. */ diff --git a/src/lib/health.test.ts b/src/lib/health.test.ts index e1c5fe3..e73b41e 100644 --- a/src/lib/health.test.ts +++ b/src/lib/health.test.ts @@ -43,6 +43,16 @@ test("the data fault names the reason the servo refused the enable", () => { ); }); +test("drive_reversed names the reversed motor, whatever the data state says", () => { + const reversed = + "Drive reversed: the shaft turned against the drive, as it does when the motor leads are swapped or the drive polarity changed. Run osc cal to recalibrate. The motor stays off until torque is switched off and on again."; + const faultFlags = 1 << 6; + expect(statements({ ...clean, faultFlags }, { flags: 0, faultCode: 8 })[0]?.text).toBe(reversed); + expect(statements({ ...clean, faultFlags }, { flags: 1 << 4, faultCode: 8 })[0]?.text).toBe( + reversed, + ); +}); + test("every fault statement says how to clear it: torque off and on", () => { for (let bit = 0; bit < 8; bit++) { const s = statements({ ...clean, faultFlags: 1 << bit }); diff --git a/src/lib/health.ts b/src/lib/health.ts index cce5c47..a873021 100644 --- a/src/lib/health.ts +++ b/src/lib/health.ts @@ -1,5 +1,5 @@ import type { Health } from "@openservocore/client"; -import { FAULT_DATA_BIT, reasons, type DataState } from "./data-state"; +import { FAULT_DATA_BIT, FAULT_DRIVE_REVERSED, reasons, type DataState } from "./data-state"; export type Level = "fault" | "warn" | "ok"; @@ -19,6 +19,9 @@ const FAULTS: readonly string[] = [ "Closed loop refused: the servo's data state does not allow it.", ]; +const DRIVE_REVERSED = + "Drive reversed: the shaft turned against the drive, as it does when the motor leads are swapped or the drive polarity changed. Run osc cal to recalibrate."; + /** * Every latched fault clears the same way: the torque_enable 0->1 edge is the * only ack, and until it comes the motor stays off (firmware kernel/faults.rs). @@ -43,6 +46,7 @@ function plural(n: number, word: string): string { /** The data fault names the reason it refused, not just that it did. */ function faultLine(bit: number, data: DataState | undefined): string { + if (bit === FAULT_DATA_BIT && data?.faultCode === FAULT_DRIVE_REVERSED) return DRIVE_REVERSED; const [reason] = data === undefined ? [] : reasons(data.flags); if (bit === FAULT_DATA_BIT && reason !== undefined) return `Closed loop refused. ${reason.text}`; return FAULTS[bit] ?? `Unknown fault, bit ${bit}.`; diff --git a/src/lib/session.tsx b/src/lib/session.tsx index 25e4994..f5e64a0 100644 --- a/src/lib/session.tsx +++ b/src/lib/session.tsx @@ -468,7 +468,7 @@ function withHealth(servo: Servo, values: CardValues | undefined): Servo { if (values === undefined) return servo; return { ...servo, - fault: faultText(values.health.faultFlags), + fault: faultText(values.health.faultFlags, values.data.faultCode), unsaved: values.health.configDirty, calibrated: values.constants.calibrated.valid, }; diff --git a/src/routes/index.tsx b/src/routes/index.tsx index fef175f..48c0d48 100644 --- a/src/routes/index.tsx +++ b/src/routes/index.tsx @@ -141,7 +141,8 @@ function ServoCard({ onOpen: () => void; }) { const { ping } = servo; - const fault = values === undefined ? undefined : faultText(values.health.faultFlags); + const fault = + values === undefined ? undefined : faultText(values.health.faultFlags, values.data.faultCode); const calibrated = values?.constants.calibrated.valid; return (