kscale 0.2.2__tar.gz → 0.3.0__tar.gz

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.
Files changed (45) hide show
  1. {kscale-0.2.2/kscale.egg-info → kscale-0.3.0}/PKG-INFO +1 -1
  2. {kscale-0.2.2 → kscale-0.3.0}/kscale/__init__.py +1 -1
  3. kscale-0.3.0/kscale/web/cli/robot_class.py +412 -0
  4. {kscale-0.2.2 → kscale-0.3.0}/kscale/web/clients/base.py +4 -3
  5. {kscale-0.2.2 → kscale-0.3.0}/kscale/web/clients/robot.py +5 -5
  6. {kscale-0.2.2 → kscale-0.3.0}/kscale/web/clients/robot_class.py +14 -9
  7. kscale-0.3.0/kscale/web/gen/api.py +129 -0
  8. {kscale-0.2.2 → kscale-0.3.0/kscale.egg-info}/PKG-INFO +1 -1
  9. {kscale-0.2.2 → kscale-0.3.0}/pyproject.toml +2 -0
  10. kscale-0.2.2/kscale/web/cli/robot_class.py +0 -113
  11. kscale-0.2.2/kscale/web/gen/api.py +0 -73
  12. {kscale-0.2.2 → kscale-0.3.0}/LICENSE +0 -0
  13. {kscale-0.2.2 → kscale-0.3.0}/MANIFEST.in +0 -0
  14. {kscale-0.2.2 → kscale-0.3.0}/README.md +0 -0
  15. {kscale-0.2.2 → kscale-0.3.0}/kscale/artifacts/__init__.py +0 -0
  16. {kscale-0.2.2 → kscale-0.3.0}/kscale/artifacts/plane.obj +0 -0
  17. {kscale-0.2.2 → kscale-0.3.0}/kscale/artifacts/plane.urdf +0 -0
  18. {kscale-0.2.2 → kscale-0.3.0}/kscale/cli.py +0 -0
  19. {kscale-0.2.2 → kscale-0.3.0}/kscale/conf.py +0 -0
  20. {kscale-0.2.2 → kscale-0.3.0}/kscale/py.typed +0 -0
  21. {kscale-0.2.2 → kscale-0.3.0}/kscale/requirements-dev.txt +0 -0
  22. {kscale-0.2.2 → kscale-0.3.0}/kscale/requirements.txt +0 -0
  23. {kscale-0.2.2 → kscale-0.3.0}/kscale/utils/__init__.py +0 -0
  24. {kscale-0.2.2 → kscale-0.3.0}/kscale/utils/api_base.py +0 -0
  25. {kscale-0.2.2 → kscale-0.3.0}/kscale/utils/checksum.py +0 -0
  26. {kscale-0.2.2 → kscale-0.3.0}/kscale/utils/cli.py +0 -0
  27. {kscale-0.2.2 → kscale-0.3.0}/kscale/web/__init__.py +0 -0
  28. {kscale-0.2.2 → kscale-0.3.0}/kscale/web/cli/__init__.py +0 -0
  29. {kscale-0.2.2 → kscale-0.3.0}/kscale/web/cli/robot.py +0 -0
  30. {kscale-0.2.2 → kscale-0.3.0}/kscale/web/cli/token.py +0 -0
  31. {kscale-0.2.2 → kscale-0.3.0}/kscale/web/cli/user.py +0 -0
  32. {kscale-0.2.2 → kscale-0.3.0}/kscale/web/clients/__init__.py +0 -0
  33. {kscale-0.2.2 → kscale-0.3.0}/kscale/web/clients/client.py +0 -0
  34. {kscale-0.2.2 → kscale-0.3.0}/kscale/web/clients/user.py +0 -0
  35. {kscale-0.2.2 → kscale-0.3.0}/kscale/web/gen/__init__.py +0 -0
  36. {kscale-0.2.2 → kscale-0.3.0}/kscale/web/utils.py +0 -0
  37. {kscale-0.2.2 → kscale-0.3.0}/kscale.egg-info/SOURCES.txt +0 -0
  38. {kscale-0.2.2 → kscale-0.3.0}/kscale.egg-info/dependency_links.txt +0 -0
  39. {kscale-0.2.2 → kscale-0.3.0}/kscale.egg-info/entry_points.txt +0 -0
  40. {kscale-0.2.2 → kscale-0.3.0}/kscale.egg-info/not-zip-safe +0 -0
  41. {kscale-0.2.2 → kscale-0.3.0}/kscale.egg-info/requires.txt +0 -0
  42. {kscale-0.2.2 → kscale-0.3.0}/kscale.egg-info/top_level.txt +0 -0
  43. {kscale-0.2.2 → kscale-0.3.0}/setup.cfg +0 -0
  44. {kscale-0.2.2 → kscale-0.3.0}/setup.py +0 -0
  45. {kscale-0.2.2 → kscale-0.3.0}/tests/test_dummy.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: kscale
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: The kscale project
5
5
  Home-page: https://github.com/kscalelabs/kscale
6
6
  Author: Benjamin Bolte
@@ -1,6 +1,6 @@
1
1
  """Defines the common interface for the K-Scale Python API."""
2
2
 
3
- __version__ = "0.2.2"
3
+ __version__ = "0.3.0"
4
4
 
5
5
  from pathlib import Path
6
6
 
