urkit 0.4.0__tar.gz → 0.4.2__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 (42) hide show
  1. {urkit-0.4.0 → urkit-0.4.2}/PKG-INFO +76 -11
  2. {urkit-0.4.0 → urkit-0.4.2}/README.md +75 -10
  3. {urkit-0.4.0 → urkit-0.4.2}/pyproject.toml +1 -1
  4. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/__init__.py +1 -1
  5. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/__main__.py +22 -1
  6. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/cli/connection_monitor.py +8 -1
  7. urkit-0.4.2/src/urkit/cli/init.py +83 -0
  8. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/cli/points.py +28 -35
  9. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/cli/teach.py +100 -154
  10. urkit-0.4.2/src/urkit/cli/terminal.py +233 -0
  11. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/points.py +8 -5
  12. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/robot.py +7 -6
  13. {urkit-0.4.0 → urkit-0.4.2}/src/urkit.egg-info/PKG-INFO +76 -11
  14. {urkit-0.4.0 → urkit-0.4.2}/src/urkit.egg-info/SOURCES.txt +2 -0
  15. {urkit-0.4.0 → urkit-0.4.2}/setup.cfg +0 -0
  16. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/cli/__init__.py +0 -0
  17. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/cli/colors.py +0 -0
  18. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/config.py +0 -0
  19. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/connection.py +0 -0
  20. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/exceptions.py +0 -0
  21. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/geometry.py +0 -0
  22. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/gripper/__init__.py +0 -0
  23. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/gripper/base.py +0 -0
  24. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/gripper/digital.py +0 -0
  25. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/gripper/presets.py +0 -0
  26. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/gripper/robotiq.py +0 -0
  27. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/gripper/robotiq_preamble.py +0 -0
  28. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/io.py +0 -0
  29. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/motion.py +0 -0
  30. {urkit-0.4.0 → urkit-0.4.2}/src/urkit/telemetry.py +0 -0
  31. {urkit-0.4.0 → urkit-0.4.2}/src/urkit.egg-info/dependency_links.txt +0 -0
  32. {urkit-0.4.0 → urkit-0.4.2}/src/urkit.egg-info/entry_points.txt +0 -0
  33. {urkit-0.4.0 → urkit-0.4.2}/src/urkit.egg-info/requires.txt +0 -0
  34. {urkit-0.4.0 → urkit-0.4.2}/src/urkit.egg-info/top_level.txt +0 -0
  35. {urkit-0.4.0 → urkit-0.4.2}/tests/test_exceptions.py +0 -0
  36. {urkit-0.4.0 → urkit-0.4.2}/tests/test_geometry.py +0 -0
  37. {urkit-0.4.0 → urkit-0.4.2}/tests/test_gripper.py +0 -0
  38. {urkit-0.4.0 → urkit-0.4.2}/tests/test_gripper_factory.py +0 -0
  39. {urkit-0.4.0 → urkit-0.4.2}/tests/test_gripper_presets.py +0 -0
  40. {urkit-0.4.0 → urkit-0.4.2}/tests/test_move_sequence.py +0 -0
  41. {urkit-0.4.0 → urkit-0.4.2}/tests/test_points.py +0 -0
  42. {urkit-0.4.0 → urkit-0.4.2}/tests/test_robot_integration.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: urkit
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Universal Robots e-Series control toolkit built on ur_rtde
5
5
  Author: URKit Contributors
6
6
  License: MIT
@@ -56,12 +56,14 @@ The typical workflow: teach points with the pendant, write a few lines of Python
56
56
 
