Main engine
Two columns for the engine, thrust from astern to ahead and engine speed, each with its setpoint.
On this page
Slow ahead past the breakwater.
Code
import { AdviceType, InstrumentState, MainEngine, Priority, Readout } from "@bosunkit/instruments";
import type { LinearAdvice } from "@bosunkit/instruments";
import { useEffect, useState } from "react";
import type { CSSProperties, Dispatch, ReactElement, SetStateAction } from "react";
export const description: string =
"Althea's Yanmar backing into a berth in Port de Sóller on a one-second feed: the lever goes ahead, through neutral to astern and a kick ahead into the caution range, with the thrust following its setpoint and the governor reporting when the speed is reached, beside the tender outboard off and starting.";
/** Feed steps for one maneuver. */
const CYCLE: number = 30;
/** Full engine speed of the Yanmar 4JH110, for the percent-to-rpm readout. */
const MAX_RPM: number = 3200;
/** How far the thrust moves toward its setpoint each second, in percentage points. */
const THRUST_RATE: number = 12;
const THRUST_ADVICE: LinearAdvice[] = [
{ min: 85, max: 100, type: AdviceType.caution, hinted: false },
{ min: -100, max: -70, type: AdviceType.caution, hinted: true },
{ min: 30, max: 60, type: AdviceType.advice, hinted: true },
];
const NAME_STYLE: CSSProperties = { fontSize: 12, color: "var(--element-neutral-color)" };
interface Lever {
thrust: number;
speed: number;
touching: boolean;
note: string;
}
/** The skipper's lever through the maneuver: the thrust and engine speed it asks for, and what is happening. */
function leverAt(step: number): Lever {
if (step < 5) {
return { thrust: 40, speed: 55, touching: false, note: "Slow ahead past the breakwater." };
}
if (step < 8) {
return { thrust: 0, speed: 25, touching: true, note: "Lever to neutral." };
}
if (step < 12) {
return { thrust: 0, speed: 25, touching: false, note: "Neutral, gliding toward the berth." };
}
if (step < 15) {
return { thrust: -45, speed: 50, touching: true, note: "Lever to half astern." };
}
if (step < 22) {
return { thrust: -45, speed: 50, touching: false, note: "Half astern, stern walking to port." };
}
if (step < 25) {
return {
thrust: 90,
speed: 85,
touching: false,
note: "Kick ahead at 90 % to stop the stern: keep it short.",
};
}
return { thrust: 0, speed: 25, touching: false, note: "Neutral, stern lines ashore." };
}
interface Engine {
step: number;
thrust: number;
speed: number;
}
function towards(current: number, target: number): number {
return current + Math.max(-THRUST_RATE, Math.min(THRUST_RATE, target - current));
}
export default function Showcase(): ReactElement {
const [engine, setEngine]: [Engine, Dispatch<SetStateAction<Engine>>] = useState<Engine>({
step: 0,
thrust: 40,
speed: 55,
});
useEffect((): (() => void) => {
const timer: number = window.setInterval((): void => {
setEngine((previous: Engine): Engine => {
const step: number = (previous.step + 1) % CYCLE;
const lever: Lever = leverAt(step);
return {
step,
thrust: lever.touching ? previous.thrust : towards(previous.thrust, lever.thrust),
speed: towards(previous.speed, lever.speed),
};
});
}, 1000);
return (): void => {
window.clearInterval(timer);
};
}, []);
const lever: Lever = leverAt(engine.step);
const governorSettled: boolean = Math.abs(engine.speed - lever.speed) < 4;
const direction: string = engine.thrust > 0 ? "ahead" : engine.thrust < 0 ? "astern" : "neutral";
return (
<div style={{ display: "flex", flexWrap: "wrap", gap: 24, alignItems: "flex-start", justifyContent: "center" }}>
<div style={{ display: "grid", gap: 8, justifyItems: "center", width: 280 }}>
<span style={NAME_STYLE}>Main engine, Yanmar 4JH110</span>
<div style={{ width: 260, height: 260 }}>
<MainEngine
thrust={engine.thrust}
thrustSetpoint={lever.thrust}
thrustTouching={lever.touching}
autoAtThrustSetpointDeadband={3}
thrustSetpointAtZeroDeadband={2}
speed={engine.speed}
speedSetpoint={lever.speed}
speedTouching={lever.touching}
autoAtSpeedSetpoint={false}
atSpeedSetpoint={governorSettled}
speedSetpointAtZeroDeadband={2}
thrustAdvices={THRUST_ADVICE}
priority={Priority.enhanced}
/>
</div>
<div style={{ display: "flex", gap: 20 }}>
<Readout label={`Thrust, ${direction}`} value={Math.abs(engine.thrust)} unit="%" maxDigits={3} />
<Readout label="Engine speed" value={(engine.speed / 100) * MAX_RPM} unit="rpm" maxDigits={4} />
</div>
<p style={{ ...NAME_STYLE, margin: 0, textAlign: "center" }}>{lever.note}</p>
</div>
<div style={{ display: "grid", gap: 16 }}>
<div style={{ display: "grid", gap: 4, justifyItems: "center" }}>
<span style={NAME_STYLE}>Tender outboard, off</span>
<div style={{ width: 130, height: 130 }}>
<MainEngine state={InstrumentState.off} thrust={0} speed={0} />
</div>
</div>
<div style={{ display: "grid", gap: 4, justifyItems: "center" }}>
<span style={NAME_STYLE}>Tender outboard, engine unit starting</span>
<div style={{ width: 130, height: 130 }}>
<MainEngine state={InstrumentState.loading} thrust={0} speed={0} />
</div>
</div>
</div>
</div>
);
}
The main engine instrument shows thrust and engine speed side by side. The thrust column has its zero at the center:
ahead fills upward and astern fills downward, from −100 to +100 percent. The speed column fills from the base, from 0 to
100 percent of RPM. Each column has its own setpoint chevrons and counts a setpoint as reached when the value is within
its deadband. thrustAdvices marks thrust ranges and turns one to triggered when the thrust setpoint enters it.
The instrument fills the box it is given, so the example sets one.
Examples
Engine
Code
import { MainEngine } from "@bosunkit/instruments";
import type { ReactElement } from "react";
export const description: string = "Thrust at 65 percent ahead with a setpoint at 70, beside a speed column at zero.";
export default function Engine(): ReactElement {
return (
<div style={{ width: 200, height: 200 }}>
<MainEngine thrust={65} thrustSetpoint={70} />
</div>
);
}
Percentage, command and feedback
The speed column is a percentage of RPM, not raw revolutions per minute. Normalize engine feedback against the appropriate reference before rendering and show the actual RPM in a companion reading when needed. Thrust and speed can have different sources and ages.
Keep measured values separate from their accepted and draft setpoints. Deadband styling indicates a numerical comparison, not confirmation of an engine command or a safe maneuver. Thrust advice reacts to the supplied setpoint, so explain whether a highlighted range describes requested thrust or observed thrust. The application owns commands, interlocks, loading and off-state decisions.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| thrust | number | undefined | 0 | Thrust in percent, −100 (full astern) to +100 (full ahead). |
| thrustSetpoint | number | undefined | – | Requested thrust, in percent from −100 (full astern) to +100 (full ahead). |
| thrustTouching | boolean | undefined | false | The crew is adjusting the thrust setpoint. The setpoint draws in its focus state and is not counted as reached. |
| atThrustSetpoint | boolean | undefined | false | Manual thrust at-setpoint flag; used when autoAtThrustSetpoint is false. |
| speed | number | undefined | 0 | Engine speed in percent RPM, 0–100. |
| speedSetpoint | number | undefined | – | Requested engine speed, in percent RPM from 0 to 100. |
| speedTouching | boolean | undefined | false | The crew is adjusting the speed setpoint. The setpoint draws in its focus state and is not counted as reached. |
| atSpeedSetpoint | boolean | undefined | false | Manual speed at-setpoint flag; used when autoAtSpeedSetpoint is false. |
| autoAtThrustSetpoint | boolean | undefined | true | Counts the thrust setpoint as reached when thrust is closer to it than autoAtThrustSetpointDeadband. When false,
atThrustSetpoint decides. |
| autoAtSpeedSetpoint | boolean | undefined | true | Counts the speed setpoint as reached when speed is closer to it than autoAtSpeedSetpointDeadband. When false,
atSpeedSetpoint decides. |
| autoAtThrustSetpointDeadband | number | undefined | 1 | Distance from the thrust setpoint, in percentage points, inside which thrust counts as reached. |
| autoAtSpeedSetpointDeadband | number | undefined | 1 | Distance from the speed setpoint, in percentage points, inside which speed counts as reached. |
| thrustSetpointAtZeroDeadband | number | undefined | 0.5 | Distance from zero, in percentage points, inside which the thrust setpoint snaps to the zero line. |
| speedSetpointAtZeroDeadband | number | undefined | 0.5 | Distance from zero, in percentage points, inside which the speed setpoint snaps to the zero line. |
| state | InstrumentState | undefined | InstrumentState.active | Active, loading or off. Loading and off draw the columns without a value box or tickmarks. |
| priority | Priority | undefined | Priority.regular | Regular or enhanced palette of the thrust and speed columns. |
| thrustAdvices | LinearAdvice[] | undefined | [] | Thrust advice zones; triggered when the thrust SETPOINT enters the range. |
| className | string | undefined | – | Class name added to the root element. |
Related
- Depth actual: another linear instrument.
- Instruments: the other instruments.