Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/actionlib/ActionClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ import type Goal from "./Goal.js";
*
*/
export default class ActionClient<
TGoal = unknown,
TFeedback = unknown,
TResult = unknown,
TGoal = never,
TFeedback = never,
TResult = never,
> extends EventEmitter<{
timeout: undefined;
}> {
goals: Partial<Record<string, Goal<TGoal>>> = {};
goals: Partial<Record<string, Goal<TGoal, TFeedback, TResult>>> = {};
/** flag to check if a status has been received */
receivedStatus = false;
ros: Ros;
Expand Down
2 changes: 1 addition & 1 deletion src/actionlib/ActionListener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import type { actionlib_msgs } from "../types/actionlib_msgs.js";
*
*/
export default class ActionListener<
TGoal,
TGoal extends object,
TFeedback,
TResult,
> extends EventEmitter<{
Expand Down
6 changes: 3 additions & 3 deletions src/actionlib/Goal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import { v4 as uuidv4 } from "uuid";
* * 'timeout' - If a timeout occurred while sending a goal.
*/
export default class Goal<
TGoal,
TFeedback = unknown,
TResult = unknown,
TGoal = never,
TFeedback = never,
TResult = never,
> extends EventEmitter<{
timeout: undefined;
status: [actionlib_msgs.GoalStatus];
Expand Down
127 changes: 68 additions & 59 deletions src/core/Action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,55 @@
*/

import { GoalStatus } from "./GoalStatus.ts";
import type { RosbridgeSendActionGoalMessage } from "../types/protocol.ts";
import {
isRosbridgeActionFeedbackMessage,
isRosbridgeActionResultMessage,
isRosbridgeCancelActionGoalMessage,
isRosbridgeSendActionGoalMessage,
} from "../types/protocol.ts";
import type Ros from "./Ros.js";
import { v4 as uuidv4 } from "uuid";
import type { ActionIdString } from "../types/emitted_events.js";
import type {
AnyActionOp,
SendActionGoalOp,
CancelActionGoalOp,
ActionFeedbackOp,
ActionResultSuccessOp,
ActionResultFailedOp,
} from "../types/protocol.js";

type ActionCallback<TGoal extends object> = (
goal: TGoal,
id: string | undefined,
) => void;
type ActionCancelCallback = (id: string) => void;

interface ActionOptions {
/**
* The ROSLIB.Ros connection handle.
*/
ros: Ros;
/**
* The action name, like '/fibonacci'.
*/
name: string;
/**
* The action type, like 'example_interfaces/Fibonacci'.
*/
actionType: string;
}

/**
* A ROS 2 action client.
*/
export default class Action<
TGoal = unknown,
TFeedback = unknown,
TResult = unknown,
TGoal extends object = object,
TFeedback extends object = object,
TResult extends object = object,
> {
isAdvertised = false;
#actionCallback: ((goal: TGoal, id: string) => void) | null = null;
#cancelCallback: ((id: string) => void) | null = null;
#actionCallback: ActionCallback<TGoal> | null = null;
#cancelCallback: ActionCancelCallback | null = null;
ros: Ros;
name: string;
actionType: string;
/**
* @param options
* @param options.ros - The ROSLIB.Ros connection handle.
* @param options.name - The action name, like '/fibonacci'.
* @param options.actionType - The action type, like 'example_interfaces/Fibonacci'.
*/
constructor({
ros,
name,
actionType,
}: {
ros: Ros;
name: string;
actionType: string;
}) {

constructor({ ros, name, actionType }: ActionOptions) {
this.ros = ros;
this.name = name;
this.actionType = actionType;
Expand All @@ -68,21 +78,24 @@ export default class Action<
return;
}

const actionGoalId = `send_action_goal:${this.name}:${uuidv4()}`;
const actionGoalId: ActionIdString = `send_action_goal:${this.name}:${uuidv4()}`;

this.ros.on(actionGoalId, function (message) {
if (isRosbridgeActionResultMessage<TResult>(message)) {
if (!message.result) {
failedCallback(message.values ?? "");
} else {
resultCallback(message.values);
this.ros.on(
actionGoalId,
(message: AnyActionOp<TGoal, TFeedback, TResult>) => {
if (message.op === "action_result") {
if (!message.result) {
failedCallback(message.values ?? "");
} else {
resultCallback(message.values);
}
} else if (message.op === "action_feedback") {
feedbackCallback?.(message.values);
}
} else if (isRosbridgeActionFeedbackMessage<TFeedback>(message)) {
feedbackCallback?.(message.values);
}
});
},
);

const call = {
const call: SendActionGoalOp<TGoal> = {
op: "send_action_goal",
id: actionGoalId,
action: this.name,
Expand All @@ -101,7 +114,7 @@ export default class Action<
* @param id - The ID of the action goal to cancel.
*/
cancelGoal(id: string) {
const call = {
const call: CancelActionGoalOp = {
op: "cancel_action_goal",
id: id,
action: this.name,
Expand All @@ -117,23 +130,23 @@ export default class Action<
* @param cancelCallback - A callback function to execute when the action is canceled.
*/
advertise(
actionCallback: (goal: TGoal, id: string) => void,
cancelCallback: (id: string) => void,
actionCallback: ActionCallback<TGoal>,
cancelCallback: ActionCancelCallback,
) {
if (this.isAdvertised || typeof actionCallback !== "function") {
return;
}

this.#actionCallback = actionCallback;
this.#cancelCallback = cancelCallback;
this.ros.on(this.name, (msg) => {
if (isRosbridgeSendActionGoalMessage(msg)) {
this.#executeAction.bind(this);
} else {
this.ros.on(this.name, (msg: AnyActionOp<TGoal, TFeedback, TResult>) => {
if (msg.op !== "send_action_goal") {
throw new Error(
"Received unrelated message on Action server event stream!",
);
}

this.#executeAction(msg);
});
this.ros.callOnConnection({
op: "advertise_action",
Expand Down Expand Up @@ -166,30 +179,26 @@ export default class Action<
* @param rosbridgeRequest.id - The ID of the action goal.
* @param rosbridgeRequest.args - The arguments of the action goal.
*/
#executeAction(rosbridgeRequest: RosbridgeSendActionGoalMessage<TGoal>) {
#executeAction(rosbridgeRequest: SendActionGoalOp<TGoal>) {
const id = rosbridgeRequest.id;

// If a cancellation callback exists, call it when a cancellation event is emitted.
if (typeof id === "string") {
this.ros.on(id, (message) => {
if (
isRosbridgeCancelActionGoalMessage(message) &&
this.#cancelCallback
) {
this.ros.on(id, (message: AnyActionOp<TGoal, TFeedback, TResult>) => {
if (message.op === "cancel_action_goal" && this.#cancelCallback) {
this.#cancelCallback(id);
}
});
}

// Call the action goal execution function provided.
if (this.#actionCallback) {
if (rosbridgeRequest.args) {
this.#actionCallback(rosbridgeRequest.args, id);
} else {
if (!rosbridgeRequest.args) {
throw new Error(
"Received Action goal with no arguments! This should never happen, because rosbridge should fill in blanks!",
);
}
this.#actionCallback(rosbridgeRequest.args, id);
}
}

Expand All @@ -200,7 +209,7 @@ export default class Action<
* @param feedback - The feedback to send.
*/
sendFeedback(id: string, feedback: TFeedback) {
const call = {
const call: ActionFeedbackOp<TFeedback> = {
op: "action_feedback",
id: id,
action: this.name,
Expand All @@ -216,7 +225,7 @@ export default class Action<
* @param result - The result to set.
*/
setSucceeded(id: string, result: TResult) {
const call = {
const call: ActionResultSuccessOp<TResult> = {
op: "action_result",
id: id,
action: this.name,
Expand All @@ -234,7 +243,7 @@ export default class Action<
* @param result - The result to set.
*/
setCanceled(id: string, result: TResult) {
const call = {
const call: ActionResultSuccessOp<TResult> = {
op: "action_result",
id: id,
action: this.name,
Expand All @@ -251,7 +260,7 @@ export default class Action<
* @param id - The action goal ID.
*/
setFailed(id: string) {
const call = {
const call: ActionResultFailedOp = {
op: "action_result",
id: id,
action: this.name,
Expand Down
Loading