franky-control 1.0.0__cp313-cp313-manylinux_2_34_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/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,8 @@
1
+ from ._franky import RobotInternal
2
+
3
+ from .robot_web_session import RobotWebSession
4
+
5
+
6
+ class Robot(RobotInternal):
7
+ def create_web_session(self, username: str, password: str):
8
+ return RobotWebSession(self, username, password)
@@ -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