Skip to content

Commit 1ac6560

Browse files
committed
Node Management and Real-Time Communication Enhancements
Addition of functional nodes - Added analogWrite node - Added digitalWrite node Real-time system management via request–acknowledgment communication - Added a data-sending node - Added a data-receiving node Temporary node removal for raw code execution added two new workflows
1 parent 8c74aae commit 1ac6560

File tree

12 files changed

+413
-90
lines changed

12 files changed

+413
-90
lines changed

README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@
88
<img src="docs/image1.png" alt="workflow screen">
99
</p>
1010

11+
<p align="center">
12+
<img src="docs/image2.png" alt="workflow screen">
13+
</p>
14+
1115

1216
Here are the initial goals for the project:
1317

1418
- [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.
1519
- [x] **Dynamic Upload Node:** Visually build logic with nodes, which will then generate, compile, and upload a custom sketch to your connected Arduino board.
16-
- [ ] **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.
20+
- [X] **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.
1721
- [x] **Example Workflows:** Provide simple, functional examples to help users get started quickly.
1822

1923
### other features
@@ -22,7 +26,7 @@ Ideas for the long-term development of the project:
2226

2327
- [ ] **Arduino Library Management:** A node to automatically install required libraries for your sketch using `arduino-cli`.
2428
- [ ] **High-Level Hardware Nodes:** Easy-to-use nodes for common components (servos, sensors, NeoPixel LEDs, etc.) that abstract away the low-level code.
25-
- [x] **In-Workflow C++ Editor:** A dedicated node to write or paste raw Arduino (C++) code directly within ComfyUI.
29+
- [ ] **In-Workflow C++ Editor:** A dedicated node to write or paste raw Arduino (C++) code directly within ComfyUI.
2630
- [ ] **Full Sketch Export:** An option to export the generated C++ code and a list of its dependencies, allowing it to be used outside of ComfyUI.
2731

2832
### Installation

__init__.py

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,51 @@
11
# __init__.py
22

3-
from .nodes import ArduinoTargetNode, ArduinoCodeNode, ArduinoCompileUploadNode
3+
from .nodes import (
4+
ArduinoTargetNode,
5+
ArduinoCompileUploadNode
6+
)
7+
from .arduino_native_nodes import (
8+
ArduinoCreateVariableNode, # <-- NOUVEAU pour la flexibilité
9+
ArduinoDigitalWriteNode,
10+
ArduinoAnalogWriteNode,
11+
ArduinoDelayNode,
12+
ArduinoVariableInfoNode # <-- NOUVEAU pour l'info
13+
)
14+
from .arduino_comms_nodes import (
15+
ArduinoSenderNode,
16+
ArduinoReceiverNode
17+
)
418

5-
# --- Mappings for ComfyUI ---
619

20+
# --- Mappings for ComfyUI ---
721
NODE_CLASS_MAPPINGS = {
22+
# Workflow 1: Build
823
"ArduinoTarget": ArduinoTargetNode,
9-
"ArduinoCode": ArduinoCodeNode,
24+
"ArduinoCreateVariable": ArduinoCreateVariableNode,
25+
"ArduinoDigitalWrite": ArduinoDigitalWriteNode,
26+
"ArduinoAnalogWrite": ArduinoAnalogWriteNode,
27+
"ArduinoDelay": ArduinoDelayNode,
28+
"ArduinoVariableInfo": ArduinoVariableInfoNode,
1029
"ArduinoCompileUpload": ArduinoCompileUploadNode,
30+
31+
# Workflow 2: Communication
32+
"ArduinoSender": ArduinoSenderNode,
33+
"ArduinoReceiver": ArduinoReceiverNode,
1134
}
1235

1336
NODE_DISPLAY_NAME_MAPPINGS = {
14-
"ArduinoTarget": "Define Arduino Target",
15-
"ArduinoCode": "Arduino Code",
16-
"ArduinoCompileUpload": "Compile & Upload to Arduino",
37+
# Workflow 1
38+
"ArduinoTarget": "1. Define Arduino Target",
39+
"ArduinoCreateVariable": "Native: Create Variable",
40+
"ArduinoDigitalWrite": "Native: DigitalWrite",
41+
"ArduinoAnalogWrite": "Native: AnalogWrite (PWM)",
42+
"ArduinoDelay": "Native: Delay (Non-Blocking)",
43+
"ArduinoVariableInfo": "Show Variable Info",
44+
"ArduinoCompileUpload": "2. Compile & Upload",
45+
46+
# Workflow 2
47+
"ArduinoSender": "Send to Arduino (by Port)",
48+
"ArduinoReceiver": "Receive from Arduino (by Port)",
1749
}
1850

1951
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]

