Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
266 changes: 266 additions & 0 deletions MACOS_SETUP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
# LEAP Hand — macOS Setup Guide

Complete end-to-end guide for first-time users on any Mac. No prior experience assumed.

---

## Prerequisites

Before starting, confirm you have the following:

| Requirement | How to check |
|---|---|
| macOS 12 or later | Apple menu → About This Mac |
| Python 3 | Open Terminal, run `python3 --version` — if missing, download from [python.org](https://www.python.org/downloads/) |
| Git | Run `git --version` in Terminal — if missing, macOS will offer to install it automatically |
| Data USB cable | Must support data transfer, not charge-only |
| 5V power supply | Required — USB alone does not power the motors |

---

## Step 1 — Open Terminal

Press `Cmd + Space`, type **Terminal**, press Enter.

All commands in this guide are typed into Terminal and run by pressing Enter.

---

## Step 2 — Clone the Repository

Download the LEAP Hand code to your Mac:

```bash
git clone https://github.com/leap-hand/LEAP_Hand_API.git
```

Navigate into the project:

```bash
cd LEAP_Hand_API/python
```

You are now inside `LEAP_Hand_API/python/` — run everything from here.

---

## Step 3 — Create a Virtual Environment

A virtual environment keeps dependencies isolated from the rest of your Mac. Do this once.

```bash
python3 -m venv test_env
```

Activate it:

```bash
source test_env/bin/activate
```

Your Terminal prompt will now start with `(test_env)` — this means it is active.

> Every time you open a new Terminal window, re-run `source test_env/bin/activate` before using the hand.

---

## Step 4 — Install Dependencies

With the environment active:

```bash
pip install dynamixel_sdk numpy
```

Takes about 30 seconds. Do this once only.

---

## Step 5 — Connect the Hardware

1. Plug in the **5V power supply** first — motors should briefly twitch or LEDs light up
2. Plug the **Micro-USB cable** from the hand into your Mac
- Use a direct port — avoid USB hubs or long extension cables
- Charge-only cables will not work

---

## Step 6 — Find Your Device Name

macOS automatically assigns each USB device a unique name. Run:

```bash
ls /dev/tty.usb*
```

Expected output:

```
/dev/tty.usbserial-FT94CRX2
```

**What each part means:**

| Part | Meaning |
|---|---|
| `/dev/` | Folder where macOS stores all hardware devices |
| `tty.` | This is a serial communication device |
| `usbserial-` | Connected over USB |
| `FT94CRX2` | Unique ID of your hand's USB chip — **yours will be different** |

**If nothing shows up:**

| Possible cause | Fix |
|---|---|
| Hand not powered | Plug in 5V power first, then USB |
| Charge-only cable | Replace with a data-capable cable |
| Loose connection | Unplug, wait 5 seconds, replug |
| Wrong USB port | Try a different port on your Mac |

---

## Step 7 — Grant Serial Port Permission

macOS blocks serial port access by default. Unlock it:

```bash
sudo chmod 777 /dev/tty.usbserial-*
```

You will be asked for your Mac login password.
The password **will not appear as you type** — no dots, no characters. This is normal. Type it and press Enter.

> Run this command every time you unplug and replug the hand. Permissions reset on each connection.

---

## Step 8 — Run

Confirm `(test_env)` is visible in your prompt, then:

```bash
python main.py
```

---

## Step 9 — What You Should See

```
Position: [3.1385 3.1308 3.1400 3.1400 3.1415 3.1400 3.1385 3.1446 3.1339 3.1339 3.1415 3.1400 3.1385 3.1400 3.1277 3.1369]
Position: [3.1385 3.1308 3.1400 3.1400 3.1415 3.1400 3.1385 3.1446 ...]
```

**What those numbers mean:**

| Value | Meaning |
|---|---|
| 16 numbers | One per motor joint (4 per finger × 4 fingers) |
| Near `3.14` | Finger is in flat, fully open home position |
| Updates at ~20 Hz | Script reads and prints positions continuously |

Press `Ctrl + C` to stop.

---

## Quick Reference — Every Session

```bash
# 1. Navigate to the code folder
cd ~/LEAP_Hand_API/python

# 2. Activate the virtual environment
source test_env/bin/activate

# 3. Grant USB permission (re-run after every replug)
sudo chmod 777 /dev/tty.usbserial-*

# 4. Run
python main.py
```

---

## Troubleshooting

| Error | Cause | Fix |
|---|---|---|
| `ls: /dev/tty.usb*: No such file or directory` | Mac cannot see the hand | Check power, cable, port. Replug. |
| `RuntimeError: Could not connect to LEAP Hand` | Permission not granted or device replugged | Run `sudo chmod 777 /dev/tty.usbserial-*` |
| `command not found: python` | Wrong command or env not active | Use `python3`, or activate env first |
| `(test_env)` not in prompt | Virtual environment not active | Run `source test_env/bin/activate` |
| `Incorrect status packet!` repeating | macOS USB latency — not a hardware fault | Hand still works. See latency fix below. |
| Motors not moving | No 5V power | Check power supply connection |
| Motors flash red | Overload | Power cycle. Lower `curr_lim` in `main.py`. |
| Motor off by 90° or 180° | Horn mounted wrong | Remount the horn (hardware fix) |

---

## Optional — Fix USB Latency Errors (Advanced)

The `Incorrect status packet!` warnings are caused by the FTDI USB chip's latency timer defaulting to 16 ms. Reducing it to 1 ms eliminates most errors.

**Steps:**

1. Install the [FTDI VCP driver for macOS](https://ftdichip.com/drivers/vcp-drivers/)
2. Find your device ID from Step 6 (the part after `usbserial-`, e.g. `FT94CRX2`)
3. Run:

```bash
sudo /usr/local/bin/ftdi_latency_timer /dev/tty.usbserial-FT94CRX2 1
```

Replace `FT94CRX2` with your own device ID.

---

## Project Structure

```
LEAP_Hand_API/
├── python/
│ ├── main.py ← entry point — edit this for your application
│ ├── leap_hand_utils/
│ │ ├── dynamixel_client.py ← USB motor communication
│ │ └── leap_hand_utils.py ← joint angle conversion helpers
│ └── test_env/ ← virtual environment (do not delete)
├── ros_module/ ← ROS 1 integration
├── ros2_module/ ← ROS 2 integration
├── cpp/ ← C++ API
├── useful_tools/ ← MANO mapping and other utilities
├── MACOS_SETUP.md ← this file
└── readme.md ← general overview
```

---

## API Reference

All control happens through the `LeapNode` class in `main.py`.

### Command methods

| Method | Convention | Open position |
|---|---|---|
| `set_leap(pose)` | LEAP native | `3.14` rad |
| `set_allegro(pose)` | Allegro compat | `0.0` rad |
| `set_ones(pose)` | Normalized sim | `-1` to `1` |

### Read methods

| Method | Returns |
|---|---|
| `read_pos()` | 16 joint positions (radians) |
| `read_vel()` | 16 joint velocities |
| `read_cur()` | 16 motor currents (milliamps) |
| `pos_vel()` | Positions + velocities (faster combined read) |
| `pos_vel_eff_srv()` | Positions + velocities + currents (fastest combined read) |

### Joint numbering

```
Index → 0 (MCP Side) 1 (MCP Fwd) 2 (PIP) 3 (DIP)
Middle → 4 5 6 7
Ring → 8 9 10 11
Thumb → 12 13 14 15
```
28 changes: 19 additions & 9 deletions python/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import glob
import platform
import numpy as np

from leap_hand_utils.dynamixel_client import *
Expand Down Expand Up @@ -35,16 +37,24 @@ def __init__(self):
# For example ls /dev/serial/by-id/* to find your LEAP Hand. Then use the result.
# For example: /dev/serial/by-id/usb-FTDI_USB__-__Serial_Converter_FT7W91VW-if00-port0
self.motors = motors = [0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]
try:
self.dxl_client = DynamixelClient(motors, '/dev/ttyUSB0', 4000000)
self.dxl_client.connect()
except Exception:
# Build candidate port list based on OS to avoid slow timeouts on non-existent ports
if platform.system() == 'Darwin':
candidate_ports = sorted(glob.glob('/dev/tty.usbserial-*') + glob.glob('/dev/tty.usbmodem*'))
elif platform.system() == 'Windows':
candidate_ports = ['COM' + str(i) for i in range(1, 20)]
else:
candidate_ports = ['/dev/ttyUSB' + str(i) for i in range(4)]
self.dxl_client = None
for port in candidate_ports:
try:
self.dxl_client = DynamixelClient(motors, '/dev/ttyUSB1', 4000000)
self.dxl_client.connect()
client = DynamixelClient(motors, port, 4000000)
client.connect()
self.dxl_client = client
break
except Exception:
self.dxl_client = DynamixelClient(motors, 'COM13', 4000000)
self.dxl_client.connect()
continue
if self.dxl_client is None:
raise RuntimeError('Could not connect to LEAP Hand on any port: ' + str(candidate_ports))
#Enables position-current control mode and the default parameters, it commands a position and then caps the current so the motors don't overload
self.dxl_client.sync_write(motors, np.ones(len(motors))*5, 11, 1)
self.dxl_client.set_torque_enabled(motors, True)
Expand Down Expand Up @@ -96,7 +106,7 @@ def main(**kwargs):
#Set to an open pose and read the joint angles 33hz
leap_hand.set_allegro(np.zeros(16))
print("Position: " + str(leap_hand.read_pos()))
time.sleep(0.03)
time.sleep(0.05)

if __name__ == "__main__":
main()
Loading