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