franky-control 1.0.2__cp313-cp313-manylinux_2_28_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- franky/__init__.py +12 -0
- franky/_franky.cpython-313-x86_64-linux-gnu.so +0 -0
- franky/_franky.pyi +1736 -0
- franky/motion.py +12 -0
- franky/reaction.py +43 -0
- franky/robot.py +8 -0
- franky/robot_web_session.py +183 -0
- franky_control-1.0.2.dist-info/METADATA +881 -0
- franky_control-1.0.2.dist-info/RECORD +27 -0
- franky_control-1.0.2.dist-info/WHEEL +5 -0
- franky_control-1.0.2.dist-info/licenses/LICENSE +165 -0
- franky_control-1.0.2.dist-info/top_level.txt +2 -0
- franky_control.libs/libPocoFoundation-7333b9fc.so.95 +0 -0
- franky_control.libs/libPocoNet-007a41b7.so.95 +0 -0
- franky_control.libs/libboost_filesystem-38deced9.so.1.66.0 +0 -0
- franky_control.libs/libboost_serialization-3aa46b41.so.1.66.0 +0 -0
- franky_control.libs/libboost_system-69a6c43e.so.1.66.0 +0 -0
- franky_control.libs/libconsole_bridge-def124d9.so.0.3 +0 -0
- franky_control.libs/libfmt-8375f6bf.so.6.2.1 +0 -0
- franky_control.libs/libfranka-92ddaf43.so.0.15.0 +0 -0
- franky_control.libs/libpinocchio_default-7b0164b0.so.3.1.0 +0 -0
- franky_control.libs/libpinocchio_parsers-50abb87c.so.3.1.0 +0 -0
- franky_control.libs/libtinyxml-435d1f53.so.0.2.6.2 +0 -0
- franky_control.libs/liburdfdom_model-9e7b5a88.so.1.0 +0 -0
- franky_control.libs/liburdfdom_model_state-741cbb83.so.1.0 +0 -0
- franky_control.libs/liburdfdom_sensor-5dbb5bb4.so.1.0 +0 -0
- franky_control.libs/liburdfdom_world-bd943329.so.1.0 +0 -0
franky/motion.py
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
from typing import Union
|
2
|
+
|
3
|
+
from ._franky import BaseCartesianPoseMotion, BaseCartesianVelocityMotion, BaseJointPositionMotion, \
|
4
|
+
BaseJointVelocityMotion, BaseTorqueMotion
|
5
|
+
|
6
|
+
Motion = Union[
|
7
|
+
BaseCartesianPoseMotion,
|
8
|
+
BaseCartesianVelocityMotion,
|
9
|
+
BaseJointPositionMotion,
|
10
|
+
BaseJointVelocityMotion,
|
11
|
+
BaseTorqueMotion
|
12
|
+
]
|
franky/reaction.py
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
from ._franky import Condition, BaseCartesianPoseMotion, BaseCartesianVelocityMotion, BaseJointPositionMotion, \
|
2
|
+
BaseJointVelocityMotion, BaseTorqueMotion, \
|
3
|
+
CartesianPoseReaction as _CartesianPoseReaction, \
|
4
|
+
CartesianVelocityReaction as _CartesianVelocityReaction, \
|
5
|
+
JointPositionReaction as _JointPositionReaction, \
|
6
|
+
JointVelocityReaction as _JointVelocityReaction, \
|
7
|
+
TorqueReaction as _TorqueReaction
|
8
|
+
|
9
|
+
from .motion import Motion
|
10
|
+
|
11
|
+
|
12
|
+
class Reaction:
|
13
|
+
_control_signal_type = None
|
14
|
+
|
15
|
+
def __new__(cls, condition: Condition, motion: Motion):
|
16
|
+
for reaction_type in _REACTION_TYPES:
|
17
|
+
if isinstance(motion, reaction_type._motion_type):
|
18
|
+
return reaction_type.__new__(reaction_type, condition, motion)
|
19
|
+
raise TypeError(f"Unknown motion type {type(motion)}.")
|
20
|
+
|
21
|
+
|
22
|
+
class CartesianPoseReaction(_CartesianPoseReaction, Reaction):
|
23
|
+
_motion_type = BaseCartesianPoseMotion
|
24
|
+
|
25
|
+
|
26
|
+
class CartesianVelocityReaction(_CartesianVelocityReaction, Reaction):
|
27
|
+
_motion_type = BaseCartesianVelocityMotion
|
28
|
+
|
29
|
+
|
30
|
+
class JointPositionReaction(_JointPositionReaction, Reaction):
|
31
|
+
_motion_type = BaseJointPositionMotion
|
32
|
+
|
33
|
+
|
34
|
+
class JointVelocityReaction(_JointVelocityReaction, Reaction):
|
35
|
+
_motion_type = BaseJointVelocityMotion
|
36
|
+
|
37
|
+
|
38
|
+
class TorqueReaction(_TorqueReaction, Reaction):
|
39
|
+
_motion_type = BaseTorqueMotion
|
40
|
+
|
41
|
+
|
42
|
+
_REACTION_TYPES = [
|
43
|
+
CartesianPoseReaction, CartesianVelocityReaction, JointPositionReaction, JointVelocityReaction, TorqueReaction]
|
franky/robot.py
ADDED
@@ -0,0 +1,183 @@
|
|
1
|
+
import base64
|
2
|
+
import hashlib
|
3
|
+
import http.client
|
4
|
+
import json
|
5
|
+
import ssl
|
6
|
+
import time
|
7
|
+
import urllib.parse
|
8
|
+
from http.client import HTTPSConnection, HTTPResponse
|
9
|
+
from typing import Dict, Optional, Any, Literal
|
10
|
+
from urllib.error import HTTPError
|
11
|
+
|
12
|
+
|
13
|
+
class FrankaAPIError(Exception):
|
14
|
+
def __init__(self, target: str, http_code: int, http_reason: str, headers: Dict[str, str], message: str):
|
15
|
+
super().__init__(
|
16
|
+
f"Franka API returned error {http_code} ({http_reason}) when accessing end-point {target}: {message}")
|
17
|
+
self.target = target
|
18
|
+
self.http_code = http_code
|
19
|
+
self.headers = headers
|
20
|
+
self.message = message
|
21
|
+
|
22
|
+
|
23
|
+
class RobotWebSession:
|
24
|
+
def __init__(self, fci_hostname: str, username: str, password: str):
|
25
|
+
self.__fci_hostname = fci_hostname
|
26
|
+
self.__username = username
|
27
|
+
self.__password = password
|
28
|
+
|
29
|
+
self.__client = None
|
30
|
+
self.__token = None
|
31
|
+
self.__control_token = None
|
32
|
+
self.__control_token_id = None
|
33
|
+
|
34
|
+
@staticmethod
|
35
|
+
def __encode_password(user: str, password: str) -> str:
|
36
|
+
bs = ",".join([str(b) for b in hashlib.sha256((password + "#" + user + "@franka").encode("utf-8")).digest()])
|
37
|
+
return base64.encodebytes(bs.encode("utf-8")).decode("utf-8")
|
38
|
+
|
39
|
+
def _send_api_request(self, target: str, headers: Optional[Dict[str, str]] = None, body: Optional[Any] = None,
|
40
|
+
method: Literal["GET", "POST", "DELETE"] = "POST"):
|
41
|
+
_headers = {
|
42
|
+
"Cookie": f"authorization={self.__token}"
|
43
|
+
}
|
44
|
+
if headers is not None:
|
45
|
+
_headers.update(headers)
|
46
|
+
self.__client.request(method, target, headers=_headers, body=body)
|
47
|
+
res: HTTPResponse = self.__client.getresponse()
|
48
|
+
if res.getcode() != 200:
|
49
|
+
raise FrankaAPIError(target, res.getcode(), res.reason, dict(res.headers), res.read().decode("utf-8"))
|
50
|
+
return res.read()
|
51
|
+
|
52
|
+
def send_api_request(self, target: str, headers: Optional[Dict[str, str]] = None, body: Optional[Any] = None,
|
53
|
+
method: Literal["GET", "POST", "DELETE"] = "POST"):
|
54
|
+
last_error = None
|
55
|
+
for i in range(3):
|
56
|
+
try:
|
57
|
+
return self._send_api_request(target, headers, body, method)
|
58
|
+
except http.client.RemoteDisconnected as ex:
|
59
|
+
last_error = ex
|
60
|
+
raise last_error
|
61
|
+
|
62
|
+
def send_control_api_request(self, target: str, headers: Optional[Dict[str, str]] = None,
|
63
|
+
body: Optional[Any] = None,
|
64
|
+
method: Literal["GET", "POST", "DELETE"] = "POST"):
|
65
|
+
if headers is None:
|
66
|
+
headers = {}
|
67
|
+
self.__check_control_token()
|
68
|
+
_headers = {
|
69
|
+
"X-Control-Token": self.__control_token
|
70
|
+
}
|
71
|
+
_headers.update(headers)
|
72
|
+
return self.send_api_request(target, headers=_headers, method=method, body=body)
|
73
|
+
|
74
|
+
def open(self):
|
75
|
+
if self.is_open:
|
76
|
+
raise RuntimeError("Session is already open.")
|
77
|
+
self.__client = HTTPSConnection(self.__fci_hostname, timeout=12, context=ssl._create_unverified_context())
|
78
|
+
self.__client.connect()
|
79
|
+
payload = json.dumps(
|
80
|
+
{"login": self.__username, "password": self.__encode_password(self.__username, self.__password)})
|
81
|
+
self.__token = self.send_api_request(
|
82
|
+
"/admin/api/login", headers={"content-type": "application/json"},
|
83
|
+
body=payload).decode("utf-8")
|
84
|
+
return self
|
85
|
+
|
86
|
+
def close(self):
|
87
|
+
if not self.is_open:
|
88
|
+
raise RuntimeError("Session is not open.")
|
89
|
+
if self.__control_token is not None:
|
90
|
+
self.release_control()
|
91
|
+
self.__token = None
|
92
|
+
self.__client.close()
|
93
|
+
|
94
|
+
def __enter__(self):
|
95
|
+
self.open()
|
96
|
+
|
97
|
+
def __exit__(self, type, value, traceback):
|
98
|
+
self.close()
|
99
|
+
|
100
|
+
def __check_control_token(self):
|
101
|
+
if self.__control_token is None:
|
102
|
+
raise RuntimeError("Client does not have control. Call take_control() first.")
|
103
|
+
|
104
|
+
def take_control(self, wait_timeout: float = 10.0):
|
105
|
+
if self.__control_token is None:
|
106
|
+
res = self.send_api_request(
|
107
|
+
"/admin/api/control-token/request", headers={"content-type": "application/json"},
|
108
|
+
body=json.dumps({"requestedBy": self.__username}))
|
109
|
+
response_dict = json.loads(res)
|
110
|
+
self.__control_token = response_dict["token"]
|
111
|
+
self.__control_token_id = response_dict["id"]
|
112
|
+
# One should probably use websockets here but that would introduce another dependency
|
113
|
+
start = time.time()
|
114
|
+
while time.time() - start < wait_timeout and not self.has_control():
|
115
|
+
time.sleep(1.0)
|
116
|
+
return self.has_control()
|
117
|
+
|
118
|
+
def release_control(self):
|
119
|
+
if self.__control_token is not None:
|
120
|
+
self.send_control_api_request(
|
121
|
+
"/admin/api/control-token", headers={"content-type": "application/json"}, method="DELETE",
|
122
|
+
body=json.dumps({"token": self.__control_token}))
|
123
|
+
self.__control_token = None
|
124
|
+
self.__control_token_id = None
|
125
|
+
|
126
|
+
def enable_fci(self):
|
127
|
+
self.send_control_api_request(
|
128
|
+
"/desk/api/system/fci", headers={"content-type": "application/x-www-form-urlencoded"},
|
129
|
+
body=f"token={urllib.parse.quote(base64.b64encode(self.__control_token.encode('ascii')))}")
|
130
|
+
|
131
|
+
def has_control(self):
|
132
|
+
if self.__control_token_id is not None:
|
133
|
+
status = self.get_system_status()
|
134
|
+
active_token = status["controlToken"]["activeToken"]
|
135
|
+
return active_token is not None and active_token["id"] == self.__control_token_id
|
136
|
+
return False
|
137
|
+
|
138
|
+
def start_task(self, task: str):
|
139
|
+
self.send_api_request(
|
140
|
+
"/desk/api/execution", headers={"content-type": "application/x-www-form-urlencoded"},
|
141
|
+
body=f"id={task}")
|
142
|
+
|
143
|
+
def unlock_brakes(self):
|
144
|
+
self.send_control_api_request(
|
145
|
+
"/desk/api/joints/unlock", headers={"content-type": "application/x-www-form-urlencoded"})
|
146
|
+
|
147
|
+
def lock_brakes(self):
|
148
|
+
self.send_control_api_request(
|
149
|
+
"/desk/api/joints/lock", headers={"content-type": "application/x-www-form-urlencoded"})
|
150
|
+
|
151
|
+
def set_mode_programming(self):
|
152
|
+
self.send_control_api_request(
|
153
|
+
"/desk/api/operating-mode/programming", headers={"content-type": "application/x-www-form-urlencoded"})
|
154
|
+
|
155
|
+
def set_mode_execution(self):
|
156
|
+
self.send_control_api_request(
|
157
|
+
"/desk/api/operating-mode/execution", headers={"content-type": "application/x-www-form-urlencoded"})
|
158
|
+
|
159
|
+
def get_system_status(self):
|
160
|
+
return json.loads(self.send_api_request("/admin/api/system-status", method="GET").decode("utf-8"))
|
161
|
+
|
162
|
+
def execute_self_test(self):
|
163
|
+
if self.get_system_status()["safety"]["recoverableErrors"]["td2Timeout"]:
|
164
|
+
self.send_control_api_request(
|
165
|
+
"/admin/api/safety/recoverable-safety-errors/acknowledge?error_id=TD2Timeout")
|
166
|
+
response = json.loads(self.send_control_api_request(
|
167
|
+
"/admin/api/safety/td2-tests/execute", headers={"content-type": "application/json"}).decode("utf-8"))
|
168
|
+
assert response["code"] == "SuccessResponse"
|
169
|
+
time.sleep(0.5)
|
170
|
+
while self.get_system_status()["safety"]["safetyControllerStatus"] == "SelfTest":
|
171
|
+
time.sleep(0.5)
|
172
|
+
|
173
|
+
@property
|
174
|
+
def client(self) -> HTTPSConnection:
|
175
|
+
return self.__client
|
176
|
+
|
177
|
+
@property
|
178
|
+
def token(self) -> str:
|
179
|
+
return self.__token
|
180
|
+
|
181
|
+
@property
|
182
|
+
def is_open(self) -> bool:
|
183
|
+
return self.__token is not None
|