57
57
  - [Quick Start](#quick-start)
58
58
  - [Configuration](#configuration)
59
+ - [Create a Config File](#create-a-config-file)
59
60
  - [Location](#location)
60
61
  - [Keys](#keys)
61
62
  - [Gripper Config](#gripper-config)
62
63
  - [Saving Config](#saving-config)
63
64
  - [Programmatic](#programmatic)
64
65
  - [Interactive CLI](#interactive-cli)
66
+ - [Init](#init)
65
67
  - [Teach Mode](#teach-mode)
66
68
  - [Points Explorer](#points-explorer)
67
69
  - [Key Map](#key-map)
@@ -92,12 +94,40 @@ The `-U` (upgrade) flag ensures you always get the latest version. This project
92
94
 
93
95
  Requires Python 3.8+ and a Universal Robots e-Series (UR3e to UR30).
94
96
 
97
+ ### Platform Support
98
+
99
+ - **Linux, macOS**: fully supported
100
+ - **Windows**: the Python library works on Windows. The interactive CLIs (`urkit teach`, `urkit points`) run on Windows but are **not fully tested and not officially supported**. A warning is printed at startup. If you hit input or console problems, use Linux, macOS, or WSL2
101
+
95
102
  ### Robot Setup (one-time)
96
103
 
97
- 1. **Network**: `☰` → `System` → `System` → `Network`. Set a static IP on the robot and a matching one on your PC. Both addresses must share the same first three octets (the network), with a different last octet (the host). For example:
98
- - **Robot**: `192.168.1.50` / Subnet `255.255.255.0`
99
- - **PC**: `192.168.1.1` / Netmask `255.255.255.0`
100
- - Verify with `ping 192.168.1.50`. Connect via direct Ethernet cable or a switch.
104
+ 1. **Network**: Connect your PC to the robot via Ethernet cable (direct) or a switch.
105
+
106
+ First, configure the robot: `☰` → `System` → `System` → `Network`. Set a static IP, e.g., `192.168.1.50` / Subnet `255.255.255.0`.
107
+
108
+ Then set a matching static IP on your PC. The exact IP can be anything, as long as the first three octets match the robot's network and the last octet is different:
109
+
110
+ **Linux**:
111
+ - Open `Settings` → `Network` → click the gear icon next to `Wired`
112
+ - Go to `IPv4` tab → change `Automatic (DHCP)` to `Manual`
113
+ - Enter Address: `192.168.1.1`, Netmask: `255.255.255.0`
114
+ - Click `Apply`, then toggle the connection off and on
115
+
116
+ **macOS**:
117
+ - Open `System Settings` → `Network` → click `Ethernet` (or your adapter)
118
+ - Click `Details...` next to the adapter name → `TCP/IP`
119
+ - Set `Configure IPv4` to `Manually`
120
+ - Enter IP: `192.168.1.1`, Subnet Mask: `255.255.255.0`
121
+ - Click `OK`
122
+
123
+ **Windows**:
124
+ - Open `Settings` → `Network & internet` → `Ethernet`
125
+ - Click the adapter name (e.g., `Ethernet`) at the top
126
+ - Under `IP settings`, click `Edit` → select `Manual`
127
+ - Toggle `IPv4` On, enter IP: `192.168.1.1`, Subnet mask: `255.255.255.0`
128
+ - Click `Save`
129
+
130
+ Verify connectivity: `ping 192.168.1.50`.
101
131
  2. **Remote Control**: `☰` → `System` → `Remote Control`: Enable. Press the remote/local button on the pendant.
102
132
  3. **Security**: `☰` → `Security` → `Services`: enable RTDE and disable EtherNet/IP, PROFINET, or MODBUS if they're claiming RTDE registers. Save and restart.
103
133
 
@@ -122,9 +152,20 @@ robot.gripper.open()
122
152
 
123
153
  The typical workflow:
124
154
 
125
- 1. **Teach points.** Use the CLI to position the robot and save named waypoints.
126
- 2. **Write code.** Create a robot, move to points by name, apply offsets, run sequences.
127
- 3. **Iterate.** Add more points, tweak your code, repeat.
155
+ 1. **Scaffold config.** Run `urkit init` to create a default `config.yaml`.
156
+ 2. **Edit it.** Set your robot IP, gripper type, and other settings.
157
+ 3. **Teach points.** Run `urkit teach` (auto-loads config) to position the robot and save waypoints.
158
+ 4. **Write code.** Load the config and move to points by name:
159
+
160
+ ```python
161
+ from urkit import URRobot
162
+
163
+ robot = URRobot.from_config("config.yaml")
164
+ robot.move_to("home")
165
+ robot.move_to("pick", offset_z=0.05)
166
+ ```
167
+
168
+ 5. **Iterate.** Add more points, tweak your code, repeat.
128
169
 
129
170
  ---
130
171
 
@@ -132,9 +173,21 @@ The typical workflow:
132
173
 
133
174
  URKit uses a YAML config file (`config.yaml`) to persist settings between sessions.
134
175
 
176
+ ### Create a Config File
177
+
178
+ Generate a default config with all options documented:
179
+
180
+ ```bash
181
+ urkit init # writes config.yaml in CWD
182
+ urkit init --output station_a.yaml # custom path
183
+ urkit init --force # overwrite if exists
184
+ ```
185
+
186
+ Edit the generated file with your robot IP and gripper settings.
187
+
135
188
  ### Location
136
189
 
137
- URKit searches for `config.yaml` in the current working directory, or an explicit path via `--config`.
190
+ The CLI auto-loads `config.yaml` from the current working directory. Pass `--config` for a custom path.
138
191
 
139
192
  ### Keys
140
193
 
@@ -224,7 +277,19 @@ robot = URRobot.from_config({"robot_ip": "192.168.1.50", "gripper": "2f-85"}) #
224
277
 
225
278
  ## Interactive CLI
226
279
 
227
- URKit provides two CLI tools: **teach** for interactive robot control, and **points** for browsing saved waypoints.
280
+ URKit provides three CLI tools: **init** to scaffold a config file, **teach** for interactive robot control, and **points** for browsing saved waypoints.
281
+
282
+ ### Init
283
+
284
+ Generate a default config file with all options documented:
285
+
286
+ ```bash
287
+ urkit init # writes config.yaml in CWD
288
+ urkit init --output station_a.yaml # custom path
289
+ urkit init --force # overwrite if exists
290
+ ```
291
+
292
+ ### Teach Mode
228
293
 
229
294
  ### Teach Mode
230
295
 
@@ -603,7 +668,7 @@ Default is `None` (controller handles IK natively). Set it globally when most of
603
668
 
604
669
  #### Point Management
605
670
 
606
- Points are stored in the active TCP frame, so they work with any tool. Swap grippers and your saved points stay valid.
671
+ Points are stored as absolute TCP poses in the robot base frame. When you move to a point, the controller moves whatever TCP is currently active to that base-frame pose, so the tool tip lands at the same physical location. Change the TCP offset (or swap grippers) and your saved points stay valid.
607
672
 
608
673
  ```python
609
674
  robot.save_point("here")
@@ -30,12 +30,14 @@ The typical workflow: teach points with the pendant, write a few lines of Python
30
30
 
31
31
  - [Quick Start](#quick-start)
32
32
  - [Configuration](#configuration)
33
+ - [Create a Config File](#create-a-config-file)
33
34
  - [Location](#location)
34
35
  - [Keys](#keys)
35
36
  - [Gripper Config](#gripper-config)
36
37
  - [Saving Config](#saving-config)
37
38
  - [Programmatic](#programmatic)
38
39
  - [Interactive CLI](#interactive-cli)
40
+ - [Init](#init)
39
41
  - [Teach Mode](#teach-mode)
40
42
  - [Points Explorer](#points-explorer)
41
43
  - [Key Map](#key-map)
@@ -66,12 +68,40 @@ The `-U` (upgrade) flag ensures you always get the latest version. This project
66
68
 
67
69
  Requires Python 3.8+ and a Universal Robots e-Series (UR3e to UR30).
68
70
 
71
+ ### Platform Support
72
+
73
+ - **Linux, macOS**: fully supported
74
+ - **Windows**: the Python library works on Windows. The interactive CLIs (`urkit teach`, `urkit points`) run on Windows but are **not fully tested and not officially supported**. A warning is printed at startup. If you hit input or console problems, use Linux, macOS, or WSL2
75
+
69
76
  ### Robot Setup (one-time)
70
77
 
71
- 1. **Network**: `☰` → `System` → `System` → `Network`. Set a static IP on the robot and a matching one on your PC. Both addresses must share the same first three octets (the network), with a different last octet (the host). For example:
72
- - **Robot**: `192.168.1.50` / Subnet `255.255.255.0`
73
- - **PC**: `192.168.1.1` / Netmask `255.255.255.0`
74
- - Verify with `ping 192.168.1.50`. Connect via direct Ethernet cable or a switch.
78
+ 1. **Network**: Connect your PC to the robot via Ethernet cable (direct) or a switch.
79
+
80
+ First, configure the robot: `☰` → `System` → `System` → `Network`. Set a static IP, e.g., `192.168.1.50` / Subnet `255.255.255.0`.
81
+
82
+ Then set a matching static IP on your PC. The exact IP can be anything, as long as the first three octets match the robot's network and the last octet is different:
83
+
84
+ **Linux**:
85
+ - Open `Settings` → `Network` → click the gear icon next to `Wired`
86
+ - Go to `IPv4` tab → change `Automatic (DHCP)` to `Manual`
87
+ - Enter Address: `192.168.1.1`, Netmask: `255.255.255.0`
88
+ - Click `Apply`, then toggle the connection off and on
89
+
90
+ **macOS**:
91
+ - Open `System Settings` → `Network` → click `Ethernet` (or your adapter)
92
+ - Click `Details...` next to the adapter name → `TCP/IP`
93
+ - Set `Configure IPv4` to `Manually`
94
+ - Enter IP: `192.168.1.1`, Subnet Mask: `255.255.255.0`
95
+ - Click `OK`
96
+
97
+ **Windows**:
98
+ - Open `Settings` → `Network & internet` → `Ethernet`
99
+ - Click the adapter name (e.g., `Ethernet`) at the top
100
+ - Under `IP settings`, click `Edit` → select `Manual`
101
+ - Toggle `IPv4` On, enter IP: `192.168.1.1`, Subnet mask: `255.255.255.0`
102
+ - Click `Save`
103
+
104
+ Verify connectivity: `ping 192.168.1.50`.
75
105
  2. **Remote Control**: `☰` → `System` → `Remote Control`: Enable. Press the remote/local button on the pendant.
76
106
  3. **Security**: `☰` → `Security` → `Services`: enable RTDE and disable EtherNet/IP, PROFINET, or MODBUS if they're claiming RTDE registers. Save and restart.
77
107
 
@@ -96,9 +126,20 @@ robot.gripper.open()
96
126
 
97
127
  The typical workflow:
98
128
 
99
- 1. **Teach points.** Use the CLI to position the robot and save named waypoints.
100
- 2. **Write code.** Create a robot, move to points by name, apply offsets, run sequences.
101
- 3. **Iterate.** Add more points, tweak your code, repeat.
129
+ 1. **Scaffold config.** Run `urkit init` to create a default `config.yaml`.
130
+ 2. **Edit it.** Set your robot IP, gripper type, and other settings.
131
+ 3. **Teach points.** Run `urkit teach` (auto-loads config) to position the robot and save waypoints.
132
+ 4. **Write code.** Load the config and move to points by name:
133
+
134
+ ```python
135
+ from urkit import URRobot
136
+
137
+ robot = URRobot.from_config("config.yaml")
138
+ robot.move_to("home")
139
+ robot.move_to("pick", offset_z=0.05)
140
+ ```
141
+
142
+ 5. **Iterate.** Add more points, tweak your code, repeat.
102
143
 
103
144
  ---
104
145
 
@@ -106,9 +147,21 @@ The typical workflow:
106
147
 
107
148
  URKit uses a YAML config file (`config.yaml`) to persist settings between sessions.
108
149
 
150
+ ### Create a Config File
151
+
152
+ Generate a default config with all options documented:
153
+
154
+ ```bash
155
+ urkit init # writes config.yaml in CWD
156
+ urkit init --output station_a.yaml # custom path
157
+ urkit init --force # overwrite if exists
158
+ ```
159
+
160
+ Edit the generated file with your robot IP and gripper settings.
161
+
109
162
  ### Location
110
163
 
111
- URKit searches for `config.yaml` in the current working directory, or an explicit path via `--config`.
164
+ The CLI auto-loads `config.yaml` from the current working directory. Pass `--config` for a custom path.
112
165
 
113
166
  ### Keys
114
167
 
@@ -198,7 +251,19 @@ robot = URRobot.from_config({"robot_ip": "192.168.1.50", "gripper": "2f-85"}) #
198
251
 
199
252
  ## Interactive CLI
200
253
 
201
- URKit provides two CLI tools: **teach** for interactive robot control, and **points** for browsing saved waypoints.
254
+ URKit provides three CLI tools: **init** to scaffold a config file, **teach** for interactive robot control, and **points** for browsing saved waypoints.
255
+
256
+ ### Init
257
+
258
+ Generate a default config file with all options documented:
259
+
260
+ ```bash
261
+ urkit init # writes config.yaml in CWD
262
+ urkit init --output station_a.yaml # custom path
263
+ urkit init --force # overwrite if exists
264
+ ```
265
+
266
+ ### Teach Mode
202
267
 
203
268
  ### Teach Mode
204
269
 
@@ -577,7 +642,7 @@ Default is `None` (controller handles IK natively). Set it globally when most of
577
642
 
578
643
  #### Point Management
579
644
 
580
- Points are stored in the active TCP frame, so they work with any tool. Swap grippers and your saved points stay valid.
645
+ Points are stored as absolute TCP poses in the robot base frame. When you move to a point, the controller moves whatever TCP is currently active to that base-frame pose, so the tool tip lands at the same physical location. Change the TCP offset (or swap grippers) and your saved points stay valid.
581
646
 
582
647
  ```python
583
648
  robot.save_point("here")
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "urkit"
7
- version = "0.4.0"
7
+ version = "0.4.2"
8
8
  description = "Universal Robots e-Series control toolkit built on ur_rtde"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -24,7 +24,7 @@ Quick start::
24
24
 
25
25
  from __future__ import annotations
26
26
 
27
- __version__ = "0.4.0"
27
+ __version__ = "0.4.2"
28
28
 
29
29
 
30
30
  from urkit.exceptions import (
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
  import argparse
13
13
  import sys
14
14
 
15
+ from urkit.cli.init import init_command
15
16
  from urkit.cli.teach import teach_command
16
17
  from urkit.cli.points import points_command
17
18
 
@@ -25,6 +26,24 @@ def main() -> None:
25
26
 
26
27
  subparsers = parser.add_subparsers(dest="command", help="Available commands")
27
28
 
29
+ # init subcommand
30
+ init_parser = subparsers.add_parser(
31
+ "init",
32
+ help="Scaffold a default config.yaml",
33
+ )
34
+ init_parser.add_argument(
35
+ "--output",
36
+ type=str,
37
+ default="config.yaml",
38
+ help="Output path (default: config.yaml in CWD)",
39
+ )
40
+ init_parser.add_argument(
41
+ "--force",
42
+ action="store_true",
43
+ default=False,
44
+ help="Overwrite existing file",
45
+ )
46
+
28
47
  # teach subcommand
29
48
  teach_parser = subparsers.add_parser(
30
49
  "teach",
@@ -113,7 +132,9 @@ def main() -> None:
113
132
  parser.print_help()
114
133
  sys.exit(1)
115
134
 
116
- if args.command == "teach":
135
+ if args.command == "init":
136
+ init_command(args)
137
+ elif args.command == "teach":
117
138
  teach_command(args)
118
139
  elif args.command == "points":
119
140
  points_command(args)
@@ -144,10 +144,17 @@ class ConnectionMonitor:
144
144
  logger.info("Connection monitor loop exited")
145
145
 
146
146
  def _trigger(self, reason: str) -> None:
147
- """Record fault reason and send SIGALRM to interrupt blocking calls."""
147
+ """Record fault reason and send SIGALRM to interrupt blocking calls.
148
+
149
+ On Windows there is no self-signal: the fault_detected flag is
150
+ the only interruption path, and callers poll it between and
151
+ after RTDE calls.
152
+ """
148
153
  self._reason = reason
149
154
  self._fault_event.set()
150
155
  logger.info("Fault detected: %s", reason)
156
+ if not hasattr(signal, "SIGALRM"):
157
+ return
151
158
  try:
152
159
  os.kill(os.getpid(), signal.SIGALRM)
153
160
  except OSError as e:
@@ -0,0 +1,83 @@
1
+ """urkit init — scaffold a default config.yaml file.
2
+
3
+ Generates a commented config file with all available options so the
4
+ user can edit it for their setup.
5
+
6
+ Usage:
7
+ urkit init # writes config.yaml in CWD
8
+ urkit init --output station_a.yaml # custom path
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ _DEFAULT_TEMPLATE = """\
17
+ # urkit config — edit and save as config.yaml
18
+ # Generated by: urkit init
19
+ #
20
+ # Load it:
21
+ # Python: robot = URRobot.from_config("config.yaml")
22
+ # CLI: urkit teach (auto-loads config.yaml)
23
+ # CLI: urkit teach --config station_a.yaml
24
+
25
+ # Robot IP address (required)
26
+ robot_ip: 192.168.1.50
27
+
28
+ # Path to SQLite points database (optional, created if missing)
29
+ points_path: points.db
30
+
31
+ # Gripper preset (optional)
32
+ # Built-in: hand-e, 2f-85, 2f-140
33
+ # Digital I/O: digital
34
+ # Omit or set to none for no gripper
35
+ gripper: none
36
+
37
+ # Gripper overrides (optional)
38
+ gripper_config:
39
+ # Physical properties (override preset defaults)
40
+ # mass: 1.0 # kg
41
+ # center_of_gravity: [0.0, 0.0, 0.0] # [x, y, z] in meters
42
+ # tcp_offset: [0.0, 0.0, 0.175, 0.0, 0.0, 0.0] # [x, y, z, rx, ry, rz]
43
+
44
+ # Robotiq-specific (hand-e, 2f-85, 2f-140)
45
+ # force: 100 # grip force 0-100
46
+ # speed: 100 # movement speed 0-100
47
+ # max_mm: 85 # max finger travel (e.g. 85 for 2F-85)
48
+
49
+ # Digital gripper
50
+ # pin: 0 # digital output pin
51
+ # close_on_high: true # true=HIGH closes, false=LOW closes
52
+
53
+ # IK reference posture (optional)
54
+ # Set to a saved point name to prevent elbow/wrist flipping between moves
55
+ # ik_reference: home
56
+
57
+ # Default motion parameters (optional)
58
+ # default_vel: 0.5 # m/s
59
+ # default_acc: 0.3 # m/s²
60
+
61
+ # Expert mode (optional) — disable safety speed clamping in CLI
62
+ # expert_mode: false
63
+ """
64
+
65
+
66
+ def init_command(args: object) -> None:
67
+ """Execute the init command to scaffold a default config file.
68
+
69
+ Args:
70
+ args: Parsed arguments with 'output' and 'force' attributes.
71
+ """
72
+ output = Path(getattr(args, "output", "config.yaml") or "config.yaml")
73
+ force = getattr(args, "force", False)
74
+
75
+ if output.exists() and not force:
76
+ print(f"Error: {output} already exists.")
77
+ print(" Remove it first or use --force to overwrite.")
78
+ sys.exit(1)
79
+
80
+ output.write_text(_DEFAULT_TEMPLATE)
81
+ print(f"Created {output}")
82
+ print("Edit it with your robot IP and gripper settings, then run:")
83
+ print(" urkit teach")
@@ -9,9 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import difflib
11
11
  import io
12
- import select
13
12
  import sys
14
- import termios
15
13
  import time
16
14
  from pathlib import Path
17
15
 
@@ -20,6 +18,7 @@ from rich.table import Table
20
18
 
21
19
  from urkit.config import _load_config as load_config
22
20
  from urkit.cli.colors import blue, cyan, dim, yellow
21
+ from urkit.cli.terminal import RawTerminal, read_burst, wait_input, warn_if_windows
23
22
  from urkit.points import Points
24
23
 
25
24
 
@@ -29,6 +28,7 @@ def points_command(args) -> None:
29
28
  Args:
30
29
  args: Parsed arguments from argparse (with points subcommand attributes).
31
30
  """
31
+ warn_if_windows()
32
32
  _explore_points(args)
33
33
 
34
34
 
@@ -120,12 +120,8 @@ def _interactive_points_filter(points_db: Points, all_points: list[str], points_
120
120
  refresh_error = None
121
121
 
122
122
  # Set terminal to raw mode
123
- old_settings = termios.tcgetattr(sys.stdin)
124
- new_settings = termios.tcgetattr(sys.stdin)
125
- new_settings[3] = new_settings[3] & ~(termios.ICANON | termios.ECHO)
126
- termios.tcsetattr(sys.stdin, termios.TCSADRAIN, new_settings)
127
-
128
- fd = sys.stdin.fileno()
123
+ raw = RawTerminal()
124
+ raw.enable()
129
125
 
130
126
  try:
131
127
  while True:
@@ -238,44 +234,41 @@ def _interactive_points_filter(points_db: Points, all_points: list[str], points_
238
234
  last_refresh = time.time()
239
235
 
240
236
  # Check for input (non-blocking to allow periodic refresh)
241
- rlist, _, _ = select.select([fd], [], [], 0.1)
242
- if not rlist:
237
+ if not wait_input(0.1):
243
238
  continue
244
239
 
245
- # Read input
240
+ # Read the full input burst. On Unix an arrow key arrives as
241
+ # \x1b[A in the same burst; on Windows the terminal shim
242
+ # translates arrow keys to the same byte sequence.
246
243
  try:
247
- ch = sys.stdin.read(1)
244
+ text = read_burst(0.0).decode("ascii", errors="replace")
248
245
  except Exception:
249
246
  break
247
+ if not text:
248
+ continue
250
249
 
251
- if ch == "\x1b": # ESC or arrow key or function key
252
- # Use longer timeout to reliably detect sequences
253
- rlist, _, _ = select.select([fd], [], [], 0.2)
254
- if rlist:
255
- # There's more input, likely an arrow or function key sequence
256
- try:
257
- ch2 = sys.stdin.read(1)
258
- if ch2 == "[":
259
- ch3 = sys.stdin.read(1)
260
- if ch3 == "A": # Up arrow - scroll up
261
- scroll = max(0, scroll - 1)
262
- needs_redraw = True
263
- elif ch3 == "B": # Down arrow - scroll down
264
- filtered = [p for p in all_points_sorted if filter_str == "" or filter_str.lower() in p.lower()]
265
- scroll = min(len(filtered) - 1, scroll + 1) if filtered else 0
266
- needs_redraw = True
267
- except Exception:
268
- break
250
+ if text.startswith("\x1b"): # ESC or arrow key or function key
251
+ # A bare ESC may still be the first byte of a sequence
252
+ # that arrives late; wait a bit before treating it as quit.
253
+ if text == "\x1b":
254
+ text = text + read_burst(0.2).decode("ascii", errors="replace")
255
+ if text.endswith("[A"): # Up arrow - scroll up
256
+ scroll = max(0, scroll - 1)
257
+ needs_redraw = True
258
+ elif text.endswith("[B"): # Down arrow - scroll down
259
+ filtered = [p for p in all_points_sorted if filter_str == "" or filter_str.lower() in p.lower()]
260
+ scroll = min(len(filtered) - 1, scroll + 1) if filtered else 0
261
+ needs_redraw = True
269
262
  else:
270
- # No more input, so this was just ESC — quit
263
+ # Just ESC (or an unrecognized sequence) — quit
271
264
  break
272
- elif ch == "\x7f" or ch == "\x08": # Backspace
265
+ elif text == "\x7f" or text == "\x08": # Backspace
273
266
  if filter_str:
274
267
  filter_str = filter_str[:-1]
275
268
  scroll = 0
276
269
  needs_redraw = True
277
- elif ch.isprintable():
278
- filter_str += ch
270
+ elif text.isprintable():
271
+ filter_str += text
279
272
  scroll = 0
280
273
  needs_redraw = True
281
274
 
@@ -283,7 +276,7 @@ def _interactive_points_filter(points_db: Points, all_points: list[str], points_
283
276
  # Ctrl+C — exit gracefully without traceback
284
277
  pass
285
278
  finally:
286
- termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
279
+ raw.disable()
287
280
  sys.stdout.write("\n")
288
281
 
289
282