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 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
@@ -0,0 +1,3 @@
1
+ """nff — Claude Code IoT Bridge."""
2
+
3
+ __version__ = "0.1.0"
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()