diff --git a/src/RosLib.ts b/src/RosLib.ts index ea4b4f33e..c3030afa6 100644 --- a/src/RosLib.ts +++ b/src/RosLib.ts @@ -78,4 +78,44 @@ export { isRosbridgeAdvertiseMessage, type RosbridgeSubscribeMessage, isRosbridgeSubscribeMessage, + type RosbridgeAuthMessage, + type RosbridgeStatusMessage, + isRosbridgeStatusMessage, + type RosbridgeSetStatusLevelMessage, + isRosbridgeSetStatusLevelMessage, + type RosbridgeFragmentMessage, + isRosbridgeFragmentMessage, + type RosbridgePngMessage, + isRosbridgePngMessage, + type RosbridgeUnadvertiseMessage, + isRosbridgeUnadvertiseMessage, + type RosbridgePublishMessage, + isRosbridgePublishMessage, + type RosbridgeUnsubscribeMessage, + isRosbridgeUnsubscribeMessage, + type RosbridgeAdvertiseServiceMessage, + isRosbridgeAdvertiseServiceMessage, + type RosbridgeUnadvertiseServiceMessage, + isRosbridgeUnadvertiseServiceMessage, + type RosbridgeCallServiceMessage, + isRosbridgeCallServiceMessage, + type RosbridgeServiceResponseMessage, + isRosbridgeServiceResponseMessage, + type RosbridgeAdvertiseActionMessage, + isRosbridgeAdvertiseActionMessage, + type RosbridgeUnadvertiseActionMessage, + isRosbridgeUnadvertiseActionMessage, + type RosbridgeSendActionGoalMessage, + isRosbridgeSendActionGoalMessage, + type RosbridgeCancelActionGoalMessage, + isRosbridgeCancelActionGoalMessage, + type RosbridgeActionFeedbackMessage, + isRosbridgeActionFeedbackMessage, + type RosbridgeActionResultMessage, + isRosbridgeActionResultMessage, + type RosbridgeMessageBase, + type FailedRosbridgeActionResultMessage, + type SuccessfulRosbridgeActionResultMessage, + type FailedRosbridgeServiceResponseMessage, + type SuccessfulRosbridgeServiceResponseMessage, } from "./types/protocol.js"; diff --git a/src/core/Action.ts b/src/core/Action.ts index 1e9a0e0b1..adb792370 100644 --- a/src/core/Action.ts +++ b/src/core/Action.ts @@ -82,15 +82,14 @@ export default class Action< } }); - const call = { + this.ros.callOnConnection({ op: "send_action_goal", id: actionGoalId, action: this.name, action_type: this.actionType, args: goal, feedback: true, - }; - this.ros.callOnConnection(call); + }); return actionGoalId; } @@ -101,12 +100,11 @@ export default class Action< * @param id - The ID of the action goal to cancel. */ cancelGoal(id: string) { - const call = { + this.ros.callOnConnection({ op: "cancel_action_goal", id: id, action: this.name, - }; - this.ros.callOnConnection(call); + }); } /** @@ -200,13 +198,12 @@ export default class Action< * @param feedback - The feedback to send. */ sendFeedback(id: string, feedback: TFeedback) { - const call = { + this.ros.callOnConnection({ op: "action_feedback", id: id, action: this.name, values: feedback, - }; - this.ros.callOnConnection(call); + }); } /** @@ -216,15 +213,14 @@ export default class Action< * @param result - The result to set. */ setSucceeded(id: string, result: TResult) { - const call = { + this.ros.callOnConnection({ op: "action_result", id: id, action: this.name, values: result, status: GoalStatus.STATUS_SUCCEEDED, result: true, - }; - this.ros.callOnConnection(call); + }); } /** @@ -234,15 +230,14 @@ export default class Action< * @param result - The result to set. */ setCanceled(id: string, result: TResult) { - const call = { + this.ros.callOnConnection({ op: "action_result", id: id, action: this.name, values: result, status: GoalStatus.STATUS_CANCELED, result: true, - }; - this.ros.callOnConnection(call); + }); } /** @@ -251,13 +246,12 @@ export default class Action< * @param id - The action goal ID. */ setFailed(id: string) { - const call = { + this.ros.callOnConnection({ op: "action_result", id: id, action: this.name, status: GoalStatus.STATUS_ABORTED, result: false, - }; - this.ros.callOnConnection(call); + }); } } diff --git a/src/core/Ros.ts b/src/core/Ros.ts index 636452c73..bf3c02ae8 100644 --- a/src/core/Ros.ts +++ b/src/core/Ros.ts @@ -5,6 +5,7 @@ import type { RosbridgeMessage, + RosbridgeMessageBase, RosbridgeSetStatusLevelMessage, } from "../types/protocol.js"; import { @@ -121,7 +122,7 @@ export default class Ros extends EventEmitter< this.transport?.close(); } - private handleMessage(message: RosbridgeMessage) { + private handleMessage(message: RosbridgeMessageBase) { if (isRosbridgePublishMessage(message)) { this.emit(message.topic, message); } else if (isRosbridgeServiceResponseMessage(message)) { @@ -165,12 +166,12 @@ export default class Ros extends EventEmitter< client: string, dest: string, rand: string, - t: object, + t: number, level: string, - end: object, + end: number, ) { - // create the request - const auth = { + // send the request + this.callOnConnection({ op: "auth", mac: mac, client: client, @@ -179,19 +180,14 @@ export default class Ros extends EventEmitter< t: t, level: level, end: end, - }; - // send the request - this.callOnConnection(auth); + }); } /** * Sends the message to the transport. * If not connected, queues the message to send once reconnected. */ - // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- to broaden argument type to any RosbridgeMessage variant - public callOnConnection( - message: T, - ) { + public callOnConnection(message: RosbridgeMessage) { if (this.isConnected()) { this.transport?.send(message); } else { @@ -207,7 +203,10 @@ export default class Ros extends EventEmitter< * @param level - Status level (none, error, warning, info). * @param [id] - Operation ID to change status level on. */ - public setStatusLevel(level: string, id?: string) { + public setStatusLevel( + level: RosbridgeSetStatusLevelMessage["level"], + id?: string, + ) { const levelMsg: RosbridgeSetStatusLevelMessage = { op: "set_level", level, diff --git a/src/core/Service.ts b/src/core/Service.ts index 5a681938e..1077e4853 100644 --- a/src/core/Service.ts +++ b/src/core/Service.ts @@ -4,10 +4,7 @@ */ import { EventEmitter } from "eventemitter3"; -import type { - RosbridgeMessage, - RosbridgeServiceResponseMessage, -} from "../types/protocol.ts"; +import type { RosbridgeMessage } from "../types/protocol.ts"; import { isRosbridgeCallServiceMessage, isRosbridgeServiceResponseMessage, @@ -88,16 +85,13 @@ export default class Service extends EventEmitter { } }); - const call = { + this.ros.callOnConnection({ op: "call_service", id: serviceCallId, service: this.name, - type: this.serviceType, args: request, timeout: timeout, - }; - - this.ros.callOnConnection(call); + }); } /** * Advertise the service. This turns the Service object from a client @@ -125,7 +119,8 @@ export default class Service extends EventEmitter { `Invalid message received on service channel: ${JSON.stringify(rosbridgeRequest)}`, ); } - const response = {}; + // @ts-expect-error -- TypeScript doesn't have a way to handle the out-parameter model used here. + const response: TResponse = {}; let success: boolean; try { success = callback(rosbridgeRequest.args, response); @@ -140,14 +135,14 @@ export default class Service extends EventEmitter { values: response, result: success, id: rosbridgeRequest.id, - } satisfies RosbridgeServiceResponseMessage>); + }); } else { this.ros.callOnConnection({ op: "service_response", service: this.name, result: success, id: rosbridgeRequest.id, - } satisfies RosbridgeServiceResponseMessage>); + }); } }; @@ -247,7 +242,7 @@ export default class Service extends EventEmitter { result: true, values: await callback(rosbridgeRequest.args), id: rosbridgeRequest.id, - } satisfies RosbridgeServiceResponseMessage); + }); } catch (err) { this.ros.callOnConnection({ op: "service_response", @@ -255,7 +250,7 @@ export default class Service extends EventEmitter { result: false, values: String(err), id: rosbridgeRequest.id, - } satisfies RosbridgeServiceResponseMessage); + }); } })().catch(console.error); }; diff --git a/src/core/Topic.ts b/src/core/Topic.ts index ab291c874..0f088c1d3 100644 --- a/src/core/Topic.ts +++ b/src/core/Topic.ts @@ -258,14 +258,12 @@ export default class Topic extends EventEmitter<{ this.advertise(); } - const call = { + this.ros.callOnConnection({ op: "publish", id: `publish:${this.name}:${uuidv4()}`, topic: this.name, msg: message, - latch: this.latch, - }; - this.ros.callOnConnection(call); + }); } /** diff --git a/src/core/transport/Transport.ts b/src/core/transport/Transport.ts index b3fe29fab..f349f9336 100644 --- a/src/core/transport/Transport.ts +++ b/src/core/transport/Transport.ts @@ -3,6 +3,7 @@ import type { RosbridgePngMessage, RosbridgeMessage, RosbridgeFragmentMessage, + RosbridgeMessageBase, } from "../../types/protocol.js"; import { isRosbridgeFragmentMessage, @@ -60,7 +61,7 @@ export abstract class AbstractTransport open: [TransportEvent]; close: [TransportEvent]; error: [TransportEvent]; - message: [RosbridgeMessage]; + message: [RosbridgeMessageBase]; }> implements ITransport { @@ -114,7 +115,7 @@ export abstract class AbstractTransport * If the message is a PNG, it is decompressed and reprocessed. * Otherwise, the message is emitted. */ - private handleRosbridgeMessage(message: RosbridgeMessage) { + private handleRosbridgeMessage(message: RosbridgeMessageBase) { if (isRosbridgeFragmentMessage(message)) { this.handleRosbridgeFragmentMessage(message); } else if (isRosbridgePngMessage(message)) { diff --git a/src/types/protocol.ts b/src/types/protocol.ts index 390fc79e3..d533da4a2 100644 --- a/src/types/protocol.ts +++ b/src/types/protocol.ts @@ -2,13 +2,13 @@ * https://github.com/RobotWebTools/rosbridge_suite/blob/ros2/ROSBRIDGE_PROTOCOL.md */ -export interface RosbridgeMessage { +export interface RosbridgeMessageBase { op: string; } export function isRosbridgeMessage( message: unknown, -): message is RosbridgeMessage { +): message is RosbridgeMessageBase { return ( message instanceof Object && "op" in message && @@ -16,7 +16,18 @@ export function isRosbridgeMessage( ); } -export interface RosbridgeStatusMessage extends RosbridgeMessage { +export interface RosbridgeAuthMessage extends RosbridgeMessageBase { + op: "auth"; + mac: string; + client: string; + dest: string; + rand: string; + t: number; + level: string; + end: number; +} + +export interface RosbridgeStatusMessage extends RosbridgeMessageBase { op: "status"; id?: string; level: string; @@ -24,24 +35,24 @@ export interface RosbridgeStatusMessage extends RosbridgeMessage { } export function isRosbridgeStatusMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeStatusMessage { return message.op === "status"; } -export interface RosbridgeSetStatusLevelMessage extends RosbridgeMessage { +export interface RosbridgeSetStatusLevelMessage extends RosbridgeMessageBase { op: "set_level"; id?: string; - level: string; + level: "info" | "warning" | "error" | "none"; } export function isRosbridgeSetStatusLevelMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeSetStatusLevelMessage { return message.op === "set_level"; } -export interface RosbridgeFragmentMessage extends RosbridgeMessage { +export interface RosbridgeFragmentMessage extends RosbridgeMessageBase { op: "fragment"; id: string; data: string; @@ -50,12 +61,12 @@ export interface RosbridgeFragmentMessage extends RosbridgeMessage { } export function isRosbridgeFragmentMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeFragmentMessage { return message.op === "fragment"; } -export interface RosbridgePngMessage extends RosbridgeMessage { +export interface RosbridgePngMessage extends RosbridgeMessageBase { op: "png"; id?: string; data: string; @@ -64,12 +75,12 @@ export interface RosbridgePngMessage extends RosbridgeMessage { } export function isRosbridgePngMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgePngMessage { return message.op === "png"; } -export interface RosbridgeAdvertiseMessage extends RosbridgeMessage { +export interface RosbridgeAdvertiseMessage extends RosbridgeMessageBase { op: "advertise"; id?: string; type: string; @@ -79,25 +90,25 @@ export interface RosbridgeAdvertiseMessage extends RosbridgeMessage { } export function isRosbridgeAdvertiseMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeAdvertiseMessage { return message.op === "advertise"; } -export interface RosbridgeUnadvertiseMessage extends RosbridgeMessage { +export interface RosbridgeUnadvertiseMessage extends RosbridgeMessageBase { op: "unadvertise"; id?: string; topic: string; } export function isRosbridgeUnadvertiseMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeUnadvertiseMessage { return message.op === "unadvertise"; } export interface RosbridgePublishMessage - extends RosbridgeMessage { + extends RosbridgeMessageBase { op: "publish"; id?: string; topic: string; @@ -105,12 +116,12 @@ export interface RosbridgePublishMessage } export function isRosbridgePublishMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgePublishMessage { return message.op === "publish"; } -export interface RosbridgeSubscribeMessage extends RosbridgeMessage { +export interface RosbridgeSubscribeMessage extends RosbridgeMessageBase { op: "subscribe"; id?: string; topic: string; @@ -122,53 +133,54 @@ export interface RosbridgeSubscribeMessage extends RosbridgeMessage { } export function isRosbridgeSubscribeMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeSubscribeMessage { return message.op === "subscribe"; } -export interface RosbridgeUnsubscribeMessage extends RosbridgeMessage { +export interface RosbridgeUnsubscribeMessage extends RosbridgeMessageBase { op: "unsubscribe"; id?: string; topic: string; } export function isRosbridgeUnsubscribeMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeUnsubscribeMessage { return message.op === "unsubscribe"; } -export interface RosbridgeAdvertiseServiceMessage extends RosbridgeMessage { +export interface RosbridgeAdvertiseServiceMessage extends RosbridgeMessageBase { op: "advertise_service"; type: string; service: string; } export function isRosbridgeAdvertiseServiceMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeAdvertiseServiceMessage { return message.op === "advertise_service"; } -export interface RosbridgeUnadvertiseServiceMessage extends RosbridgeMessage { +export interface RosbridgeUnadvertiseServiceMessage + extends RosbridgeMessageBase { op: "unadvertise_service"; service: string; } export function isRosbridgeUnadvertiseServiceMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeUnadvertiseServiceMessage { return message.op === "unadvertise_service"; } -export interface RosbridgeCallServiceMessage - extends RosbridgeMessage { +export interface RosbridgeCallServiceMessage + extends RosbridgeMessageBase { op: "call_service"; id?: string; service: string; /** - * @todo this should be deeply partial when *outgoing*, because rosbridge will "fill in the blanks", + * TODO this should be deeply partial when *outgoing*, because rosbridge will "fill in the blanks", * but it's not partial when *incoming* - need to figure out a way to represent this. */ args: TArgs; @@ -178,66 +190,66 @@ export interface RosbridgeCallServiceMessage } export function isRosbridgeCallServiceMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeCallServiceMessage { return message.op === "call_service"; } -interface BaseRosbridgeServiceResponseMessage extends RosbridgeMessage { +interface BaseRosbridgeServiceResponseMessage extends RosbridgeMessageBase { op: "service_response"; id?: string; service: string; - result: boolean; } /** If the service call failed, `values` will be a string error message. */ -interface FailedRosbridgeServiceResponseMessage +export interface FailedRosbridgeServiceResponseMessage extends BaseRosbridgeServiceResponseMessage { values?: string; result: false; } -interface SuccessfulRosbridgeServiceResponseMessage +export interface SuccessfulRosbridgeServiceResponseMessage extends BaseRosbridgeServiceResponseMessage { values: TValues; result: true; } -export type RosbridgeServiceResponseMessage = +export type RosbridgeServiceResponseMessage = | FailedRosbridgeServiceResponseMessage | SuccessfulRosbridgeServiceResponseMessage; export function isRosbridgeServiceResponseMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeServiceResponseMessage { return message.op === "service_response"; } -export interface RosbridgeAdvertiseActionMessage extends RosbridgeMessage { +export interface RosbridgeAdvertiseActionMessage extends RosbridgeMessageBase { op: "advertise_action"; type: string; action: string; } export function isRosbridgeAdvertiseActionMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeAdvertiseActionMessage { return message.op === "advertise_action"; } -export interface RosbridgeUnadvertiseActionMessage extends RosbridgeMessage { +export interface RosbridgeUnadvertiseActionMessage + extends RosbridgeMessageBase { op: "unadvertise_action"; action: string; } export function isRosbridgeUnadvertiseActionMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeUnadvertiseActionMessage { return message.op === "unadvertise_action"; } export interface RosbridgeSendActionGoalMessage - extends RosbridgeMessage { + extends RosbridgeMessageBase { op: "send_action_goal"; id: string; action: string; @@ -249,25 +261,25 @@ export interface RosbridgeSendActionGoalMessage } export function isRosbridgeSendActionGoalMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeSendActionGoalMessage { return message.op === "send_action_goal"; } -export interface RosbridgeCancelActionGoalMessage extends RosbridgeMessage { +export interface RosbridgeCancelActionGoalMessage extends RosbridgeMessageBase { op: "cancel_action_goal"; id: string; action: string; } export function isRosbridgeCancelActionGoalMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeCancelActionGoalMessage { return message.op === "cancel_action_goal"; } export interface RosbridgeActionFeedbackMessage - extends RosbridgeMessage { + extends RosbridgeMessageBase { op: "action_feedback"; id: string; action: string; @@ -275,25 +287,25 @@ export interface RosbridgeActionFeedbackMessage } export function isRosbridgeActionFeedbackMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeActionFeedbackMessage { return message.op === "action_feedback"; } -interface RosbridgeActionResultMessageBase extends RosbridgeMessage { +interface RosbridgeActionResultMessageBase extends RosbridgeMessageBase { op: "action_result"; id: string; action: string; status: number; } -interface FailedRosbridgeActionResultMessage +export interface FailedRosbridgeActionResultMessage extends RosbridgeActionResultMessageBase { result: false; values?: string; } -interface SuccessfulRosbridgeActionResultMessage +export interface SuccessfulRosbridgeActionResultMessage extends RosbridgeActionResultMessageBase { values: TResultValues; result: true; @@ -304,12 +316,12 @@ export type RosbridgeActionResultMessage = | SuccessfulRosbridgeActionResultMessage; export function isRosbridgeActionResultMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeActionResultMessage { return message.op === "action_result"; } -export interface RosbridgeActionStatusMessage extends RosbridgeMessage { +export interface RosbridgeActionStatusMessage extends RosbridgeMessageBase { op: "action_status"; id: string; action: string; @@ -317,7 +329,29 @@ export interface RosbridgeActionStatusMessage extends RosbridgeMessage { } export function isRosbridgeActionStatusMessage( - message: RosbridgeMessage, + message: RosbridgeMessageBase, ): message is RosbridgeActionStatusMessage { return message.op === "action_status"; } + +export type RosbridgeMessage = + | RosbridgeAuthMessage + | RosbridgeStatusMessage + | RosbridgeSetStatusLevelMessage + | RosbridgeFragmentMessage + | RosbridgePngMessage + | RosbridgeAdvertiseMessage + | RosbridgeUnadvertiseMessage + | RosbridgePublishMessage + | RosbridgeSubscribeMessage + | RosbridgeUnsubscribeMessage + | RosbridgeAdvertiseServiceMessage + | RosbridgeUnadvertiseServiceMessage + | RosbridgeCallServiceMessage + | RosbridgeServiceResponseMessage + | RosbridgeAdvertiseActionMessage + | RosbridgeUnadvertiseActionMessage + | RosbridgeSendActionGoalMessage + | RosbridgeCancelActionGoalMessage + | RosbridgeActionFeedbackMessage + | RosbridgeActionResultMessage; diff --git a/test/ros.test.ts b/test/ros.test.ts index 45e36e68b..6937c49f8 100644 --- a/test/ros.test.ts +++ b/test/ros.test.ts @@ -229,10 +229,13 @@ describe("Ros", function () { publishMockTransportEvent("open", new Event("open")); const rosOnceSpy = vi.spyOn(ros, "once"); - ros.callOnConnection({ op: "test" }); + ros.callOnConnection({ op: "set_level", level: "info" }); expect(rosOnceSpy).not.toHaveBeenCalled(); - expect(mockTransport.send).toHaveBeenCalledWith({ op: "test" }); + expect(mockTransport.send).toHaveBeenCalledWith({ + op: "set_level", + level: "info", + }); }); it("queues the message to send once connected", async () => { @@ -242,7 +245,7 @@ describe("Ros", function () { // When disconnected, the message is queued to send const rosOnceSpy = vi.spyOn(ros, "once"); - ros.callOnConnection({ op: "test" }); + ros.callOnConnection({ op: "set_level", level: "info" }); expect(rosOnceSpy).toHaveBeenCalledWith("open", expect.any(Function)); expect(mockTransport.send).not.toHaveBeenCalled(); @@ -251,7 +254,10 @@ describe("Ros", function () { await ros.connect(mockRosUrl); publishMockTransportEvent("open", new Event("open")); - expect(mockTransport.send).toHaveBeenCalledWith({ op: "test" }); + expect(mockTransport.send).toHaveBeenCalledWith({ + op: "set_level", + level: "info", + }); }); }); }); diff --git a/test/transport.test.ts b/test/transport.test.ts index cc73fdb29..e4a840681 100644 --- a/test/transport.test.ts +++ b/test/transport.test.ts @@ -56,7 +56,8 @@ describe("Transport", () => { it("should handle RosbridgeMessage", () => { const message: RosbridgeMessage = { - op: "test", + op: "set_level", + level: "info", }; const messageEvent: Partial = { @@ -370,10 +371,10 @@ describe("Transport", () => { it("should send messages as JSON", () => { const transport = new NativeWebSocketTransport(mockSocket); - transport.send({ op: "test" }); + transport.send({ op: "set_level", level: "info" }); expect(mockSocket.send).toHaveBeenCalledWith( - JSON.stringify({ op: "test" }), + JSON.stringify({ op: "set_level", level: "info" }), ); }); @@ -483,7 +484,8 @@ describe("Transport", () => { transport.on("message", messageListener); const message: RosbridgeMessage = { - op: "test", + op: "set_level", + level: "info", }; const messageEvent: Partial = { @@ -515,10 +517,10 @@ describe("Transport", () => { it("should send messages as JSON", () => { const transport = new WsWebSocketTransport(mockSocket); - transport.send({ op: "test" }); + transport.send({ op: "set_level", level: "info" }); expect(mockSocket.send).toHaveBeenCalledWith( - JSON.stringify({ op: "test" }), + JSON.stringify({ op: "set_level", level: "info" }), ); }); @@ -628,7 +630,8 @@ describe("Transport", () => { transport.on("message", messageListener); const message: RosbridgeMessage = { - op: "test", + op: "set_level", + level: "info", }; const messageEvent: ws.MessageEvent = {