arduino_comms_nodes.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# arduino_comms_nodes.py
2+
3+
from .src.serial_communicator import send_and_receive
4+
from .nodes import ARDUINO_PROFILES
5+
6+
class ArduinoSenderNode:
7+
@classmethod
8+
def INPUT_TYPES(s):
9+
return {
10+
"required": {
11+
"port": ("STRING", {"default": "COM3"}),
12+
"variable_name": ("STRING", {"default": "state_pin_13"}),
13+
"value": ("STRING", {"default": "HIGH", "multiline": False}),
14+
},
15+
}
16+
RETURN_TYPES = ("STRING",); RETURN_NAMES = ("status",); FUNCTION = "send_data"; CATEGORY = "Arduino/Communication"
17+
18+
def send_data(self, port, variable_name, value):
19+
if port not in ARDUINO_PROFILES:
20+
return (f"❌ ERROR: No profile for {port}. Upload code first.",)
21+
22+
comm_map = ARDUINO_PROFILES[port].get("comm_map", {})
23+
if variable_name not in comm_map:
24+
return (f"❌ ERROR: Variable '{variable_name}' not found in profile for {port}.",)
25+
26+
details = comm_map[variable_name]
27+
variable_index = details["index"]
28+
29+
try:
30+
value_str = str(value).strip().upper()
31+
if value_str == "HIGH": int_value = 1
32+
elif value_str == "LOW": int_value = 0
33+
else: int_value = int(float(value_str))
34+
except (ValueError, TypeError):
35+
return (f"❌ ERROR: Invalid value '{value}'. Must be an integer, HIGH, or LOW.",)
36+
37+
command = f"S:{variable_index}:{int_value}\n"
38+
success, response = send_and_receive(port, command)
39+
40+
if not success: return (f"❌ ERROR: {response}",)
41+
42+
expected_ack = f"OK:S:{variable_index}"
43+
if response.startswith(expected_ack):
44+
return (f"✅ Sent {value} to '{variable_name}'.",)
45+
else:
46+
return (f"⚠️ UNEXPECTED RESPONSE: {response}",)
47+
48+
class ArduinoReceiverNode:
49+
@classmethod
50+
def INPUT_TYPES(s):
51+
return {
52+
"required": {
53+
"port": ("STRING", {"default": "COM3"}),
54+
"variable_name": ("STRING", {"default": "my_variable"}),
55+
},
56+
"optional": { "trigger": ("*",), }
57+
}
58+
RETURN_TYPES = ("INT", "STRING",); RETURN_NAMES = ("value", "status",); FUNCTION = "receive_data"; CATEGORY = "Arduino/Communication"
59+
60+
def receive_data(self, port, variable_name, trigger=None):
61+
if port not in ARDUINO_PROFILES:
62+
return (-1, f"❌ ERROR: No profile for {port}. Upload code first.",)
63+
64+
comm_map = ARDUINO_PROFILES[port].get("comm_map", {})
65+
if variable_name not in comm_map:
66+
return (-1, f"❌ ERROR: Variable '{variable_name}' not found in profile for {port}.",)
67+
68+
# THE FIX: This entire 'if' block is removed to allow reading any variable type.
69+
# if details['type'] != 'shared':
70+
# return (-1, f"❌ ERROR: Can only receive from general-purpose variables...")
71+
72+
details = comm_map[variable_name]
73+
variable_index = details["index"]
74+
command = f"G:{variable_index}\n"
75+
success, response = send_and_receive(port, command)
76+
77+
if not success: return (-1, f"❌ ERROR: {response}",)
78+
79+
parts = response.split(':')
80+
if len(parts) == 3 and parts[0] == 'R' and parts[1] == str(variable_index):
81+
try:
82+
value = int(parts[2])
83+
return (value, f"✅ Received {value} from '{variable_name}'.")
84+
except ValueError:
85+
return (-1, f"⚠️ INVALID VALUE in response: {response}")
86+
else:
87+
return (-1, f"⚠️ UNEXPECTED RESPONSE: {response}")

