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
14 changes: 10 additions & 4 deletions src/lib/bus/spans.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
10 changes: 7 additions & 3 deletions src/lib/bus/spans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(", ");
}
Expand Down
2 changes: 2 additions & 0 deletions src/lib/data-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
10 changes: 10 additions & 0 deletions src/lib/health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
6 changes: 5 additions & 1 deletion src/lib/health.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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).
Expand All @@ -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}.`;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
3 changes: 2 additions & 1 deletion src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Link
Expand Down
Loading