blanchon's picture
Mostly UI Update
18b0fa5
import type {
SlaveDriver,
DriverJointState,
ConnectionStatus,
RobotCommand,
MockSlaveConfig,
StateUpdateCallback,
StatusChangeCallback,
UnsubscribeFn
} from "$lib/types/robotDriver";
import { getRobotPollingConfig } from "$lib/configs/performanceConfig";
/**
* Mock Slave Driver
* Simulates a physical robot for testing slave command execution
*/
export class MockSlave implements SlaveDriver {
readonly type = "slave" as const;
readonly id: string;
readonly name: string;
private _status: ConnectionStatus = { isConnected: false };
private config: MockSlaveConfig;
// Joint states
private jointStates: DriverJointState[] = [];
// Event callbacks
private stateCallbacks: StateUpdateCallback[] = [];
private statusCallbacks: StatusChangeCallback[] = [];
// Simulation control
private simulationIntervalId?: number;
constructor(config: MockSlaveConfig, initialJointStates: DriverJointState[]) {
this.config = config;
this.id = `mock-slave-${Date.now()}`;
this.name = `Mock Slave Robot`;
// Initialize joint states
this.jointStates = initialJointStates.map((state) => ({
...state,
virtualValue: state.virtualValue,
realValue: state.virtualValue // Mock starts with perfect sync
}));
console.log(`Created MockSlave with ${this.jointStates.length} joints`);
}
get status(): ConnectionStatus {
return this._status;
}
async connect(): Promise<void> {
console.log(`Connecting ${this.name}...`);
// Simulate connection delay
if (this.config.simulateLatency) {
await new Promise((resolve) => setTimeout(resolve, this.config.simulateLatency));
}
this._status = { isConnected: true, lastConnected: new Date() };
this.notifyStatusChange();
// Start simulation loop
this.startSimulation();
console.log(`${this.name} connected`);
}
async disconnect(): Promise<void> {
console.log(`Disconnecting ${this.name}...`);
this.stopSimulation();
this._status = { isConnected: false };
this.notifyStatusChange();
console.log(`${this.name} disconnected`);
}
async executeCommand(command: RobotCommand): Promise<void> {
if (!this._status.isConnected) {
throw new Error("Cannot execute command: slave not connected");
}
// Simulate command error if configured
if (this.config.simulateErrors && Math.random() < 0.1) {
throw new Error("Simulated command execution error");
}
console.log(`MockSlave executing command with ${command.joints.length} joint updates`);
// Apply joint updates
for (const jointUpdate of command.joints) {
const joint = this.jointStates.find((j) => j.name === jointUpdate.name);
if (joint) {
joint.virtualValue = jointUpdate.value;
// Mock robot has perfect response (real = virtual)
joint.realValue = jointUpdate.value;
}
}
// Simulate response delay
if (this.config.responseDelay) {
await new Promise((resolve) => setTimeout(resolve, this.config.responseDelay));
}
// Notify state update
this.notifyStateUpdate();
}
async executeCommands(commands: RobotCommand[]): Promise<void> {
console.log(`MockSlave executing batch of ${commands.length} commands`);
for (const command of commands) {
await this.executeCommand(command);
// Small delay between commands
if (commands.length > 1) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
}
async readJointStates(): Promise<DriverJointState[]> {
if (!this._status.isConnected) {
throw new Error("Cannot read states: slave not connected");
}
// Simulate read latency
if (this.config.simulateLatency) {
await new Promise((resolve) => setTimeout(resolve, this.config.simulateLatency));
}
return [...this.jointStates];
}
async writeJointState(jointName: string, value: number): Promise<void> {
const command: RobotCommand = {
timestamp: Date.now(),
joints: [{ name: jointName, value }]
};
await this.executeCommand(command);
}
async writeJointStates(updates: { jointName: string; value: number }[]): Promise<void> {
const command: RobotCommand = {
timestamp: Date.now(),
joints: updates.map((update) => ({ name: update.jointName, value: update.value }))
};
await this.executeCommand(command);
}
// Event subscription methods
onStateUpdate(callback: StateUpdateCallback): UnsubscribeFn {
this.stateCallbacks.push(callback);
return () => {
const index = this.stateCallbacks.indexOf(callback);
if (index >= 0) {
this.stateCallbacks.splice(index, 1);
}
};
}
onStatusChange(callback: StatusChangeCallback): UnsubscribeFn {
this.statusCallbacks.push(callback);
return () => {
const index = this.statusCallbacks.indexOf(callback);
if (index >= 0) {
this.statusCallbacks.splice(index, 1);
}
};
}
// Private methods
private startSimulation(): void {
if (this.simulationIntervalId) {
clearInterval(this.simulationIntervalId);
}
// Use performance config for update rate
this.simulationIntervalId = setInterval(() => {
this.notifyStateUpdate();
}, getRobotPollingConfig().STATE_UPDATE_INTERVAL_MS);
}
private stopSimulation(): void {
if (this.simulationIntervalId) {
clearInterval(this.simulationIntervalId);
this.simulationIntervalId = undefined;
}
}
private notifyStateUpdate(): void {
this.stateCallbacks.forEach((callback) => {
try {
callback([...this.jointStates]);
} catch (error) {
console.error("Error in state update callback:", error);
}
});
}
private notifyStatusChange(): void {
this.statusCallbacks.forEach((callback) => {
try {
callback(this._status);
} catch (error) {
console.error("Error in status change callback:", error);
}
});
}
}