nff 0.1.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.
- nff-0.1.0/LICENSE +21 -0
- nff-0.1.0/PKG-INFO +220 -0
- nff-0.1.0/README.md +188 -0
- nff-0.1.0/nff/__init__.py +3 -0
- nff-0.1.0/nff/cli.py +79 -0
- nff-0.1.0/nff/commands/__init__.py +0 -0
- nff-0.1.0/nff/commands/doctor.py +176 -0
- nff-0.1.0/nff/commands/flash.py +190 -0
- nff-0.1.0/nff/commands/init.py +207 -0
- nff-0.1.0/nff/commands/monitor.py +104 -0
- nff-0.1.0/nff/config.py +90 -0
- nff-0.1.0/nff/mcp_server.py +215 -0
- nff-0.1.0/nff/tools/__init__.py +0 -0
- nff-0.1.0/nff/tools/boards.py +90 -0
- nff-0.1.0/nff/tools/installer.py +216 -0
- nff-0.1.0/nff/tools/serial.py +244 -0
- nff-0.1.0/nff/tools/toolchain.py +396 -0
- nff-0.1.0/nff.egg-info/PKG-INFO +220 -0
- nff-0.1.0/nff.egg-info/SOURCES.txt +29 -0
- nff-0.1.0/nff.egg-info/dependency_links.txt +1 -0
- nff-0.1.0/nff.egg-info/entry_points.txt +2 -0
- nff-0.1.0/nff.egg-info/requires.txt +10 -0
- nff-0.1.0/nff.egg-info/top_level.txt +1 -0
- nff-0.1.0/pyproject.toml +61 -0
- nff-0.1.0/setup.cfg +4 -0
- nff-0.1.0/tests/test_boards.py +162 -0
- nff-0.1.0/tests/test_config.py +119 -0
- nff-0.1.0/tests/test_doctor.py +237 -0
- nff-0.1.0/tests/test_mcp.py +219 -0
- nff-0.1.0/tests/test_serial.py +235 -0
- nff-0.1.0/tests/test_toolchain.py +290 -0
nff-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GauthierLechevalier
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
nff-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nff
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Claude Code IoT Bridge — connect Claude to hardware via USB
|
|
5
|
+
Author-email: Gauthier Lechevalier <gauthier.lechevalier26@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/GLechevalier/nff
|
|
8
|
+
Project-URL: Repository, https://github.com/GLechevalier/nff
|
|
9
|
+
Project-URL: Bug Tracker, https://github.com/GLechevalier/nff/issues
|
|
10
|
+
Keywords: arduino,esp32,mcp,claude,iot,serial,embedded
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
18
|
+
Classifier: Topic :: System :: Hardware
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: pyserial>=3.5
|
|
23
|
+
Requires-Dist: mcp>=1.0.0
|
|
24
|
+
Requires-Dist: click>=8.0
|
|
25
|
+
Requires-Dist: rich>=13.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
29
|
+
Requires-Dist: black; extra == "dev"
|
|
30
|
+
Requires-Dist: ruff; extra == "dev"
|
|
31
|
+
Dynamic: license-file
|
|
32
|
+
|
|
33
|
+
# nff — Claude Code IoT Bridge
|
|
34
|
+
|
|
35
|
+
**nff** connects [Claude Code](https://claude.ai/code) to physical hardware over USB. It exposes your board as a set of MCP tools so Claude can autonomously write firmware, compile it, upload it, read serial output, and debug — all from a single conversation.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
you: "Make the LED blink every 200 ms and print the state to serial"
|
|
39
|
+
Claude: [writes sketch] → [compiles] → [uploads to ESP32] → [reads serial] → done
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Supported boards (v1):** Arduino Uno · Mega · Nano · Leonardo · ESP32 (CP210x / CH340) · ESP8266 (FTDI)
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Quickstart
|
|
47
|
+
|
|
48
|
+
### 1. Install
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install nff
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Plug in your board, then run init
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
nff init
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`nff init` does three things automatically:
|
|
61
|
+
- Detects your board by USB vendor/product ID
|
|
62
|
+
- Installs `arduino-cli` if it isn't on your system yet
|
|
63
|
+
- Registers the nff MCP server in `~/.claude/claude_desktop_config.json`
|
|
64
|
+
|
|
65
|
+
Expected output:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
✓ Found: ESP32 (CP210x) on COM10 (vendor: 10c4, product: ea60)
|
|
69
|
+
✓ arduino-cli installed.
|
|
70
|
+
✓ Config written to C:\Users\you\.nff\config.json
|
|
71
|
+
✓ MCP config written to C:\Users\you\.claude\claude_desktop_config.json
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 3. Verify everything works
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
nff doctor
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
All checks should be green. If `arduino-cli` boards/cores are missing, install them:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
arduino-cli core install arduino:avr # Arduino boards
|
|
84
|
+
arduino-cli core install esp32:esp32 # ESP32
|
|
85
|
+
arduino-cli core install esp8266:esp8266 # ESP8266
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 4. Open Claude Code and start talking to your hardware
|
|
89
|
+
|
|
90
|
+
Restart Claude Code (or Claude Desktop) so it picks up the new MCP server. You're ready.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## CLI Reference
|
|
95
|
+
|
|
96
|
+
| Command | Description |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `nff init` | Detect board, install arduino-cli, write config, register MCP server |
|
|
99
|
+
| `nff flash <file>` | Compile and upload a `.ino` sketch or sketch directory |
|
|
100
|
+
| `nff monitor` | Interactive serial monitor (Ctrl+C to exit) |
|
|
101
|
+
| `nff doctor` | Check all dependencies and configuration |
|
|
102
|
+
| `nff install-deps` | Re-download and install arduino-cli |
|
|
103
|
+
| `nff mcp` | Start the MCP server (called automatically by Claude Code) |
|
|
104
|
+
|
|
105
|
+
### `nff flash`
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
nff flash ./blink.ino
|
|
109
|
+
nff flash ./my_sketch/ # sketch directory
|
|
110
|
+
nff flash ./blink.ino --board arduino:avr:uno --port COM3
|
|
111
|
+
nff flash ./blink.ino --manual-reset # for boards with broken auto-reset
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### `nff monitor`
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
nff monitor
|
|
118
|
+
nff monitor --port COM10 --baud 115200
|
|
119
|
+
nff monitor --timeout 10 # stop after 10 seconds
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## MCP Tools (what Claude can call)
|
|
125
|
+
|
|
126
|
+
Once registered, Claude Code has access to these tools:
|
|
127
|
+
|
|
128
|
+
| Tool | What it does |
|
|
129
|
+
|---|---|
|
|
130
|
+
| `list_devices()` | List all connected USB boards |
|
|
131
|
+
| `flash(code, board?, port?)` | Write, compile, and upload a sketch |
|
|
132
|
+
| `serial_read(duration_ms?, port?, baud?)` | Capture serial output for N ms |
|
|
133
|
+
| `serial_write(data, port?, baud?)` | Send a string to the device |
|
|
134
|
+
| `reset_device(port?)` | Toggle DTR to hardware-reset the board |
|
|
135
|
+
| `get_device_info(port?)` | Return port, board name, FQBN, baud rate |
|
|
136
|
+
|
|
137
|
+
All tools fall back to the default device in `~/.nff/config.json` when `port` and `board` are omitted.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Config file
|
|
142
|
+
|
|
143
|
+
Stored at `~/.nff/config.json`, written by `nff init`, editable by hand:
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"version": "1",
|
|
148
|
+
"default_device": {
|
|
149
|
+
"port": "COM10",
|
|
150
|
+
"board": "ESP32 (CP210x)",
|
|
151
|
+
"fqbn": "esp32:esp32:esp32",
|
|
152
|
+
"baud": 115200
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Supported Boards
|
|
160
|
+
|
|
161
|
+
| Board | Vendor ID | Product ID | FQBN |
|
|
162
|
+
|---|---|---|---|
|
|
163
|
+
| Arduino Uno | 2341 | 0043 | `arduino:avr:uno` |
|
|
164
|
+
| Arduino Mega 2560 | 2341 | 0010 | `arduino:avr:mega` |
|
|
165
|
+
| Arduino Leonardo | 2341 | 0036 | `arduino:avr:leonardo` |
|
|
166
|
+
| Arduino Nano | 2341 | 0058 | `arduino:avr:nano` |
|
|
167
|
+
| ESP32 (CP210x) | 10c4 | ea60 | `esp32:esp32:esp32` |
|
|
168
|
+
| ESP32 (CH340) | 1a86 | 7523 | `esp32:esp32:esp32` |
|
|
169
|
+
| ESP8266 (FTDI) | 0403 | 6001 | `esp8266:esp8266:generic` |
|
|
170
|
+
|
|
171
|
+
Board not listed? Open a PR — adding one is [two lines of code](CONTRIBUTING.md#adding-a-new-board).
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Linux: serial port permissions
|
|
176
|
+
|
|
177
|
+
On Linux, serial ports require the `dialout` group:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
sudo usermod -aG dialout $USER
|
|
181
|
+
# then log out and back in
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`nff doctor` will detect this and print the fix if your port is inaccessible.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Repository structure
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
nff/
|
|
192
|
+
├── nff/
|
|
193
|
+
│ ├── cli.py # Click entry point — routes subcommands
|
|
194
|
+
│ ├── mcp_server.py # MCP server — registers all tools for Claude
|
|
195
|
+
│ ├── config.py # Read/write ~/.nff/config.json
|
|
196
|
+
│ ├── commands/
|
|
197
|
+
│ │ ├── init.py # nff init
|
|
198
|
+
│ │ ├── flash.py # nff flash
|
|
199
|
+
│ │ ├── monitor.py # nff monitor
|
|
200
|
+
│ │ └── doctor.py # nff doctor
|
|
201
|
+
│ └── tools/
|
|
202
|
+
│ ├── boards.py # USB vendor ID detection
|
|
203
|
+
│ ├── serial.py # pyserial read/write/stream
|
|
204
|
+
│ ├── toolchain.py # arduino-cli subprocess wrappers
|
|
205
|
+
│ └── installer.py # arduino-cli auto-installer
|
|
206
|
+
├── scripts/
|
|
207
|
+
│ └── install_arduino_cli.py # Standalone installer (thin wrapper)
|
|
208
|
+
├── sketches/
|
|
209
|
+
│ └── blink_esp32/ # Example sketch
|
|
210
|
+
├── tests/
|
|
211
|
+
├── pyproject.toml
|
|
212
|
+
└── CONTRIBUTING.md
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## License
|
|
218
|
+
|
|
219
|
+
MIT — see [LICENSE](LICENSE).
|
|
220
|
+
Copyright (c) 2026 Gauthier Lechevalier
|
nff-0.1.0/README.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# nff — Claude Code IoT Bridge
|
|
2
|
+
|
|
3
|
+
**nff** connects [Claude Code](https://claude.ai/code) to physical hardware over USB. It exposes your board as a set of MCP tools so Claude can autonomously write firmware, compile it, upload it, read serial output, and debug — all from a single conversation.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
you: "Make the LED blink every 200 ms and print the state to serial"
|
|
7
|
+
Claude: [writes sketch] → [compiles] → [uploads to ESP32] → [reads serial] → done
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
**Supported boards (v1):** Arduino Uno · Mega · Nano · Leonardo · ESP32 (CP210x / CH340) · ESP8266 (FTDI)
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
### 1. Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install nff
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### 2. Plug in your board, then run init
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
nff init
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`nff init` does three things automatically:
|
|
29
|
+
- Detects your board by USB vendor/product ID
|
|
30
|
+
- Installs `arduino-cli` if it isn't on your system yet
|
|
31
|
+
- Registers the nff MCP server in `~/.claude/claude_desktop_config.json`
|
|
32
|
+
|
|
33
|
+
Expected output:
|
|
34
|
+
|
|
35
|
+
```
|
|
36
|
+
✓ Found: ESP32 (CP210x) on COM10 (vendor: 10c4, product: ea60)
|
|
37
|
+
✓ arduino-cli installed.
|
|
38
|
+
✓ Config written to C:\Users\you\.nff\config.json
|
|
39
|
+
✓ MCP config written to C:\Users\you\.claude\claude_desktop_config.json
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 3. Verify everything works
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
nff doctor
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
All checks should be green. If `arduino-cli` boards/cores are missing, install them:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
arduino-cli core install arduino:avr # Arduino boards
|
|
52
|
+
arduino-cli core install esp32:esp32 # ESP32
|
|
53
|
+
arduino-cli core install esp8266:esp8266 # ESP8266
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 4. Open Claude Code and start talking to your hardware
|
|
57
|
+
|
|
58
|
+
Restart Claude Code (or Claude Desktop) so it picks up the new MCP server. You're ready.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## CLI Reference
|
|
63
|
+
|
|
64
|
+
| Command | Description |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `nff init` | Detect board, install arduino-cli, write config, register MCP server |
|
|
67
|
+
| `nff flash <file>` | Compile and upload a `.ino` sketch or sketch directory |
|
|
68
|
+
| `nff monitor` | Interactive serial monitor (Ctrl+C to exit) |
|
|
69
|
+
| `nff doctor` | Check all dependencies and configuration |
|
|
70
|
+
| `nff install-deps` | Re-download and install arduino-cli |
|
|
71
|
+
| `nff mcp` | Start the MCP server (called automatically by Claude Code) |
|
|
72
|
+
|
|
73
|
+
### `nff flash`
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
nff flash ./blink.ino
|
|
77
|
+
nff flash ./my_sketch/ # sketch directory
|
|
78
|
+
nff flash ./blink.ino --board arduino:avr:uno --port COM3
|
|
79
|
+
nff flash ./blink.ino --manual-reset # for boards with broken auto-reset
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### `nff monitor`
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
nff monitor
|
|
86
|
+
nff monitor --port COM10 --baud 115200
|
|
87
|
+
nff monitor --timeout 10 # stop after 10 seconds
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## MCP Tools (what Claude can call)
|
|
93
|
+
|
|
94
|
+
Once registered, Claude Code has access to these tools:
|
|
95
|
+
|
|
96
|
+
| Tool | What it does |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `list_devices()` | List all connected USB boards |
|
|
99
|
+
| `flash(code, board?, port?)` | Write, compile, and upload a sketch |
|
|
100
|
+
| `serial_read(duration_ms?, port?, baud?)` | Capture serial output for N ms |
|
|
101
|
+
| `serial_write(data, port?, baud?)` | Send a string to the device |
|
|
102
|
+
| `reset_device(port?)` | Toggle DTR to hardware-reset the board |
|
|
103
|
+
| `get_device_info(port?)` | Return port, board name, FQBN, baud rate |
|
|
104
|
+
|
|
105
|
+
All tools fall back to the default device in `~/.nff/config.json` when `port` and `board` are omitted.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Config file
|
|
110
|
+
|
|
111
|
+
Stored at `~/.nff/config.json`, written by `nff init`, editable by hand:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"version": "1",
|
|
116
|
+
"default_device": {
|
|
117
|
+
"port": "COM10",
|
|
118
|
+
"board": "ESP32 (CP210x)",
|
|
119
|
+
"fqbn": "esp32:esp32:esp32",
|
|
120
|
+
"baud": 115200
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Supported Boards
|
|
128
|
+
|
|
129
|
+
| Board | Vendor ID | Product ID | FQBN |
|
|
130
|
+
|---|---|---|---|
|
|
131
|
+
| Arduino Uno | 2341 | 0043 | `arduino:avr:uno` |
|
|
132
|
+
| Arduino Mega 2560 | 2341 | 0010 | `arduino:avr:mega` |
|
|
133
|
+
| Arduino Leonardo | 2341 | 0036 | `arduino:avr:leonardo` |
|
|
134
|
+
| Arduino Nano | 2341 | 0058 | `arduino:avr:nano` |
|
|
135
|
+
| ESP32 (CP210x) | 10c4 | ea60 | `esp32:esp32:esp32` |
|
|
136
|
+
| ESP32 (CH340) | 1a86 | 7523 | `esp32:esp32:esp32` |
|
|
137
|
+
| ESP8266 (FTDI) | 0403 | 6001 | `esp8266:esp8266:generic` |
|
|
138
|
+
|
|
139
|
+
Board not listed? Open a PR — adding one is [two lines of code](CONTRIBUTING.md#adding-a-new-board).
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## Linux: serial port permissions
|
|
144
|
+
|
|
145
|
+
On Linux, serial ports require the `dialout` group:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
sudo usermod -aG dialout $USER
|
|
149
|
+
# then log out and back in
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
`nff doctor` will detect this and print the fix if your port is inaccessible.
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Repository structure
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
nff/
|
|
160
|
+
├── nff/
|
|
161
|
+
│ ├── cli.py # Click entry point — routes subcommands
|
|
162
|
+
│ ├── mcp_server.py # MCP server — registers all tools for Claude
|
|
163
|
+
│ ├── config.py # Read/write ~/.nff/config.json
|
|
164
|
+
│ ├── commands/
|
|
165
|
+
│ │ ├── init.py # nff init
|
|
166
|
+
│ │ ├── flash.py # nff flash
|
|
167
|
+
│ │ ├── monitor.py # nff monitor
|
|
168
|
+
│ │ └── doctor.py # nff doctor
|
|
169
|
+
│ └── tools/
|
|
170
|
+
│ ├── boards.py # USB vendor ID detection
|
|
171
|
+
│ ├── serial.py # pyserial read/write/stream
|
|
172
|
+
│ ├── toolchain.py # arduino-cli subprocess wrappers
|
|
173
|
+
│ └── installer.py # arduino-cli auto-installer
|
|
174
|
+
├── scripts/
|
|
175
|
+
│ └── install_arduino_cli.py # Standalone installer (thin wrapper)
|
|
176
|
+
├── sketches/
|
|
177
|
+
│ └── blink_esp32/ # Example sketch
|
|
178
|
+
├── tests/
|
|
179
|
+
├── pyproject.toml
|
|
180
|
+
└── CONTRIBUTING.md
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## License
|
|
186
|
+
|
|
187
|
+
MIT — see [LICENSE](LICENSE).
|
|
188
|
+
Copyright (c) 2026 Gauthier Lechevalier
|
nff-0.1.0/nff/cli.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""nff — entry point that wires all subcommands into one CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from nff import __version__
|
|
11
|
+
|
|
12
|
+
if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
|
|
13
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
14
|
+
|
|
15
|
+
console = Console(legacy_windows=False)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Root group
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
@click.group()
|
|
23
|
+
@click.version_option(__version__, "-V", "--version", prog_name="nff")
|
|
24
|
+
def cli() -> None:
|
|
25
|
+
"""nff — Claude Code IoT Bridge.
|
|
26
|
+
|
|
27
|
+
Connects Claude Code to physical hardware devices via USB.
|
|
28
|
+
Run `nff init` to get started.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# Subcommands
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
from nff.commands.init import init # noqa: E402
|
|
37
|
+
from nff.commands.flash import flash # noqa: E402
|
|
38
|
+
from nff.commands.monitor import monitor # noqa: E402
|
|
39
|
+
from nff.commands.doctor import doctor # noqa: E402
|
|
40
|
+
|
|
41
|
+
cli.add_command(init)
|
|
42
|
+
cli.add_command(flash)
|
|
43
|
+
cli.add_command(monitor)
|
|
44
|
+
cli.add_command(doctor)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@cli.command("install-deps")
|
|
48
|
+
@click.option("--force", is_flag=True, help="Reinstall even if already present.")
|
|
49
|
+
def install_deps(force: bool) -> None:
|
|
50
|
+
"""Download and install arduino-cli (runs automatically during `nff init`)."""
|
|
51
|
+
from nff.tools import installer
|
|
52
|
+
console.print("[bold cyan]arduino-cli installer[/bold cyan]")
|
|
53
|
+
try:
|
|
54
|
+
exe = installer.install(force=force)
|
|
55
|
+
if not installer.verify(exe):
|
|
56
|
+
raise SystemExit(1)
|
|
57
|
+
except Exception as exc:
|
|
58
|
+
console.print(f" [bold red]✗[/bold red] {exc}")
|
|
59
|
+
raise SystemExit(1)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@cli.command()
|
|
63
|
+
def mcp() -> None:
|
|
64
|
+
"""Start the MCP server (stdio). Called automatically by Claude Code."""
|
|
65
|
+
from nff.mcp_server import main as _mcp_main
|
|
66
|
+
_mcp_main()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
# Entry point
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
def main() -> None:
|
|
74
|
+
"""Setuptools / pipx entry point: ``nff = "nff.cli:main"``."""
|
|
75
|
+
cli()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""nff doctor — dependency and configuration health check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import pathlib
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
# Fix sys.path when this file is run directly (`python doctor.py`).
|
|
9
|
+
# Python adds commands/ to sys.path, making `nff` unresolvable.
|
|
10
|
+
if __name__ == "__main__":
|
|
11
|
+
_pkg_parent = str(pathlib.Path(__file__).resolve().parents[2])
|
|
12
|
+
if _pkg_parent not in sys.path:
|
|
13
|
+
sys.path.insert(0, _pkg_parent)
|
|
14
|
+
|
|
15
|
+
import importlib.metadata
|
|
16
|
+
import json
|
|
17
|
+
import platform
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import NamedTuple
|
|
21
|
+
|
|
22
|
+
import click
|
|
23
|
+
from rich.console import Console
|
|
24
|
+
|
|
25
|
+
# Windows PowerShell defaults to cp1252 which can't encode ✓/✗.
|
|
26
|
+
# Reconfigure stdout to UTF-8 before Rich initialises its stream.
|
|
27
|
+
if sys.platform == "win32" and hasattr(sys.stdout, "reconfigure"):
|
|
28
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
29
|
+
|
|
30
|
+
from nff import config as cfg_module
|
|
31
|
+
from nff.tools import boards as boards_module
|
|
32
|
+
from nff.tools import toolchain
|
|
33
|
+
|
|
34
|
+
console = Console(legacy_windows=False)
|
|
35
|
+
|
|
36
|
+
_CLAUDE_DESKTOP_CONFIG = Path.home() / ".claude" / "claude_desktop_config.json"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Check(NamedTuple):
|
|
40
|
+
passed: bool
|
|
41
|
+
detail: str # printed next to ✓ / ✗
|
|
42
|
+
fix: str | None = None # hint shown when failed
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------------------------------------------------------------------------
|
|
46
|
+
# Individual checks
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
def check_python() -> Check:
|
|
50
|
+
v = sys.version_info
|
|
51
|
+
label = f"Python {v.major}.{v.minor}.{v.micro}"
|
|
52
|
+
if (v.major, v.minor) >= (3, 10):
|
|
53
|
+
return Check(True, label)
|
|
54
|
+
return Check(False, f"{label} — nff requires Python 3.10+", "Upgrade Python")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def check_arduino_cli() -> Check:
|
|
58
|
+
version = toolchain.arduino_cli_version()
|
|
59
|
+
if version:
|
|
60
|
+
return Check(True, f"{version} ({toolchain.find_arduino_cli()})")
|
|
61
|
+
return Check(
|
|
62
|
+
False,
|
|
63
|
+
"arduino-cli not found",
|
|
64
|
+
"Install from https://arduino.github.io/arduino-cli",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def check_esptool() -> Check:
|
|
69
|
+
version = toolchain.esptool_version()
|
|
70
|
+
if version:
|
|
71
|
+
loc = toolchain.find_esptool() or "python -m esptool"
|
|
72
|
+
return Check(True, f"{version} ({loc})")
|
|
73
|
+
return Check(False, "esptool not found", "Run: pip install esptool")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def check_pyserial() -> Check:
|
|
77
|
+
try:
|
|
78
|
+
version = importlib.metadata.version("pyserial")
|
|
79
|
+
return Check(True, f"pyserial {version}")
|
|
80
|
+
except importlib.metadata.PackageNotFoundError:
|
|
81
|
+
return Check(False, "pyserial not installed", "Run: pip install pyserial")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def check_config() -> Check:
|
|
85
|
+
if not cfg_module.exists():
|
|
86
|
+
return Check(False, "Config not found", "Run: nff init")
|
|
87
|
+
try:
|
|
88
|
+
cfg_module.load()
|
|
89
|
+
return Check(True, f"Config found at {cfg_module.CONFIG_PATH}")
|
|
90
|
+
except cfg_module.ConfigError as exc:
|
|
91
|
+
return Check(
|
|
92
|
+
False,
|
|
93
|
+
f"Config unreadable: {exc}",
|
|
94
|
+
f"Fix or delete {cfg_module.CONFIG_PATH}",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def check_device() -> Check:
|
|
99
|
+
"""Check that a recognised board is detected and its port is openable."""
|
|
100
|
+
# Lazy import — pyserial may not be installed; check_pyserial will flag it.
|
|
101
|
+
try:
|
|
102
|
+
import serial as _serial
|
|
103
|
+
except ImportError:
|
|
104
|
+
return Check(False, "Cannot check device — pyserial missing", "Run: pip install pyserial")
|
|
105
|
+
|
|
106
|
+
devices = boards_module.list_devices()
|
|
107
|
+
if not devices:
|
|
108
|
+
return Check(False, "No recognised board detected", "Plug in a board and run nff init")
|
|
109
|
+
|
|
110
|
+
device = devices[0]
|
|
111
|
+
label = f"Device detected: {device.board} on {device.port}"
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
conn = _serial.Serial(device.port, timeout=0.5)
|
|
115
|
+
conn.close()
|
|
116
|
+
except _serial.SerialException as exc:
|
|
117
|
+
fix = f"Port {device.port} is inaccessible: {exc}"
|
|
118
|
+
if platform.system() == "Linux":
|
|
119
|
+
fix += "\n → Add yourself to the dialout group: sudo usermod -aG dialout $USER"
|
|
120
|
+
return Check(False, f"{label} — port inaccessible", fix)
|
|
121
|
+
|
|
122
|
+
return Check(True, label)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def check_claude_desktop() -> Check:
|
|
126
|
+
if not _CLAUDE_DESKTOP_CONFIG.exists():
|
|
127
|
+
return Check(False, "Claude Desktop config not found", "Run: nff init")
|
|
128
|
+
try:
|
|
129
|
+
data = json.loads(_CLAUDE_DESKTOP_CONFIG.read_text(encoding="utf-8"))
|
|
130
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
131
|
+
return Check(False, f"Claude Desktop config unreadable: {exc}")
|
|
132
|
+
if "nff" not in data.get("mcpServers", {}):
|
|
133
|
+
return Check(
|
|
134
|
+
False,
|
|
135
|
+
"nff not registered in Claude Desktop config",
|
|
136
|
+
"Run: nff init",
|
|
137
|
+
)
|
|
138
|
+
return Check(True, f"Claude Desktop config OK ({_CLAUDE_DESKTOP_CONFIG})")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# Click command
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
_CHECKS = [
|
|
146
|
+
check_python,
|
|
147
|
+
check_arduino_cli,
|
|
148
|
+
check_esptool,
|
|
149
|
+
check_pyserial,
|
|
150
|
+
check_config,
|
|
151
|
+
check_device,
|
|
152
|
+
check_claude_desktop,
|
|
153
|
+
]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@click.command()
|
|
157
|
+
def doctor() -> None:
|
|
158
|
+
"""Check dependencies, config, and device connectivity."""
|
|
159
|
+
any_failed = False
|
|
160
|
+
|
|
161
|
+
for fn in _CHECKS:
|
|
162
|
+
result = fn()
|
|
163
|
+
if result.passed:
|
|
164
|
+
console.print(f" [bold green]✓[/bold green] {result.detail}")
|
|
165
|
+
else:
|
|
166
|
+
console.print(f" [bold red]✗[/bold red] {result.detail}")
|
|
167
|
+
if result.fix:
|
|
168
|
+
console.print(f" [yellow]→[/yellow] {result.fix}")
|
|
169
|
+
any_failed = True
|
|
170
|
+
|
|
171
|
+
if any_failed:
|
|
172
|
+
sys.exit(1)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == "__main__":
|
|
176
|
+
doctor()
|