Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial OpenRPC docs - with state subscription #5

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
Open
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
16 changes: 16 additions & 0 deletions Documentation/API Backend/openrpc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# OpenRPC Document

The API is documented here using the OpenRPC 1.2.4 standard. Note that there are later published versions of the standard, but the available tooling is implemented for the v1.2.4 spec.

The full api is defined in openrpc.json, however, this means reading and updating a single file would become unwieldy and merge conflicts would arise. To mitigate this, [open-rpc-compiler](https://www.npmjs.com/package/open-rpc-compiler?activeTab=readme) is used to break each method and schema into its own file organized in this directory's subdirectories, named for the elements they comprise in the openrpc.json doc.


## Install
```
npm install open-rpc-compiler --no-save
```

To compile the elements into the openrpc.json file run the following command from this directory.
```
open-rpc-compile > openrpc.json
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Somewhere we need to include unit information. Presumably this is in μm for linear joints, but not sure what we'd be doing angular joints in? I'd maybe lean towards making it a double and using radians

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think defining the units in the name of the data type would be a good idea. For example jointPositionRadians

I didn't know what the unit was and none of this was meant to be finalized data types, just simple examples.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Usually if you are trying to look at raw motor positions. (which is really just for troubleshooting and calibrating) You would just get the step count from origin. Knowing the current radians of a motor that may fully rotate 20 to 30 times in the joints full range of motion is not as useful as you might think

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can see us needing both values - the raw positions seems useful for calibration/troubleshooting as you mention, the joint position in radians would be the easiest way to display a 3D representation of the robot though I think.

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"type": "object",
"properties": {
"jointIndex": {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps redundant if we're doing an array of these? I can't see a lot of uses for reading back just one joint at a time.

"type": "integer"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what the spec says about this but should we define ints etc with exact widths, eg a 32-bit integer? It's probably not too much of an issue to just define integer as 64-bit signed in all cases, but maybe we'd want to pack from/unpack to specific types on the motion controller side (ie C, C++ or Rust)?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSON schema does not allow for this. We need to map keys to memory space on the controller anyway, so we probably can specify the type for C / Rust in the same lookup table we use for memory mapping?

Copy link
Author

@evan-brooks evan-brooks Sep 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we actually could just add our own keyword if we wanted.

{
  "type": "object",
    "properties": {
        "jointIndex": {
            "type": "integer",
            "data-type": "u8",
        }
    }
}

},
"position": {
"type": "integer"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"type": "array",
"items": {
"$ref": "#/components/schemas/jointPosition"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like being able to reference stuff in like this, seems useful

}
}
11 changes: 11 additions & 0 deletions Documentation/API Backend/openrpc/components/schemas/state.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"type": "object",
"properties": {
"robotOnline": {
"type": "boolean"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it's going to be an enum, since we could have a lot of potential states - eg drives offline, estop, teach/manual, active-paused, run etc

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can define an enum

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to be careful about this. Most operators manuals for industrial robots have a state chapter (including error states) that are dozens of pages long. This could be daunting to write out on an enum

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the robotOnline was literally just put in as an example value. The point of this was just to establish a format that people like you can use to develop the details

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point on the enum getting pretty big - I can see us maybe having a hierarchy of states, eg the main state is error, the substate is error-hardware-overcurrent and there's also an associated error message.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On second thoughts maybe the state is just presented as a string (for showing to the user) and anything that needs to make decisions checks state flags which the controller toggles based on the state, eg jog_enabled, has_error - that keeps the state logic more contained inside the controller

},
"jointPositions": {
"$ref": "#/components/schemas/jointPositions"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd keep this separate from the controller state - the complete controller state does include joint positions, but it also includes a lot of other values and I think we need to be able to pick and choose. Also it just seems cleaner to keep things separate.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the getState method provides the ability to include or exclude keys. You can also call the getPosition method to get just that. The reason that all state items are also included in a state object is so that there is a place to list all of the state parameters and it is a mechanism for transmitting any changes to state.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is a way to generalize the API. So we dont' necessarily have to create methods for every parameter that gets added to state. If you add a new key to state, it can automatically be subscribed to.

}
}
}
4 changes: 4 additions & 0 deletions Documentation/API Backend/openrpc/info.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": "0.1.0",
"title": "Motion Controller API"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "getActiveSubscriptions",
"description": "Get a list of state keys for which the client is currently subscribed to value changes",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very useful

"params": [],
"result": {
"name": "activeStateKeySubscriptions",
"description": "the state keys to which the client is currently subsribed",
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
43 changes: 43 additions & 0 deletions Documentation/API Backend/openrpc/methods/getState.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"name": "getState",
"description": "Get the current state and optionally subscribe to changes",
"params": [
{
"name": "include",
"description": "state keys that should be included in the response. If undefined, all keys except any specifially excluded are included.",
"required": false,
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
{
"name": "exclude",
"description": "state keys that should not be included in the response.",
"required": false,
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
{
"name": "subscribeToChanges",
"required": false,
"description": "Subscribe to changes in the keys included in the request",
"schema": {
"type": "boolean"
}
}
],
"result": {
"name": "state",
"description": "the current state. For subscription updates, only keys for changed values will be included.",
"schema": {
"$ref": "#/components/schemas/state"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "getPosition",
"description": "Get current joint positions",
"params": [],
"result": {
"name": "jointPositions",
"description": "updated joint positions",
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/jointPosition"
}
}
},
"examples": [
{
"name": "getPosition",
"params": [],
"result": {
"name": "jointPositions",
"value": [
{
"jointIndex": 1,
"position": 393
},
{
"jointIndex": 5,
"position": 1829
}
]
}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
{
"name": "jogJoint",
"description": "Jog an joint with a specified velocity",
"params": [
{
"name": "jointIndex",
"description": "The index of the joint to jog",
"schema": {
"type": "integer"
}
},
{
"name": "direction",
"description": "Direction to jog the joint.",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably you could just specify a negative velocity rather than having this be separate?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. That makes sense

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have to be careful with the velocity field. There are other factors that contribute to the final velocity of the jog. Most of these factors are determined in the safety circuit. For example if a specific safety switch is open (like a person gate) than the max jog speed of the robot is radically lowered and would be outside of whatever value you sent to the API.

In that respect. I believe the velocity value should instead be a percentage.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that would depend on what you expected to happen if something changed the maximum permissible speed while the robot was moving, for example if the control setpoint is 20% and robot is moving at 20mm/s then the safety system changes the limit from 100mm/s to 50mm/s, would you expect the robot to drop back to 10mm/s or remain constant at 20mm/s.

We'd probably also want a separate absolute interface for use with external control systems (ROS etc) - perhaps that would be enabled in addition to the jog interface if the client has an extra permission enabled, since it shouldn't be accessible from the UI as it'd be operational in run mode instead of teach/manual mode.

"schema": {
"type": "integer"
}
},
{
"name": "speed",
"description": "The speed at which to jog.",
"schema": {
"type": "integer"
}
}
],
"result": {
"name": "noResult",
"schema": {
"type": "null"
}
},
"examples": [
{
"name": "jogJoint",
"params": [
{
"name": "jointIndex",
"value": 3
},
{
"name": "direction",
"value": 1
},
{
"name": "speed",
"value": 88
}
],
"result": {
"name": "noResult",
"value": null
}
}
]
}
34 changes: 34 additions & 0 deletions Documentation/API Backend/openrpc/methods/unsubscribeState.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "unsubscribeState",
"description": "Unsubscribe from changes to state",
"params": [
{
"name": "include",
"description": "state keys that should be unsubscribed from. If undefined, all keys except any specifially excluded are included.",
"required": false,
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
},
{
"name": "exclude",
"description": "state keys that should not be unsubscribed from",
"required": false,
"schema": {
"type": "array",
"items": {
"type": "string"
}
}
}
],
"result": {
"name": "noResult",
"schema": {
"type": "null"
}
}
}
Loading