robot-control 0.1.9

A Rust library for controlling DC motor robots via PCA9685 PWM boards over I2C
Documentation
const test = require('node:test');
const assert = require('node:assert/strict');
const { connectRobot } = require('../static/control.js');

function fixture(protocol = 'http:') {
    let socket;
    class WebSocket {
        static OPEN = 1;
        constructor(url) { this.url = url; this.readyState = 0; this.sent = []; socket = this; }
        send(message) { this.sent.push(JSON.parse(message)); }
        close() { this.readyState = 3; this.onclose(); }
    }
    let status;
    let resets = 0;
    const connection = connectRobot(WebSocket, { protocol, host: 'robot.local:8080' },
        value => { status = value; }, () => { resets++; });
    return { connection, socket, status: () => status, resets: () => resets };
}

test('URL preserves scheme/host/port; no disconnected sends or automatic demand replay', (t) => {
    const errors = t.mock.method(console, 'error', () => {});
    for (const protocol of ['http:', 'https:']) {
        const f = fixture(protocol);
        assert.equal(f.socket.url, `${protocol === 'https:' ? 'wss:' : 'ws:'}//robot.local:8080/ws`);
        assert.equal(f.connection.sendDriveCommand(0.5, 0.5), false);
        f.socket.readyState = 1;
        f.socket.onopen();
        assert.equal(f.socket.sent.length, 0);
        f.connection.sendDriveCommand(0.2, -0.3);
        assert.deepEqual(f.socket.sent, [{ left: 0.2, right: -0.3 }]);
        f.socket.close();
        assert.equal(f.connection.sendDriveCommand(0.5, 0.5), false);
        f.socket.readyState = 1;
        f.socket.onopen();
        assert.equal(f.socket.sent.length, 1);
    }
    assert.equal(errors.mock.callCount(), 0);
});

test('busy resets local demands and never retries; acceptance reports ownership', (t) => {
    const errors = t.mock.method(console, 'error', () => {});
    const f = fixture();
    f.socket.readyState = 1;
    f.socket.onmessage({ data: JSON.stringify({ code: 'busy', error: 'busy' }) });
    assert.match(f.status(), /Another client/);
    assert.equal(f.resets(), 1);
    assert.equal(f.socket.sent.length, 0);
    assert.equal(f.socket.readyState, 1);
    f.socket.onmessage({ data: JSON.stringify({ status: 'accepted' }) });
    assert.match(f.status(), /You control/);
    assert.equal(errors.mock.callCount(), 1);
    assert.equal(errors.mock.calls[0].arguments[0], '[robot-control] busy: busy');
});

test('overload and fault close the stream instead of retrying stale commands', (t) => {
    const errors = t.mock.method(console, 'error', () => {});
    for (const code of ['queue_full', 'fault', 'shutdown']) {
        const f = fixture();
        f.socket.readyState = 1;
        f.socket.onmessage({ data: JSON.stringify({ code, error: 'Rejected command' }) });
        assert.equal(f.socket.readyState, 3);
        assert.match(f.status(), /Rejected command.*Disconnected/);
        assert.equal(f.socket.sent.length, 0);
    }
    assert.deepEqual(errors.mock.calls.map(call => call.arguments[0]), [
        '[robot-control] queue_full: Rejected command',
        '[robot-control] fault: Rejected command',
        '[robot-control] shutdown: Rejected command',
    ]);
});

test('superseded commands stay quiet; malformed replies and connection errors are logged', (t) => {
    const errors = t.mock.method(console, 'error', () => {});
    const f = fixture();
    f.socket.readyState = 1;
    f.socket.onmessage({ data: JSON.stringify({ code: 'superseded', error: 'stopped' }) });
    assert.equal(f.socket.readyState, 1);
    assert.equal(errors.mock.callCount(), 0);
    f.socket.onmessage({ data: 'not json' });
    assert.equal(f.socket.readyState, 3);
    assert.match(f.status(), /Invalid server response/);
    assert.equal(errors.mock.callCount(), 1);
    assert.match(errors.mock.calls[0].arguments[0], /Invalid server response/);
    const event = { type: 'error' };
    f.socket.onerror(event);
    assert.equal(errors.mock.callCount(), 2);
    assert.equal(errors.mock.calls[1].arguments[1], event);
});