arduino_native_nodes.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# arduino_native_nodes.py
2+
3+
import copy
4+
from .src.code_generator import create_communication_map
5+
6+
ARDUINO_CODE_BLOCK = "ARDUINO_CODE_BLOCK"
7+
8+
def create_empty_code_block():
9+
"""Creates the base data structure for our Arduino code."""
10+
return {
11+
"setup_pins": set(),
12+
"setup_code": [],
13+
"loop_steps": [], # Kept for potential future use, but ignored by current generator
14+
"global_vars": [],
15+
"shared_variable_names": [],
16+
"pin_states": {},
17+
}
18+
19+
def _append_code_to_last_step(steps, code_line):
20+
if not steps or not isinstance(steps[-1], list):
21+
steps.append([])
22+
steps[-1].append(code_line)
23+
24+
class ArduinoCreateVariableNode:
25+
@classmethod
26+
def INPUT_TYPES(s):
27+
return { "required": { "variable_name": ("STRING", {"multiline": False, "default": "my_variable"}) }, "optional": { "code_in": (ARDUINO_CODE_BLOCK,), } }
28+
RETURN_TYPES = (ARDUINO_CODE_BLOCK,); RETURN_NAMES = ("code_out",); FUNCTION = "create_var"; CATEGORY = "Arduino/Native"
29+
def create_var(self, variable_name, code_in=None):
30+
if code_in is None: code_in = create_empty_code_block()
31+
new_code_block = copy.deepcopy(code_in)
32+
if variable_name not in new_code_block["shared_variable_names"]:
33+
new_code_block["shared_variable_names"].append(variable_name)
34+
return (new_code_block,)
35+
36+
class ArduinoVariableInfoNode:
37+
@classmethod
38+
def INPUT_TYPES(s):
39+
return { "required": { "code_block": (ARDUINO_CODE_BLOCK,), } }
40+
RETURN_TYPES = ("STRING",); RETURN_NAMES = ("info",); FUNCTION = "get_info"; CATEGORY = "Arduino/Build"
41+
def get_info(self, code_block):
42+
comm_map = create_communication_map(code_block)
43+
info = "--- Controllable Variables ---\n"
44+
if not comm_map:
45+
info += "None detected. Use DigitalWrite or Create Variable nodes."
46+
else:
47+
for name, details in comm_map.items():
48+
info += f"- {name} (type: {details['type']})\n"
49+
return (info,)
50+
51+
class ArduinoDigitalWriteNode:
52+
@classmethod
53+
def INPUT_TYPES(s):
54+
return { "required": { "pin": ("INT", {"default": 13, "min": 0}), "value": (["HIGH", "LOW"],), }, "optional": { "code_in": (ARDUINO_CODE_BLOCK,), } }
55+
RETURN_TYPES = (ARDUINO_CODE_BLOCK,); RETURN_NAMES = ("code_out",); FUNCTION = "generate_code"; CATEGORY = "Arduino/Native"
56+
def generate_code(self, pin, value, code_in=None):
57+
if code_in is None: code_in = create_empty_code_block()
58+
new_code_block = copy.deepcopy(code_in)
59+
new_code_block["setup_pins"].add(pin)
60+
# RENAMING: Use state_pin_X as the variable name
61+
new_code_block["pin_states"][f"state_pin_{pin}"] = {"type": "digital", "value": value}
62+
# The node no longer adds code to the loop, it just defines the initial state.
63+
return (new_code_block,)
64+
65+
class ArduinoAnalogWriteNode:
66+
@classmethod
67+
def INPUT_TYPES(s):
68+
return { "required": { "pin": ("INT", {"default": 9, "min": 0}), "value": ("INT", {"default": 128, "min": 0, "max": 255}), }, "optional": { "code_in": (ARDUINO_CODE_BLOCK,), } }
69+
RETURN_TYPES = (ARDUINO_CODE_BLOCK,); RETURN_NAMES = ("code_out",); FUNCTION = "generate_code"; CATEGORY = "Arduino/Native"
70+
def generate_code(self, pin, value, code_in=None):
71+
if code_in is None: code_in = create_empty_code_block()
72+
new_code_block = copy.deepcopy(code_in)
73+
new_code_block["setup_pins"].add(pin)
74+
# RENAMING: Use state_pin_X as the variable name
75+
new_code_block["pin_states"][f"state_pin_{pin}"] = {"type": "analog", "value": value}
76+
return (new_code_block,)
77+
78+
class ArduinoDelayNode:
79+
@classmethod
80+
def INPUT_TYPES(s):
81+
return { "required": { "delay_ms": ("INT", {"default": 1000, "min": 0}), }, "optional": { "code_in": (ARDUINO_CODE_BLOCK,), } }
82+
RETURN_TYPES = (ARDUINO_CODE_BLOCK,); RETURN_NAMES = ("code_out",); FUNCTION = "generate_delay"; CATEGORY = "Arduino/Native"
83+
def generate_delay(self, delay_ms, code_in=None):
84+
# This node is now informational and doesn't affect the generated loop code.
85+
if code_in is None: code_in = create_empty_code_block()
86+
return (copy.deepcopy(code_in),)

docs/image1.png

28.8 KB
Loading

docs/image2.png

110 KB
Loading

0 commit comments

Comments
 (0)