Skip to content

Commit 0552516

Browse files
committed
install arduino-cli and arduino USB connection
- automatic installation of arduino cli on startup - Added a node to initialize communication between Arduino and ComfyUI with board list and port
1 parent 157e0a9 commit 0552516

File tree

7 files changed

+256
-1
lines changed

7 files changed

+256
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
Here are the initial goals for the project:
88

9-
- [ ] **Self-contained Installer Node:** Automatically downloads and manages `arduino-cli` locally. No manual setup required from the user, ensuring a smooth "all-in-one" experience.
9+
- [x] **Self-contained Installer Node:** Automatically downloads and manages `arduino-cli` locally. No manual setup required from the user, ensuring a smooth "all-in-one" experience.
1010
- [ ] **Dynamic Upload Node:** Visually build logic with nodes, which will then generate, compile, and upload a custom sketch to your connected Arduino board.
1111
- [ ] **Real-time Communication:** Implement a standard sketch and corresponding nodes to send live data (e.g., servo angles, LED colors) from ComfyUI to a running Arduino without re-uploading.
1212
- [ ] **Example Workflows:** Provide simple, functional examples to help users get started quickly.

__init__.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# __init__.py
2+
3+
from .nodes import ArduinoTargetNode # <-- NOM MIS À JOUR
4+
5+
# --- Mappings for ComfyUI ---
6+
7+
NODE_CLASS_MAPPINGS = {
8+
"ArduinoTarget": ArduinoTargetNode, # <-- NOM MIS À JOUR
9+
}
10+
11+
NODE_DISPLAY_NAME_MAPPINGS = {
12+
"ArduinoTarget": "Define Arduino Target", # <-- NOM MIS À JOUR
13+
}
14+
15+
print("...Custom Nodes: ComfyUI-Arduino loaded.")

nodes.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# nodes.py
2+
3+
import os
4+
from .src.arduino_installer import setup_arduino_cli
5+
from .src.arduino_board_finder import get_available_boards, get_fqbn_by_name
6+
import serial.tools.list_ports
7+
8+
def list_physical_ports():
9+
ports = serial.tools.list_ports.comports()
10+
return [f"{p.device} - {p.description}" for p in ports] if ports else ["No COM ports found"]
11+
12+
# --------------------------------------------------------------------
13+
# -- GLOBAL INITIALIZATION (RUNS ONCE AT STARTUP) --------------------
14+
# --------------------------------------------------------------------
15+
NODE_DIR = os.path.dirname(os.path.abspath(__file__))
16+
print("--- Initializing ComfyUI-Arduino: Setting up arduino-cli ---")
17+
18+
# The setup function now returns all the paths we need or an error
19+
ARDUINO_CLI_PATH, ARDUINO_CONFIG_PATH, SETUP_ERROR = setup_arduino_cli(NODE_DIR)
20+
21+
AVAILABLE_BOARDS = ["Error: CLI setup failed"]
22+
AVAILABLE_PORTS = ["Error: pyserial missing or failed"]
23+
24+
if SETUP_ERROR is None:
25+
print("--- Fetching board lists and COM ports ---")
26+
AVAILABLE_BOARDS = get_available_boards(ARDUINO_CLI_PATH, ARDUINO_CONFIG_PATH)
27+
AVAILABLE_PORTS = list_physical_ports()
28+
print(f" Found {len(AVAILABLE_BOARDS)} board types and {len(AVAILABLE_PORTS)} COM ports.")
29+
print("--- ComfyUI-Arduino initialized successfully. ---")
30+
else:
31+
print(f"FATAL: ComfyUI-Arduino setup failed: {SETUP_ERROR}")
32+
33+
# --------------------------------------------------------------------
34+
# -- NODE: DEFINE ARDUINO TARGET -------------------------------------
35+
# --------------------------------------------------------------------
36+
class ArduinoTargetNode:
37+
@classmethod
38+
def INPUT_TYPES(s):
39+
return {
40+
"required": {
41+
"board_name": (AVAILABLE_BOARDS, ),
42+
"port_str": (AVAILABLE_PORTS, ),
43+
}
44+
}
45+
46+
RETURN_TYPES = ("STRING", "STRING", "STRING")
47+
RETURN_NAMES = ("port", "fqbn", "status")
48+
FUNCTION = "define_target"
49+
CATEGORY = "Arduino"
50+
51+
def define_target(self, board_name, port_str):
52+
if SETUP_ERROR is not None:
53+
return ("ERROR", "ERROR", f"Setup failed: {SETUP_ERROR}")
54+
55+
port = port_str.split(' ')[0]
56+
print(f"--- Defining target: Board '{board_name}' on port '{port}' ---")
57+
58+
fqbn, error = get_fqbn_by_name(ARDUINO_CLI_PATH, ARDUINO_CONFIG_PATH, board_name)
59+
60+
if error:
61+
return ("ERROR", "ERROR", error)
62+
63+
status_message = f"✅ Target defined: {board_name} ({fqbn}) on {port}"
64+
return (port, fqbn, status_message)

requirements.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# requirements.txt
2+
requests
3+
pyserial

src/arduino_board_finder.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# src/arduino_board_finder.py
2+
3+
from .cli_utils import run_cli_command
4+
5+
def get_available_boards(cli_path: str, config_path: str) -> list[str]:
6+
"""
7+
Gets a list of all board names known by the installed cores.
8+
"""
9+
success, data = run_cli_command(cli_path, config_path, ["board", "listall", "--format", "json"], expect_json=True)
10+
11+
if not success:
12+
print(f"❌ Error fetching all available boards: {data}")
13+
return ["Error: Could not list boards"]
14+
15+
if 'boards' in data and data.get('boards'):
16+
return sorted(list(set(b['name'] for b in data['boards'])))
17+
18+
return ["Error: No boards found by listall command"]
19+
20+
def get_fqbn_by_name(cli_path: str, config_path: str, board_name: str) -> tuple[str | None, str | None]:
21+
"""
22+
Finds the FQBN for a given board name.
23+
"""
24+
success, data = run_cli_command(cli_path, config_path, ["board", "listall", "--format", "json"], expect_json=True)
25+
26+
if not success or 'boards' not in data:
27+
return None, f"Could not retrieve board list to find FQBN for '{board_name}'"
28+
29+
for board in data['boards']:
30+
if board.get('name') == board_name:
31+
return board.get('fqbn'), None
32+
33+
return None, f"FQBN for '{board_name}' not found."

src/arduino_installer.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# src/arduino_installer.py
2+
3+
import os
4+
import platform
5+
import shutil
6+
import requests
7+
import zipfile
8+
import tarfile
9+
import io
10+
import textwrap
11+
from .cli_utils import run_cli_command
12+
13+
# --- Constants ---
14+
BASE_URL = "https://downloads.arduino.cc/arduino-cli/arduino-cli_latest"
15+
BIN_DIR_NAME = "bin"
16+
DATA_DIR_NAME = "arduino_data"
17+
CONFIG_FILE_NAME = "arduino-cli.yaml"
18+
CORE_TO_INSTALL = "arduino:avr"
19+
20+
def get_platform_specific_url():
21+
system = platform.system()
22+
arch = platform.machine()
23+
if system == "Windows": return f"{BASE_URL}_Windows_64bit.zip"
24+
if system == "Linux":
25+
return f"{BASE_URL}_Linux_ARM64.tar.gz" if "aarch64" in arch or "arm64" in arch else f"{BASE_URL}_Linux_64bit.tar.gz"
26+
if system == "Darwin": # macOS
27+
return f"{BASE_URL}_macOS_ARM64.tar.gz" if "arm64" in arch else f"{BASE_URL}_macOS_64bit.tar.gz"
28+
raise NotImplementedError(f"Unsupported OS: {system} {arch}")
29+
30+
def get_cli_executable_path(install_dir: str) -> str:
31+
bin_dir = os.path.join(install_dir, BIN_DIR_NAME)
32+
executable_name = "arduino-cli.exe" if platform.system() == "Windows" else "arduino-cli"
33+
return os.path.join(bin_dir, executable_name)
34+
35+
def setup_arduino_cli(install_dir: str) -> tuple[str | None, str | None, str | None]:
36+
cli_path = get_cli_executable_path(install_dir)
37+
data_dir = os.path.join(install_dir, DATA_DIR_NAME)
38+
config_path = os.path.join(install_dir, CONFIG_FILE_NAME)
39+
40+
if not os.path.exists(cli_path):
41+
print(" arduino-cli not found. Starting download and installation...")
42+
bin_dir = os.path.dirname(cli_path)
43+
if os.path.exists(bin_dir): shutil.rmtree(bin_dir)
44+
os.makedirs(bin_dir, exist_ok=True)
45+
try:
46+
url = get_platform_specific_url()
47+
print(f" Downloading from: {url}")
48+
response = requests.get(url, stream=True); response.raise_for_status()
49+
print(" Extracting archive...")
50+
if url.endswith(".zip"):
51+
with zipfile.ZipFile(io.BytesIO(response.content)) as z: z.extractall(bin_dir)
52+
else:
53+
with tarfile.open(fileobj=io.BytesIO(response.content), mode="r:gz") as t: t.extractall(bin_dir)
54+
if platform.system() != "Windows": os.chmod(cli_path, 0o755)
55+
print(f"✅ arduino-cli installed successfully: {cli_path}")
56+
except Exception as e:
57+
return None, None, f"Critical error during arduino-cli download/extraction: {e}"
58+
59+
if not os.path.exists(config_path):
60+
print(f"--- First time setup: Manually creating a clean arduino-cli.yaml ---")
61+
os.makedirs(data_dir, exist_ok=True)
62+
data_dir_fwd = data_dir.replace('\\', '/')
63+
config_content = textwrap.dedent(f"""
64+
directories:
65+
data: {data_dir_fwd}
66+
downloads: {data_dir_fwd}/downloads
67+
user: {data_dir_fwd}/user
68+
""").strip()
69+
try:
70+
with open(config_path, 'w') as f: f.write(config_content)
71+
print(f" Local config file created at: {config_path}")
72+
except Exception as e:
73+
return None, None, f"Failed to write config file: {e}"
74+
75+
print("--- Verifying arduino-cli core installation ---")
76+
success, data = run_cli_command(cli_path, config_path, ["core", "list", "--format", "json"], expect_json=True)
77+
if not success: return None, None, f"Error checking installed cores: {data}"
78+
79+
# *** THE ONE AND ONLY FIX IS HERE ***
80+
# The key is 'platforms', not 'result'.
81+
core_list = data.get('platforms', []) if isinstance(data, dict) else []
82+
83+
core_is_installed = any(CORE_TO_INSTALL in core.get('id', '') for core in core_list if isinstance(core, dict))
84+
85+
if core_is_installed:
86+
print(f"✅ Core '{CORE_TO_INSTALL}' is already installed.")
87+
else:
88+
print(f" Core '{CORE_TO_INSTALL}' not found. Installing...")
89+
print(" Updating core index... (This may take a moment)")
90+
success, res = run_cli_command(cli_path, config_path, ["core", "update-index"])
91+
if not success: return None, None, f"Error updating core index: {res}"
92+
93+
print(f" Installing core '{CORE_TO_INSTALL}'...")
94+
success, res = run_cli_command(cli_path, config_path, ["core", "install", CORE_TO_INSTALL])
95+
if not success: return None, None, f"Error installing core: {res}"
96+
print(f"✅ Core '{CORE_TO_INSTALL}' installed successfully.")
97+
98+
return cli_path, config_path, None

src/cli_utils.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# src/cli_utils.py
2+
3+
import subprocess
4+
import json
5+
import os
6+
7+
def run_cli_command(cli_path: str, config_path: str, args: list, expect_json=False):
8+
"""
9+
Runs a command with arduino-cli using a specific config file.
10+
"""
11+
# Prepend the config file argument to every command
12+
command = [cli_path, "--config-file", config_path] + args
13+
14+
startupinfo = None
15+
if os.name == 'nt':
16+
startupinfo = subprocess.STARTUPINFO()
17+
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
18+
19+
try:
20+
# We don't print the full command anymore to reduce log spam
21+
# print(f" Running command: {' '.join(command)}")
22+
process = subprocess.run(
23+
command,
24+
capture_output=True,
25+
text=True,
26+
check=True,
27+
startupinfo=startupinfo
28+
)
29+
30+
stdout = process.stdout.strip()
31+
32+
if expect_json:
33+
return True, json.loads(stdout) if stdout else {}
34+
35+
return True, stdout
36+
37+
except subprocess.CalledProcessError as e:
38+
error_message = f"Command failed: {' '.join(args)}\n" \
39+
f"Stderr: {e.stderr.strip()}"
40+
return False, error_message
41+
except Exception as e:
42+
return False, f"An unexpected error occurred running {' '.join(args)}: {e}"

0 commit comments

Comments
 (0)