@@ -0,0 +1,412 @@
1
+ """Defines the CLI for getting information about robot classes."""
2
+
3
+ import json
4
+ import logging
5
+ import math
6
+ import time
7
+ from typing import Sequence
8
+
9
+ import click
10
+ from tabulate import tabulate
11
+
12
+ from kscale.utils.cli import coro
13
+ from kscale.web.clients.robot_class import RobotClassClient
14
+ from kscale.web.gen.api import RobotURDFMetadataInput
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ @click.group()
20
+ def cli() -> None:
21
+ """Get information about robot classes."""
22
+ pass
23
+
24
+
25
+ @cli.command()
26
+ @coro
27
+ async def list() -> None:
28
+ """Lists all robot classes."""
29
+ client = RobotClassClient()
30
+ robot_classes = await client.get_robot_classes()
31
+ if robot_classes:
32
+ # Prepare table data
33
+ table_data = [
34
+ [
35
+ click.style(rc.id, fg="blue"),
36
+ click.style(rc.class_name, fg="green"),
37
+ rc.description or "N/A",
38
+ ]
39
+ for rc in robot_classes
40
+ ]
41
+ click.echo(tabulate(table_data, headers=["ID", "Name", "Description"], tablefmt="simple"))
42
+ else:
43
+ click.echo(click.style("No robot classes found", fg="red"))
44
+
45
+
46
+ @cli.command()
47
+ @click.argument("name")
48
+ @click.option("-d", "--description", type=str, default=None)
49
+ @coro
50
+ async def add(
51
+ name: str,
52
+ description: str | None = None,
53
+ ) -> None:
54
+ """Adds a new robot class."""
55
+ async with RobotClassClient() as client:
56
+ robot_class = await client.create_robot_class(name, description)
57
+ click.echo("Robot class created:")
58
+ click.echo(f" ID: {click.style(robot_class.id, fg='blue')}")
59
+ click.echo(f" Name: {click.style(robot_class.class_name, fg='green')}")
60
+ click.echo(f" Description: {click.style(robot_class.description or 'N/A', fg='yellow')}")
61
+
62
+
63
+ @cli.command()
64
+ @click.argument("current_name")
65
+ @click.option("-n", "--name", type=str, default=None)
66
+ @click.option("-d", "--description", type=str, default=None)
67
+ @coro
68
+ async def update(current_name: str, name: str | None = None, description: str | None = None) -> None:
69
+ """Updates a robot class."""
70
+ async with RobotClassClient() as client:
71
+ robot_class = await client.update_robot_class(current_name, name, description)
72
+ click.echo("Robot class updated:")
73
+ click.echo(f" ID: {click.style(robot_class.id, fg='blue')}")
74
+ click.echo(f" Name: {click.style(robot_class.class_name, fg='green')}")
75
+ click.echo(f" Description: {click.style(robot_class.description or 'N/A', fg='yellow')}")
76
+
77
+
78
+ @cli.command()
79
+ @click.argument("name")
80
+ @click.argument("json_path", type=click.Path(exists=True))
81
+ @coro
82
+ async def update_metadata(name: str, json_path: str) -> None:
83
+ """Updates the metadata of a robot class."""
84
+ with open(json_path, "r", encoding="utf-8") as f:
85
+ raw_metadata = json.load(f)
86
+ metadata = RobotURDFMetadataInput.model_validate(raw_metadata)
87
+ async with RobotClassClient() as client:
88
+ robot_class = await client.update_robot_class(name, new_metadata=metadata)
89
+ click.echo("Robot class metadata updated:")
90
+ click.echo(f" ID: {click.style(robot_class.id, fg='blue')}")
91
+ click.echo(f" Name: {click.style(robot_class.class_name, fg='green')}")
92
+
93
+
94
+ @cli.command()
95
+ @click.argument("name")
96
+ @coro
97
+ async def delete(name: str) -> None:
98
+ """Deletes a robot class."""
99
+ async with RobotClassClient() as client:
100
+ await client.delete_robot_class(name)
101
+ click.echo(f"Robot class deleted: {click.style(name, fg='red')}")
102
+
103
+
104
+ @cli.group()
105
+ def urdf() -> None:
106
+ """Handle the robot class URDF."""
107
+ pass
108
+
109
+
110
+ @urdf.command()
111
+ @click.argument("class_name")
112
+ @click.argument("urdf_file")
113
+ @coro
114
+ async def upload(class_name: str, urdf_file: str) -> None:
115
+ """Uploads a URDF file to a robot class."""
116
+ async with RobotClassClient() as client:
117
+ response = await client.upload_robot_class_urdf(class_name, urdf_file)
118
+ click.echo("URDF uploaded:")
119
+ click.echo(f" Filename: {click.style(response.filename, fg='green')}")
120
+
121
+
122
+ @urdf.command()
123
+ @click.argument("class_name")
124
+ @click.option("--no-cache", is_flag=True, default=False)
125
+ @coro
126
+ async def download(class_name: str, no_cache: bool) -> None:
127
+ """Downloads a URDF file from a robot class."""
128
+ async with RobotClassClient() as client:
129
+ urdf_file = await client.download_compressed_urdf(class_name, cache=not no_cache)
130
+ click.echo(f"URDF downloaded: {click.style(urdf_file, fg='green')}")
131
+
132
+
133
+ @urdf.command()
134
+ @click.argument("class_name")
135
+ @click.option("--no-cache", is_flag=True, default=False)
136
+ @click.option("--hide-gui", is_flag=True, default=False)
137
+ @click.option("--hide-origin", is_flag=True, default=False)
138
+ @click.option("--see-thru", is_flag=True, default=False)
139
+ @click.option("--show-collision", is_flag=True, default=False)
140
+ @click.option("--show-inertia", is_flag=True, default=False)
141
+ @click.option("--fixed-base", is_flag=True, default=False)
142
+ @click.option("--no-merge", is_flag=True, default=False)
143
+ @click.option("--dt", type=float, default=0.01)
144
+ @click.option("--start-height", type=float, default=0.0)
145
+ @click.option("--cycle-duration", type=float, default=2.0)
146
+ @coro
147
+ async def pybullet(
148
+ class_name: str,
149
+ no_cache: bool,
150
+ hide_gui: bool,
151
+ hide_origin: bool,
152
+ see_thru: bool,
153
+ show_collision: bool,
154
+ show_inertia: bool,
155
+ fixed_base: bool,
156
+ no_merge: bool,
157
+ dt: float,
158
+ start_height: float,
159
+ cycle_duration: float,
160
+ ) -> None:
161
+ """Shows the URDF file for a robot class in PyBullet."""
162
+ try:
163
+ import pybullet as p
164
+ except ImportError:
165
+ click.echo(click.style("PyBullet is not installed; install it with `pip install pybullet`", fg="red"))
166
+ return
167
+ async with RobotClassClient() as client:
168
+ urdf_base = await client.download_and_extract_urdf(class_name, cache=not no_cache)
169
+ try:
170
+ urdf_path = next(urdf_base.glob("*.urdf"))
171
+ except StopIteration:
172
+ click.echo(click.style(f"No URDF file found in {urdf_base}", fg="red"))
173
+ return
174
+
175
+ # Connect to PyBullet.
176
+ p.connect(p.GUI)
177
+ p.setGravity(0, 0, -9.81)
178
+ p.setRealTimeSimulation(0)
179
+
180
+ # Create floor plane
181
+ floor = p.createCollisionShape(p.GEOM_PLANE)
182
+ p.createMultiBody(0, floor)
183
+
184
+ # Turn off panels.
185
+ if hide_gui:
186
+ p.configureDebugVisualizer(p.COV_ENABLE_GUI, 0)
187
+ p.configureDebugVisualizer(p.COV_ENABLE_SEGMENTATION_MARK_PREVIEW, 0)
188
+ p.configureDebugVisualizer(p.COV_ENABLE_DEPTH_BUFFER_PREVIEW, 0)
189
+ p.configureDebugVisualizer(p.COV_ENABLE_RGB_BUFFER_PREVIEW, 0)
190
+
191
+ # Enable mouse picking.
192
+ p.configureDebugVisualizer(p.COV_ENABLE_MOUSE_PICKING, 1)
193
+
194
+ # Load the robot URDF.
195
+ start_position = [0.0, 0.0, start_height]
196
+ start_orientation = p.getQuaternionFromEuler([0.0, 0.0, 0.0])
197
+ flags = p.URDF_USE_INERTIA_FROM_FILE
198
+ if not no_merge:
199
+ flags |= p.URDF_MERGE_FIXED_LINKS
200
+
201
+ robot = p.loadURDF(
202
+ str(urdf_path.resolve().absolute()),
203
+ start_position,
204
+ start_orientation,
205
+ flags=flags,
206
+ useFixedBase=fixed_base,
207
+ )
208
+
209
+ # Display collision meshes as separate object.
210
+ if show_collision:
211
+ collision_flags = p.URDF_USE_INERTIA_FROM_FILE | p.URDF_USE_SELF_COLLISION_EXCLUDE_ALL_PARENTS
212
+ collision = p.loadURDF(
213
+ str(urdf_path.resolve().absolute()),
214
+ start_position,
215
+ start_orientation,
216
+ flags=collision_flags,
217
+ useFixedBase=0,
218
+ )
219
+
220
+ # Make collision shapes semi-transparent.
221
+ joint_ids = [i for i in range(p.getNumJoints(collision))] + [-1]
222
+ for i in joint_ids:
223
+ p.changeVisualShape(collision, i, rgbaColor=[1, 0, 0, 0.5])
224
+
225
+ # Initializes physics parameters.
226
+ p.changeDynamics(floor, -1, lateralFriction=1, spinningFriction=-1, rollingFriction=-1)
227
+ p.setPhysicsEngineParameter(fixedTimeStep=dt, maxNumCmdPer1ms=1000)
228
+
229
+ # Shows the origin of the robot.
230
+ if not hide_origin:
231
+ p.addUserDebugLine([0, 0, 0], [0.1, 0, 0], [1, 0, 0], parentObjectUniqueId=robot, parentLinkIndex=-1)
232
+ p.addUserDebugLine([0, 0, 0], [0, 0.1, 0], [0, 1, 0], parentObjectUniqueId=robot, parentLinkIndex=-1)
233
+ p.addUserDebugLine([0, 0, 0], [0, 0, 0.1], [0, 0, 1], parentObjectUniqueId=robot, parentLinkIndex=-1)
234
+
235
+ # Make the robot see-through.
236
+ joint_ids = [i for i in range(p.getNumJoints(robot))] + [-1]
237
+ if see_thru:
238
+ shape_data = p.getVisualShapeData(robot)
239
+ for i in joint_ids:
240
+ prev_color = shape_data[i][-1]
241
+ p.changeVisualShape(robot, i, rgbaColor=prev_color[:3] + (0.9,))
242
+
243
+ def draw_box(pt: Sequence[Sequence[float]], color: tuple[float, float, float], obj_id: int, link_id: int) -> None:
244
+ """Draw a box in PyBullet debug visualization.
245
+
246
+ Args:
247
+ pt: List of 8 points defining box vertices, each point is [x,y,z]
248
+ color: RGB color tuple for the box lines
249
+ obj_id: PyBullet object ID to attach box to
250
+ link_id: Link ID on the object to attach box to
251
+ """
252
+ assert len(pt) == 8
253
+ assert all(len(p) == 3 for p in pt)
254
+
255
+ p.addUserDebugLine(pt[0], pt[1], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
256
+ p.addUserDebugLine(pt[1], pt[3], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
257
+ p.addUserDebugLine(pt[3], pt[2], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
258
+ p.addUserDebugLine(pt[2], pt[0], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
259
+
260
+ p.addUserDebugLine(pt[0], pt[4], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
261
+ p.addUserDebugLine(pt[1], pt[5], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
262
+ p.addUserDebugLine(pt[2], pt[6], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
263
+ p.addUserDebugLine(pt[3], pt[7], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
264
+
265
+ p.addUserDebugLine(pt[4 + 0], pt[4 + 1], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
266
+ p.addUserDebugLine(pt[4 + 1], pt[4 + 3], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
267
+ p.addUserDebugLine(pt[4 + 3], pt[4 + 2], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
268
+ p.addUserDebugLine(pt[4 + 2], pt[4 + 0], color, 1, parentObjectUniqueId=obj_id, parentLinkIndex=link_id)
269
+
270
+ # Shows bounding boxes around each part of the robot representing the inertia frame.
271
+ if show_inertia:
272
+ for i in joint_ids:
273
+ dynamics_info = p.getDynamicsInfo(robot, i)
274
+ mass = dynamics_info[0]
275
+ if mass <= 0:
276
+ continue
277
+ inertia = dynamics_info[2]
278
+
279
+ # Calculate box dimensions.
280
+ ixx, iyy, izz = inertia[0], inertia[1], inertia[2]
281
+ box_scale_x = math.sqrt(6 * (iyy + izz - ixx) / mass) / 2
282
+ box_scale_y = math.sqrt(6 * (ixx + izz - iyy) / mass) / 2
283
+ box_scale_z = math.sqrt(6 * (ixx + iyy - izz) / mass) / 2
284
+ half_extents = [box_scale_x, box_scale_y, box_scale_z]
285
+
286
+ # Create box vertices in local inertia frame
287
+ pt = [
288
+ [half_extents[0], half_extents[1], half_extents[2]],
289
+ [-half_extents[0], half_extents[1], half_extents[2]],
290
+ [half_extents[0], -half_extents[1], half_extents[2]],
291
+ [-half_extents[0], -half_extents[1], half_extents[2]],
292
+ [half_extents[0], half_extents[1], -half_extents[2]],
293
+ [-half_extents[0], half_extents[1], -half_extents[2]],
294
+ [half_extents[0], -half_extents[1], -half_extents[2]],
295
+ [-half_extents[0], -half_extents[1], -half_extents[2]],
296
+ ]
297
+
298
+ draw_box(pt, (1, 0, 0), robot, i)
299
+
300
+ # Show joint controller.
301
+ joints: dict[str, int] = {}
302
+ controls: dict[str, float] = {}
303
+ for i in range(p.getNumJoints(robot)):
304
+ joint_info = p.getJointInfo(robot, i)
305
+ name = joint_info[1].decode("utf-8")
306
+ joint_type = joint_info[2]
307
+ joints[name] = i
308
+ if joint_type == p.JOINT_PRISMATIC:
309
+ joint_min, joint_max = joint_info[8:10]
310
+ controls[name] = p.addUserDebugParameter(name, joint_min, joint_max, 0.0)
311
+ elif joint_type == p.JOINT_REVOLUTE:
312
+ joint_min, joint_max = joint_info[8:10]
313
+ controls[name] = p.addUserDebugParameter(name, joint_min, joint_max, 0.0)
314
+
315
+ def reset_joints_to_zero(robot: int, joints: dict[str, int]) -> None:
316
+ for joint_id in joints.values():
317
+ joint_info = p.getJointInfo(robot, joint_id)
318
+ joint_min, joint_max = joint_info[8:10]
319
+ zero_position = (joint_min + joint_max) / 2
320
+ p.setJointMotorControl2(robot, joint_id, p.POSITION_CONTROL, zero_position)
321
+
322
+ def reset_camera(position: int) -> None:
323
+ height = start_height if fixed_base else 0
324
+ camera_positions = {
325
+ 1: (2.0, 0, -30, [0, 0, height]), # Default view
326
+ 2: (2.0, 90, -30, [0, 0, height]), # Side view
327
+ 3: (2.0, 180, -30, [0, 0, height]), # Back view
328
+ 4: (2.0, 270, -30, [0, 0, height]), # Other side view
329
+ 5: (2.0, 0, 0, [0, 0, height]), # Front level view
330
+ 6: (2.0, 0, -80, [0, 0, height]), # Top-down view
331
+ 7: (1.5, 45, -45, [0, 0, height]), # Closer angled view
332
+ 8: (3.0, 30, -30, [0, 0, height]), # Further angled view
333
+ 9: (2.0, 0, 30, [0, 0, height]), # Low angle view
334
+ }
335
+
336
+ if position in camera_positions:
337
+ distance, yaw, pitch, target = camera_positions[position]
338
+ p.resetDebugVisualizerCamera(
339
+ cameraDistance=distance,
340
+ cameraYaw=yaw,
341
+ cameraPitch=pitch,
342
+ cameraTargetPosition=target,
343
+ )
344
+
345
+ # Run the simulation until the user closes the window.
346
+ last_time = time.time()
347
+ prev_control_values = {k: 0.0 for k in controls}
348
+ cycle_joints = False
349
+ cycle_start_time = 0.0
350
+
351
+ while p.isConnected():
352
+ # Reset the simulation if "r" was pressed.
353
+ keys = p.getKeyboardEvents()
354
+ if ord("r") in keys and keys[ord("r")] & p.KEY_WAS_TRIGGERED:
355
+ p.resetBasePositionAndOrientation(robot, start_position, start_orientation)
356
+ p.setJointMotorControlArray(
357
+ robot,
358
+ range(p.getNumJoints(robot)),
359
+ p.POSITION_CONTROL,
360
+ targetPositions=[0] * p.getNumJoints(robot),
361
+ )
362
+
363
+ # Reset joints to zero position if "z" was pressed
364
+ if ord("z") in keys and keys[ord("z")] & p.KEY_WAS_TRIGGERED:
365
+ reset_joints_to_zero(robot, joints)
366
+ cycle_joints = False # Stop joint cycling if it was active
367
+
368
+ # Reset camera if number keys 1-9 are pressed
369
+ for i in range(1, 10):
370
+ if ord(str(i)) in keys and keys[ord(str(i))] & p.KEY_WAS_TRIGGERED:
371
+ reset_camera(i)
372
+
373
+ # Start/stop joint cycling if "c" was pressed
374
+ if ord("c") in keys and keys[ord("c")] & p.KEY_WAS_TRIGGERED:
375
+ cycle_joints = not cycle_joints
376
+ if cycle_joints:
377
+ cycle_start_time = time.time()
378
+ else:
379
+ # When stopping joint cycling, set joints to their current positions
380
+ for k, v in controls.items():
381
+ current_position = p.getJointState(robot, joints[k])[0]
382
+ p.setJointMotorControl2(robot, joints[k], p.POSITION_CONTROL, current_position)
383
+
384
+ # Set joint positions.
385
+ if cycle_joints:
386
+ elapsed_time = time.time() - cycle_start_time
387
+ cycle_progress = (elapsed_time % cycle_duration) / cycle_duration
388
+ for k, v in controls.items():
389
+ joint_info = p.getJointInfo(robot, joints[k])
390
+ joint_min, joint_max = joint_info[8:10]
391
+ target_position = joint_min + (joint_max - joint_min) * math.sin(cycle_progress * math.pi)
392
+ p.setJointMotorControl2(robot, joints[k], p.POSITION_CONTROL, target_position)
393
+ else:
394
+ for k, v in controls.items():
395
+ try:
396
+ target_position = p.readUserDebugParameter(v)
397
+ if target_position != prev_control_values[k]:
398
+ prev_control_values[k] = target_position
399
+ p.setJointMotorControl2(robot, joints[k], p.POSITION_CONTROL, target_position)
400
+ except p.error:
401
+ logger.debug("Failed to set joint %s", k)
402
+ pass
403
+
404
+ # Step simulation.
405
+ p.stepSimulation()
406
+ cur_time = time.time()
407
+ time.sleep(max(0, dt - (cur_time - last_time)))
408
+ last_time = cur_time
409
+
410
+
411
+ if __name__ == "__main__":
412
+ cli()
@@ -360,9 +360,10 @@ class BaseClient:
360
360
  files: dict[str, Any] | None = None,
361
361
  ) -> dict[str, Any]:
362
362
  url = urljoin(self.base_url, endpoint)
363
- kwargs: dict[str, Any] = {"params": params}
364
-
365
- if data:
363
+ kwargs: dict[str, Any] = {}
364
+ if params is not None:
365
+ kwargs["params"] = params
366
+ if data is not None:
366
367
  if isinstance(data, BaseModel):
367
368
  kwargs["json"] = data.model_dump(exclude_unset=True)
368
369
  else:
@@ -19,16 +19,16 @@ class RobotClient(BaseClient):
19
19
  class_name: str,
20
20
  description: str | None = None,
21
21
  ) -> RobotResponse:
22
- params = {"class_name": class_name}
22
+ data = {"class_name": class_name}
23
23
  if description is not None:
24
- params["description"] = description
25
- data = await self._request(
24
+ data["description"] = description
25
+ response = await self._request(
26
26
  "PUT",
27
27
  f"/robot/{robot_name}",
28
- params=params,
28
+ data=data,
29
29
  auth=True,
30
30
  )
31
- return RobotResponse.model_validate(data)
31
+ return RobotResponse.model_validate(response)
32
32
 
33
33
  async def get_robot_by_id(self, robot_id: str) -> RobotResponse:
34
34
  data = await self._request("GET", f"/robot/id/{robot_id}", auth=True)
@@ -5,6 +5,7 @@ import json
5
5
  import logging
6
6
  import tarfile
7
7
  from pathlib import Path
8
+ from typing import Any
8
9
 
9
10
  import httpx
10
11
 
@@ -13,6 +14,7 @@ from kscale.web.gen.api import (
13
14
  RobotClass,
14
15
  RobotDownloadURDFResponse,
15
16
  RobotUploadURDFResponse,
17
+ RobotURDFMetadataInput,
16
18
  )
17
19
  from kscale.web.utils import get_robots_dir, should_refresh_file
18
20
 
@@ -34,13 +36,13 @@ class RobotClassClient(BaseClient):
34
36
  return [RobotClass.model_validate(item) for item in data]
35
37
 
36
38
  async def create_robot_class(self, class_name: str, description: str | None = None) -> RobotClass:
37
- params = {}
39
+ data = {}
38
40
  if description is not None:
39
- params["description"] = description
41
+ data["description"] = description
40
42
  data = await self._request(
41
43
  "PUT",
42
44
  f"/robots/{class_name}",
43
- params=params,
45
+ data=data,
44
46
  auth=True,
45
47
  )
46
48
  return RobotClass.model_validate(data)
@@ -50,18 +52,21 @@ class RobotClassClient(BaseClient):
50
52
  class_name: str,
51
53
  new_class_name: str | None = None,
52
54
  new_description: str | None = None,
55
+ new_metadata: RobotURDFMetadataInput | None = None,
53
56
  ) -> RobotClass:
54
- params = {}
57
+ data: dict[str, Any] = {}
55
58
  if new_class_name is not None:
56
- params["new_class_name"] = new_class_name
59
+ data["new_class_name"] = new_class_name
57
60
  if new_description is not None:
58
- params["new_description"] = new_description
59
- if not params:
61
+ data["new_description"] = new_description
62
+ if new_metadata is not None:
63
+ data["new_metadata"] = new_metadata.model_dump()
64
+ if not data:
60
65
  raise ValueError("No parameters to update")
61
66
  data = await self._request(
62
67
  "POST",
63
68
  f"/robots/{class_name}",
64
- params=params,
69
+ data=data,
65
70
  auth=True,
66
71
  )
67
72
  return RobotClass.model_validate(data)
@@ -84,7 +89,7 @@ class RobotClassClient(BaseClient):
84
89
  data = await self._request(
85
90
  "PUT",
86
91
  f"/robots/urdf/{class_name}",
87
- params={"filename": urdf_file.name, "content_type": content_type},
92
+ data={"filename": urdf_file.name, "content_type": content_type},
88
93
  auth=True,
89
94
  )
90
95
  response = RobotUploadURDFResponse.model_validate(data)
@@ -0,0 +1,129 @@
1
+ """Auto-generated by generate.sh script."""
2
+
3
+ # generated by datamodel-codegen:
4
+ # filename: openapi.json
5
+ # timestamp: 2025-01-21T07:10:22+00:00
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Dict, List, Optional, Union
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+
14
+ class APIKeyResponse(BaseModel):
15
+ api_key: str = Field(..., title="Api Key")
16
+
17
+
18
+ class AddRobotClassRequest(BaseModel):
19
+ description: Optional[str] = Field(None, title="Description")
20
+
21
+
22
+ class AddRobotRequest(BaseModel):
23
+ description: Optional[str] = Field(None, title="Description")
24
+ class_name: str = Field(..., title="Class Name")
25
+
26
+
27
+ class JointMetadataInput(BaseModel):
28
+ id: Optional[int] = Field(None, title="Id")
29
+ kp: Optional[Union[float, str]] = Field(None, title="Kp")
30
+ kd: Optional[Union[float, str]] = Field(None, title="Kd")
31
+ offset: Optional[Union[float, str]] = Field(None, title="Offset")
32
+ lower_limit: Optional[Union[float, str]] = Field(None, title="Lower Limit")
33
+ upper_limit: Optional[Union[float, str]] = Field(None, title="Upper Limit")
34
+
35
+
36
+ class JointMetadataOutput(BaseModel):
37
+ id: Optional[int] = Field(None, title="Id")
38
+ kp: Optional[str] = Field(None, title="Kp")
39
+ kd: Optional[str] = Field(None, title="Kd")
40
+ offset: Optional[str] = Field(None, title="Offset")
41
+ lower_limit: Optional[str] = Field(None, title="Lower Limit")
42
+ upper_limit: Optional[str] = Field(None, title="Upper Limit")
43
+
44
+
45
+ class OICDInfo(BaseModel):
46
+ authority: str = Field(..., title="Authority")
47
+ client_id: str = Field(..., title="Client Id")
48
+
49
+
50
+ class Robot(BaseModel):
51
+ id: str = Field(..., title="Id")
52
+ robot_name: str = Field(..., title="Robot Name")
53
+ description: str = Field(..., title="Description")
54
+ user_id: str = Field(..., title="User Id")
55
+ class_id: str = Field(..., title="Class Id")
56
+
57
+
58
+ class RobotDownloadURDFResponse(BaseModel):
59
+ url: str = Field(..., title="Url")
60
+ md5_hash: str = Field(..., title="Md5 Hash")
61
+
62
+
63
+ class RobotResponse(BaseModel):
64
+ id: str = Field(..., title="Id")
65
+ robot_name: str = Field(..., title="Robot Name")
66
+ description: str = Field(..., title="Description")
67
+ user_id: str = Field(..., title="User Id")
68
+ class_name: str = Field(..., title="Class Name")
69
+
70
+
71
+ class RobotURDFMetadataInput(BaseModel):
72
+ joint_name_to_metadata: Optional[Dict[str, JointMetadataInput]] = Field(None, title="Joint Name To Metadata")
73
+
74
+
75
+ class RobotURDFMetadataOutput(BaseModel):
76
+ joint_name_to_metadata: Optional[Dict[str, JointMetadataOutput]] = Field(None, title="Joint Name To Metadata")
77
+
78
+
79
+ class RobotUploadURDFRequest(BaseModel):
80
+ filename: str = Field(..., title="Filename")
81
+ content_type: str = Field(..., title="Content Type")
82
+
83
+
84
+ class RobotUploadURDFResponse(BaseModel):
85
+ url: str = Field(..., title="Url")
86
+ filename: str = Field(..., title="Filename")
87
+ content_type: str = Field(..., title="Content Type")
88
+
89
+
90
+ class UpdateRobotClassRequest(BaseModel):
91
+ new_class_name: Optional[str] = Field(None, title="New Class Name")
92
+ new_description: Optional[str] = Field(None, title="New Description")
93
+ new_metadata: Optional[RobotURDFMetadataInput] = None
94
+
95
+
96
+ class UpdateRobotRequest(BaseModel):
97
+ new_robot_name: Optional[str] = Field(None, title="New Robot Name")
98
+ new_description: Optional[str] = Field(None, title="New Description")
99
+
100
+
101
+ class UserResponse(BaseModel):
102
+ user_id: str = Field(..., title="User Id")
103
+ is_admin: bool = Field(..., title="Is Admin")
104
+ can_upload: bool = Field(..., title="Can Upload")
105
+ can_test: bool = Field(..., title="Can Test")
106
+
107
+
108
+ class ValidationError(BaseModel):
109
+ loc: List[Union[str, int]] = Field(..., title="Location")
110
+ msg: str = Field(..., title="Message")
111
+ type: str = Field(..., title="Error Type")
112
+
113
+
114
+ class HTTPValidationError(BaseModel):
115
+ detail: Optional[List[ValidationError]] = Field(None, title="Detail")
116
+
117
+
118
+ class ProfileResponse(BaseModel):
119
+ email: str = Field(..., title="Email")
120
+ email_verified: bool = Field(..., title="Email Verified")
121
+ user: UserResponse
122
+
123
+
124
+ class RobotClass(BaseModel):
125
+ id: str = Field(..., title="Id")
126
+ class_name: str = Field(..., title="Class Name")
127
+ description: str = Field(..., title="Description")
128
+ user_id: str = Field(..., title="User Id")
129
+ metadata: Optional[RobotURDFMetadataOutput] = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: kscale
3
- Version: 0.2.2
3
+ Version: 0.3.0
4
4
  Summary: The kscale project
5
5
  Home-page: https://github.com/kscalelabs/kscale
6
6
  Author: Benjamin Bolte
@@ -34,6 +34,8 @@ namespace_packages = false
34
34
 
35
35
  module = [
36
36
  "setuptools",
37
+ "pybullet.*",
38
+ "tabulate.*",
37
39
  ]
38
40
  ignore_missing_imports = true
39
41
 
@@ -1,113 +0,0 @@
1
- """Defines the CLI for getting information about robot classes."""
2
-
3
- import logging
4
-
5
- import click
6
- from tabulate import tabulate
7
-
8
- from kscale.utils.cli import coro
9
- from kscale.web.clients.robot_class import RobotClassClient
10
-
11
- logger = logging.getLogger(__name__)
12
-
13
-
14
- @click.group()
15
- def cli() -> None:
16
- """Get information about robot classes."""
17
- pass
18
-
19
-
20
- @cli.command()
21
- @coro
22
- async def list() -> None:
23
- """Lists all robot classes."""
24
- client = RobotClassClient()
25
- robot_classes = await client.get_robot_classes()
26
- if robot_classes:
27
- # Prepare table data
28
- table_data = [
29
- [
30
- click.style(rc.id, fg="blue"),
31
- click.style(rc.class_name, fg="green"),
32
- rc.description or "N/A",
33
- ]
34
- for rc in robot_classes
35
- ]
36
- click.echo(tabulate(table_data, headers=["ID", "Name", "Description"], tablefmt="simple"))
37
- else:
38
- click.echo(click.style("No robot classes found", fg="red"))
39
-
40
-
41
- @cli.command()
42
- @click.argument("name")
43
- @click.option("-d", "--description", type=str, default=None)
44
- @coro
45
- async def add(
46
- name: str,
47
- description: str | None = None,
48
- ) -> None:
49
- """Adds a new robot class."""
50
- async with RobotClassClient() as client:
51
- robot_class = await client.create_robot_class(name, description)
52
- click.echo("Robot class created:")
53
- click.echo(f" ID: {click.style(robot_class.id, fg='blue')}")
54
- click.echo(f" Name: {click.style(robot_class.class_name, fg='green')}")
55
- click.echo(f" Description: {click.style(robot_class.description or 'N/A', fg='yellow')}")
56
-
57
-
58
- @cli.command()
59
- @click.argument("current_name")
60
- @click.option("-n", "--name", type=str, default=None)
61
- @click.option("-d", "--description", type=str, default=None)
62
- @coro
63
- async def update(current_name: str, name: str | None = None, description: str | None = None) -> None:
64
- """Updates a robot class."""
65
- async with RobotClassClient() as client:
66
- robot_class = await client.update_robot_class(current_name, name, description)
67
- click.echo("Robot class updated:")
68
- click.echo(f" ID: {click.style(robot_class.id, fg='blue')}")
69
- click.echo(f" Name: {click.style(robot_class.class_name, fg='green')}")
70
- click.echo(f" Description: {click.style(robot_class.description or 'N/A', fg='yellow')}")
71
-
72
-
73
- @cli.command()
74
- @click.argument("name")
75
- @coro
76
- async def delete(name: str) -> None:
77
- """Deletes a robot class."""
78
- async with RobotClassClient() as client:
79
- await client.delete_robot_class(name)
80
- click.echo(f"Robot class deleted: {click.style(name, fg='red')}")
81
-
82
-
83
- @cli.group()
84
- def urdf() -> None:
85
- """Handle the robot class URDF."""
86
- pass
87
-
88
-
89
- @urdf.command()
90
- @click.argument("class_name")
91
- @click.argument("urdf_file")
92
- @coro
93
- async def upload(class_name: str, urdf_file: str) -> None:
94
- """Uploads a URDF file to a robot class."""
95
- async with RobotClassClient() as client:
96
- response = await client.upload_robot_class_urdf(class_name, urdf_file)
97
- click.echo("URDF uploaded:")
98
- click.echo(f" Filename: {click.style(response.filename, fg='green')}")
99
-
100
-
101
- @urdf.command()
102
- @click.argument("class_name")
103
- @click.option("--no-cache", is_flag=True, default=False)
104
- @coro
105
- async def download(class_name: str, no_cache: bool) -> None:
106
- """Downloads a URDF file from a robot class."""
107
- async with RobotClassClient() as client:
108
- urdf_file = await client.download_compressed_urdf(class_name, cache=not no_cache)
109
- click.echo(f"URDF downloaded: {click.style(urdf_file, fg='green')}")
110
-
111
-
112
- if __name__ == "__main__":
113
- cli()
@@ -1,73 +0,0 @@
1
- """Auto-generated by generate.sh script."""
2
-
3
- # generated by datamodel-codegen:
4
- # filename: openapi.json
5
- # timestamp: 2025-01-15T22:35:42+00:00
6
-
7
- from __future__ import annotations
8
-
9
- from typing import List, Optional, Union
10
-
11
- from pydantic import BaseModel, Field
12
-
13
-
14
- class OICDInfo(BaseModel):
15
- authority: str = Field(..., title="Authority")
16
- client_id: str = Field(..., title="Client Id")
17
-
18
-
19
- class Robot(BaseModel):
20
- id: str = Field(..., title="Id")
21
- robot_name: str = Field(..., title="Robot Name")
22
- description: str = Field(..., title="Description")
23
- user_id: str = Field(..., title="User Id")
24
- class_id: str = Field(..., title="Class Id")
25
-
26
-
27
- class RobotClass(BaseModel):
28
- id: str = Field(..., title="Id")
29
- class_name: str = Field(..., title="Class Name")
30
- description: str = Field(..., title="Description")
31
- user_id: str = Field(..., title="User Id")
32
-
33
-
34
- class RobotDownloadURDFResponse(BaseModel):
35
- url: str = Field(..., title="Url")
36
- md5_hash: str = Field(..., title="Md5 Hash")
37
-
38
-
39
- class RobotResponse(BaseModel):
40
- id: str = Field(..., title="Id")
41
- robot_name: str = Field(..., title="Robot Name")
42
- description: str = Field(..., title="Description")
43
- user_id: str = Field(..., title="User Id")
44
- class_name: str = Field(..., title="Class Name")
45
-
46
-
47
- class RobotUploadURDFResponse(BaseModel):
48
- url: str = Field(..., title="Url")
49
- filename: str = Field(..., title="Filename")
50
- content_type: str = Field(..., title="Content Type")
51
-
52
-
53
- class UserResponse(BaseModel):
54
- user_id: str = Field(..., title="User Id")
55
- is_admin: bool = Field(..., title="Is Admin")
56
- can_upload: bool = Field(..., title="Can Upload")
57
- can_test: bool = Field(..., title="Can Test")
58
-
59
-
60
- class ValidationError(BaseModel):
61
- loc: List[Union[str, int]] = Field(..., title="Location")
62
- msg: str = Field(..., title="Message")
63
- type: str = Field(..., title="Error Type")
64
-
65
-
66
- class HTTPValidationError(BaseModel):
67
- detail: Optional[List[ValidationError]] = Field(None, title="Detail")
68
-
69
-
70
- class ProfileResponse(BaseModel):
71
- email: str = Field(..., title="Email")
72
- email_verified: bool = Field(..., title="Email Verified")
73
- user: UserResponse
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes