haybale-protokol-osc 0.0.3__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.
Files changed (31) hide show
  1. haybale_protokol_osc-0.0.3/.gitignore +42 -0
  2. haybale_protokol_osc-0.0.3/PKG-INFO +12 -0
  3. haybale_protokol_osc-0.0.3/README.md +75 -0
  4. haybale_protokol_osc-0.0.3/haybale_protokol_osc/NOTES.md +49 -0
  5. haybale_protokol_osc-0.0.3/haybale_protokol_osc/OVERVIEW.md +11 -0
  6. haybale_protokol_osc-0.0.3/haybale_protokol_osc/QUICKREF.md +8 -0
  7. haybale_protokol_osc-0.0.3/haybale_protokol_osc/__init__.py +103 -0
  8. haybale_protokol_osc-0.0.3/haybale_protokol_osc/adapters/__init__.py +0 -0
  9. haybale_protokol_osc-0.0.3/haybale_protokol_osc/arg_pins.py +241 -0
  10. haybale_protokol_osc-0.0.3/haybale_protokol_osc/docs/haybale-protokol-osc.node.OscReceiveNode.md +50 -0
  11. haybale_protokol_osc-0.0.3/haybale_protokol_osc/docs/haybale-protokol-osc.node.OscSendNode.md +38 -0
  12. haybale_protokol_osc-0.0.3/haybale_protokol_osc/docs/haybale-protokol-osc.state.OscState.md +9 -0
  13. haybale_protokol_osc-0.0.3/haybale_protokol_osc/driver.py +608 -0
  14. haybale_protokol_osc-0.0.3/haybale_protokol_osc/editors/__init__.py +0 -0
  15. haybale_protokol_osc-0.0.3/haybale_protokol_osc/framing.py +79 -0
  16. haybale_protokol_osc-0.0.3/haybale_protokol_osc/haybale.toml +48 -0
  17. haybale_protokol_osc-0.0.3/haybale_protokol_osc/macros/.gitkeep +0 -0
  18. haybale_protokol_osc-0.0.3/haybale_protokol_osc/nodes/__init__.py +0 -0
  19. haybale_protokol_osc-0.0.3/haybale_protokol_osc/nodes/osc_receive.py +402 -0
  20. haybale_protokol_osc-0.0.3/haybale_protokol_osc/nodes/osc_send.py +230 -0
  21. haybale_protokol_osc-0.0.3/haybale_protokol_osc/panels/__init__.py +0 -0
  22. haybale_protokol_osc-0.0.3/haybale_protokol_osc/pattern.py +88 -0
  23. haybale_protokol_osc-0.0.3/haybale_protokol_osc/settings/__init__.py +0 -0
  24. haybale_protokol_osc-0.0.3/haybale_protokol_osc/shape.py +293 -0
  25. haybale_protokol_osc-0.0.3/haybale_protokol_osc/skins/__init__.py +0 -0
  26. haybale_protokol_osc-0.0.3/haybale_protokol_osc/states/__init__.py +0 -0
  27. haybale_protokol_osc-0.0.3/haybale_protokol_osc/states/osc_state.py +27 -0
  28. haybale_protokol_osc-0.0.3/haybale_protokol_osc/themes/__init__.py +0 -0
  29. haybale_protokol_osc-0.0.3/haybale_protokol_osc/types/__init__.py +0 -0
  30. haybale_protokol_osc-0.0.3/haybale_protokol_osc/widgets/__init__.py +0 -0
  31. haybale_protokol_osc-0.0.3/pyproject.toml +33 -0
@@ -0,0 +1,42 @@
1
+ # Patterns below are anchored with a leading slash (/build/) so they match only
2
+ # at the repo root. An unanchored pattern (build/) matches at EVERY depth —
3
+ # including inside barn/, where it would silently exclude your library's own
4
+ # files. See the note at the end of this file before adding patterns.
5
+
6
+ # Byte-compiled / optimized / DLL files
7
+ __pycache__/
8
+ *.py[cod]
9
+ *$py.class
10
+
11
+ # Distribution / packaging (root only)
12
+ /build/
13
+ /dist/
14
+ *.egg-info/
15
+ *.egg
16
+
17
+ # Virtual environments (root only)
18
+ /.venv/
19
+ /venv/
20
+ /env/
21
+
22
+ # Test / type-check caches
23
+ .pytest_cache/
24
+ .ruff_cache/
25
+ .mypy_cache/
26
+ .tox/
27
+ .coverage
28
+ htmlcov/
29
+
30
+ # Editor / OS
31
+ .DS_Store
32
+ .idea/
33
+ .vscode/
34
+
35
+ # Haywire per-session UI state (open graph, pan/zoom) — not project state
36
+ .haywire/workspace_state.json
37
+
38
+ # ── Before you add a pattern ────────────────────────────────────────────────
39
+ # Anything ignored inside barn/<your-library>/ will be MISSING for everyone who
40
+ # installs your library — haywire share publishes by git URL, so consumers get
41
+ # a clone of this repo, not a built package. If a pattern is only meant for the
42
+ # repo root, anchor it with a leading slash: /build/ not build/.
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.5
2
+ Name: haybale-protokol-osc
3
+ Version: 0.0.3
4
+ Summary: OSC for haybale-protokol: UDP and TCP gates, OSC Receive and Send nodes
5
+ Project-URL: Source, https://github.com/going-haywire/haybale-protokol
6
+ License: MIT
7
+ Keywords: control,network,osc,protocol
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: haybale-core>=0.4.2
10
+ Requires-Dist: haybale-protokol
11
+ Requires-Dist: haywire-core>=0.5.0
12
+ Requires-Dist: python-osc==1.10.2
@@ -0,0 +1,75 @@
1
+ # Protokol OSC
2
+
3
+ # Protokol OSC
4
+
5
+ OSC over UDP and TCP for Haywire, on top of `haybale-protokol`: an OSC Receive node and an OSC Send
6
+ node, each using a gate from the project's `.haywire/protokol.toml` (see the Protokol notes
7
+ for the file format).
8
+
9
+ ## TCP
10
+
11
+ A TCP gate is a client (`role = "client"`, connects to `host`) or a server
12
+ (`role = "server"`, accepts any number of peers on `bind`). Choose the framing your peer
13
+ speaks: `framing = "1.0"` (length prefix) or `"1.1"` (SLIP, the default). A client keeps
14
+ reconnecting (0.5 s, 1 s, 2 s, 4 s, then every 5 s) and shows `reconnecting (n)`; anything sent
15
+ while it is disconnected is dropped and counted, never queued. A server sends each message to
16
+ all its peers. TCP gates may be `two-way`, so one gate can feed an OSC Receive and an OSC Send.
17
+
18
+ A peer that disappears without closing (power cut, lost Wi-Fi) is noticed by TCP keepalive
19
+ after about 11 s; a client then reconnects. A peer that stops reading is disconnected once
20
+ it holds up a send for 0.2 s, rather than slowing the graph down.
21
+
22
+ ## Gates on a node
23
+
24
+ A node's **Gate** setting takes one or more gates. Pick them as chips from the list, which
25
+ offers this project's OSC gates that fit the node (input or two-way for Receive, output or
26
+ two-way for Send), or type a name and press Enter. OSC Receive listens on every listed gate;
27
+ its **From gate** outlet says where a message came in. OSC Send sends each message to every
28
+ listed gate. A listed gate that cannot be used is reported in `status`; the others still work.
29
+
30
+ ## OSC Receive
31
+
32
+ Runs its flow for each message on its input gate whose address matches **Address**, an OSC
33
+ address pattern (`*`, `?`, `[0-9]`, `{a,b}`; empty takes everything). Each wildcard gets a
34
+ `match_N` outlet, which takes the type of whatever you connect it to.
35
+
36
+ **Arg Shape** lists the arguments: OSC type tags (`i f d h s b T r`), groupings
37
+ `vi2`–`vi4`, `vf2`–`vf4`, `m3`, `m4` for vectors and matrices, and `_` / `_n` to skip
38
+ arguments. The `args` outlet always carries every argument as a list; each token adds its own outlet. A message that does not fit is skipped and counted
39
+ in `status`. By default the node fires once at start with the last message the gate saw
40
+ (**Start Value**) and keeps only the newest message when its flow runs behind.
41
+
42
+ ## OSC Send
43
+
44
+ Sends **Address** with one argument per inlet, in inlet order. Connect anything to the empty
45
+ inlet to add an argument. Vectors go out as their components (`vf3` → three `f`). Use `d` or
46
+ `h` in the shape for 64-bit numbers. An int that does not fit a 32-bit `i` is dropped, and
47
+ `status` says which argument it was. Each control tick sends, unless the **Only send changes** setting is on: then
48
+ a tick sends only when the address, type tags or values differ from the last message sent.
49
+
50
+ The Arg shape and the pins follow each other: typing the shape rebuilds the pins, and
51
+ growing, unplugging (a grown pin) or reordering pins rewrites the shape.
52
+
53
+ <!-- marketstall:share-url:start -->
54
+ Always the latest (tracks the current branch):
55
+
56
+ ```sh
57
+ https://github.com/going-haywire/haybale-protokol/blob/master/marketstall.toml
58
+ ```
59
+
60
+ Frozen to this version:
61
+
62
+ ```sh
63
+ https://github.com/going-haywire/haybale-protokol/blob/v0.0.3/marketstall.toml
64
+ ```
65
+ <!-- marketstall:share-url:end -->
66
+
67
+ OSC for haybale-protokol: UDP and TCP gates, OSC Receive and Send nodes
68
+
69
+ ## Nodes
70
+ ### Protokol
71
+ - **OSC Receive** — Triggered for each OSC message on an input gate that matches the address pattern
72
+ - **OSC Send** — Sends one OSC message to an output gate each time it runs
73
+
74
+ ## States
75
+ - **Protokol OSC** — Registers the OSC driver with Protokol
@@ -0,0 +1,49 @@
1
+ # Protokol OSC
2
+
3
+ OSC over UDP and TCP for Haywire, on top of `haybale-protokol`: an OSC Receive node and an OSC Send
4
+ node, each using a gate from the project's `.haywire/protokol.toml` (see the Protokol notes
5
+ for the file format).
6
+
7
+ ## TCP
8
+
9
+ A TCP gate is a client (`role = "client"`, connects to `host`) or a server
10
+ (`role = "server"`, accepts any number of peers on `bind`). Choose the framing your peer
11
+ speaks: `framing = "1.0"` (length prefix) or `"1.1"` (SLIP, the default). A client keeps
12
+ reconnecting (0.5 s, 1 s, 2 s, 4 s, then every 5 s) and shows `reconnecting (n)`; anything sent
13
+ while it is disconnected is dropped and counted, never queued. A server sends each message to
14
+ all its peers. TCP gates may be `two-way`, so one gate can feed an OSC Receive and an OSC Send.
15
+
16
+ A peer that disappears without closing (power cut, lost Wi-Fi) is noticed by TCP keepalive
17
+ after about 11 s; a client then reconnects. A peer that stops reading is disconnected once
18
+ it holds up a send for 0.2 s, rather than slowing the graph down.
19
+
20
+ ## Gates on a node
21
+
22
+ A node's **Gate** setting takes one or more gates. Pick them as chips from the list, which
23
+ offers this project's OSC gates that fit the node (input or two-way for Receive, output or
24
+ two-way for Send), or type a name and press Enter. OSC Receive listens on every listed gate;
25
+ its **From gate** outlet says where a message came in. OSC Send sends each message to every
26
+ listed gate. A listed gate that cannot be used is reported in `status`; the others still work.
27
+
28
+ ## OSC Receive
29
+
30
+ Runs its flow for each message on its input gate whose address matches **Address**, an OSC
31
+ address pattern (`*`, `?`, `[0-9]`, `{a,b}`; empty takes everything). Each wildcard gets a
32
+ `match_N` outlet, which takes the type of whatever you connect it to.
33
+
34
+ **Arg Shape** lists the arguments: OSC type tags (`i f d h s b T r`), groupings
35
+ `vi2`–`vi4`, `vf2`–`vf4`, `m3`, `m4` for vectors and matrices, and `_` / `_n` to skip
36
+ arguments. The `args` outlet always carries every argument as a list; each token adds its own outlet. A message that does not fit is skipped and counted
37
+ in `status`. By default the node fires once at start with the last message the gate saw
38
+ (**Start Value**) and keeps only the newest message when its flow runs behind.
39
+
40
+ ## OSC Send
41
+
42
+ Sends **Address** with one argument per inlet, in inlet order. Connect anything to the empty
43
+ inlet to add an argument. Vectors go out as their components (`vf3` → three `f`). Use `d` or
44
+ `h` in the shape for 64-bit numbers. An int that does not fit a 32-bit `i` is dropped, and
45
+ `status` says which argument it was. Each control tick sends, unless the **Only send changes** setting is on: then
46
+ a tick sends only when the address, type tags or values differ from the last message sent.
47
+
48
+ The Arg shape and the pins follow each other: typing the shape rebuilds the pins, and
49
+ growing, unplugging (a grown pin) or reordering pins rewrites the shape.
@@ -0,0 +1,11 @@
1
+ # Protokol OSC
2
+
3
+ OSC for haybale-protokol: UDP and TCP gates, OSC Receive and Send nodes
4
+
5
+ ## Nodes
6
+ ### Protokol
7
+ - **OSC Receive** — Triggered for each OSC message on an input gate that matches the address pattern
8
+ - **OSC Send** — Sends one OSC message to an output gate each time it runs
9
+
10
+ ## States
11
+ - **Protokol OSC** — Registers the OSC driver with Protokol
@@ -0,0 +1,8 @@
1
+ # haybale-protokol-osc — component index (v0.0.3)
2
+
3
+ ## node
4
+ - `haybale-protokol-osc:node:OscReceiveNode` — OSC Receive — Triggered for each OSC message on an input gate that matches the address pattern _tags: osc, receive, control, network, protokol, touchosc_
5
+ - `haybale-protokol-osc:node:OscSendNode` — OSC Send — Sends one OSC message to an output gate each time it runs _tags: osc, send, control, network, protokol, touchosc_
6
+
7
+ ## state
8
+ - `haybale-protokol-osc:state:OscState` — Protokol OSC — Registers the OSC driver with Protokol
@@ -0,0 +1,103 @@
1
+ """
2
+ OSC for haybale-protokol: the OSC Protocol driver and the OSC Receive and Send nodes.
3
+
4
+ Add your custom components in the corresponding folders:
5
+ - nodes/ — node definitions
6
+ - macros/ — macro documents (.hwm)
7
+ - types/ — custom data types
8
+ - widgets/ — UI widgets for data types
9
+ - skins/ — custom node skins
10
+ - adapters/ — type-to-type conversion adapters
11
+ - settings/ — library settings definitions
12
+ - states/ — library app and session states
13
+ - themes/ — workbench and node themes
14
+ - panels/ — custom UI panels
15
+ - editors/ — custom UI editors
16
+ """
17
+
18
+ from pathlib import Path
19
+
20
+ from haywire.api import (
21
+ AdapterRegistry,
22
+ BaseLibrary,
23
+ MacroRegistry,
24
+ NodeRegistry,
25
+ SettingsRegistry,
26
+ SkinRegistry,
27
+ ThemeRegistry,
28
+ TypeRegistry,
29
+ WidgetRegistry,
30
+ library,
31
+ )
32
+
33
+ from haywire.api.workbench import EditorRegistry, PanelRegistry, StateRegistry
34
+
35
+
36
+ @library(
37
+ file_watcher=True,
38
+ )
39
+ class Library(BaseLibrary):
40
+ """OSC over UDP for haybale-protokol."""
41
+
42
+ def register_components(self):
43
+ """Register all components with the global registries."""
44
+ base_path = Path(__file__).parent
45
+
46
+ self.add_folder_to_registry(
47
+ folder_path=str(base_path / "settings"),
48
+ registry_cls=SettingsRegistry,
49
+ )
50
+
51
+ self.add_folder_to_registry(
52
+ folder_path=str(base_path / "states"),
53
+ registry_cls=StateRegistry,
54
+ )
55
+
56
+ self.add_folder_to_registry(
57
+ folder_path=str(base_path / "themes"),
58
+ registry_cls=ThemeRegistry,
59
+ )
60
+
61
+ self.add_folder_to_registry(
62
+ folder_path=str(base_path / "types"),
63
+ registry_cls=TypeRegistry,
64
+ )
65
+
66
+ self.add_folder_to_registry(
67
+ folder_path=str(base_path / "adapters"),
68
+ registry_cls=AdapterRegistry,
69
+ )
70
+
71
+ self.add_folder_to_registry(
72
+ folder_path=str(base_path / "widgets"),
73
+ registry_cls=WidgetRegistry,
74
+ )
75
+
76
+ self.add_folder_to_registry(
77
+ folder_path=str(base_path / "skins"),
78
+ registry_cls=SkinRegistry,
79
+ )
80
+
81
+ self.add_folder_to_registry(
82
+ folder_path=str(base_path / "nodes"),
83
+ registry_cls=NodeRegistry,
84
+ )
85
+
86
+ self.add_folder_to_registry(
87
+ folder_path=str(base_path / "macros"),
88
+ registry_cls=MacroRegistry,
89
+ )
90
+
91
+ self.add_folder_to_registry(
92
+ folder_path=str(base_path / "panels"),
93
+ registry_cls=PanelRegistry,
94
+ )
95
+
96
+ self.add_folder_to_registry(
97
+ folder_path=str(base_path / "editors"),
98
+ registry_cls=EditorRegistry,
99
+ )
100
+
101
+ def validate(self) -> bool:
102
+ """Validate library structure."""
103
+ return True
@@ -0,0 +1,241 @@
1
+ """Argument pins that follow the Arg shape, shared by OSC Receive and OSC Send.
2
+
3
+ The Arg shape text is the source of truth. Each named token has a pin with a stable id
4
+ (``arg_<n>``, numbers never reused, because an edge's id contains its port's id); the node
5
+ store keeps the ids in shape order. The pin edits a user makes rewrite the text:
6
+
7
+ - connecting the empty ``ADD`` slot appends the edge type's token (undo takes it back);
8
+ - deleting the edge that grew a pin removes the pin and its token (typed pins stay);
9
+ - reordering pins reorders the tokens; ``_`` skips stay where they are.
10
+
11
+ Every rewrite goes through the ``arg_shape`` port, whose ``on_change`` rebuilds the pins,
12
+ inside the user's one action, so each edit is one undo step.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import re
18
+ from typing import Any
19
+
20
+ from haywire.api import ADD, PortOrigin
21
+
22
+ from haybale_protokol_osc.shape import ShapeError, Token, parse_shape, token_for_type
23
+
24
+ ARG_ID = re.compile(r"arg_(\d+)")
25
+
26
+
27
+ class ArgPins:
28
+ """Mixin for a ``BaseNode`` with an ``arg_shape`` config port and ``arg_<n>`` pins."""
29
+
30
+ #: True for Receive (outlets, ``_`` allowed), False for Send (inlets).
31
+ receive: bool = False
32
+
33
+ # Provided by BaseNode (the mixin comes first in the bases, so it defines no stubs).
34
+ ports: dict[str, Any]
35
+ store: Any
36
+ value: Any
37
+ add: Any
38
+ post_value: Any
39
+
40
+ # -- the node supplies --------------------------------------------------------
41
+
42
+ def _arg_spec(self, port_id: str, type_cls: Any, origin: PortOrigin, label: str) -> Any:
43
+ """The inlet or outlet spec of one argument pin."""
44
+ raise NotImplementedError
45
+
46
+ # -- reading ------------------------------------------------------------------
47
+
48
+ def _tokens(self) -> list[Token]:
49
+ """The current Arg shape; raises ``ShapeError`` when the text is invalid."""
50
+ return parse_shape(str(self.value("arg_shape") or ""), receive=self.receive)
51
+
52
+ def _arg_ids(self) -> list[str]:
53
+ return list(getattr(self.store, "arg_ids", []))
54
+
55
+ def _slot_id(self) -> str:
56
+ return str(getattr(self.store, "arg_slot", "arg_1"))
57
+
58
+ def _arg_ports(self) -> list[Any]:
59
+ """The typed argument pins, in shape order."""
60
+ return [self.ports[port_id] for port_id in self._arg_ids() if port_id in self.ports]
61
+
62
+ def _pins_mismatch(self) -> str | None:
63
+ """Why the argument pins no longer match the Arg shape (a pin removed from its menu), or ``None``."""
64
+ try:
65
+ named = [token for token in self._tokens() if token.haywire_type is not None]
66
+ except ShapeError as exc:
67
+ return f"Arg shape: {exc}"
68
+ if len(self._arg_ports()) != len(named):
69
+ return "pins and Arg shape differ: retype the Arg shape"
70
+ return None
71
+
72
+ # -- building -----------------------------------------------------------------
73
+
74
+ def _add_arg_ports(self, tokens: list[Token]) -> None:
75
+ """Add one pin per named token, then the empty slot. Call inside the node's ``rejig``."""
76
+ import haywire.api as builtin
77
+
78
+ from haybale_core.types.specs import BYTES
79
+
80
+ named = [token for token in tokens if token.haywire_type is not None]
81
+ ids = self._arg_ids()[: len(named)]
82
+ slot = self._slot_id()
83
+ while len(ids) < len(named):
84
+ # The empty slot holds no edge (connecting grows it), so its id is free to take.
85
+ ids.append(slot if slot not in ids else self._new_arg_id(ids))
86
+ if slot in ids:
87
+ slot = self._new_arg_id(ids)
88
+ self.store.arg_ids = ids
89
+ self.store.arg_slot = slot
90
+ grown = getattr(self.store, "arg_grown", [])
91
+ self.store.arg_grown = [port_id for port_id in grown if port_id in ids]
92
+ for position, (port_id, token) in enumerate(zip(ids, named, strict=True), start=1):
93
+ name = str(token.haywire_type)
94
+ type_cls = BYTES if name == "BYTES" else getattr(builtin, name)
95
+ self.add(self._arg_spec(port_id, type_cls, PortOrigin.RESOLVED, f"{position}: {token.text}"))
96
+ self.add(self._arg_spec(slot, ADD, PortOrigin.DECLARED, "+"))
97
+
98
+ def _move_slot_last(self) -> None:
99
+ """Put the empty slot after every other port, so it is the last pin of its side."""
100
+ slot = self.ports.get(self._slot_id())
101
+ if slot is None:
102
+ return
103
+ others = [port.order for port in self.ports.values() if port is not slot]
104
+ if others and slot.order <= max(others):
105
+ slot.order = max(others) + 1
106
+ wrapper = getattr(self, "wrapper", None)
107
+ if wrapper is not None:
108
+ wrapper.mark_layout_changed()
109
+
110
+ def _new_arg_id(self, taken: list[str]) -> str:
111
+ used = [int(m.group(1)) for port_id in (*self.ports, *taken) if (m := ARG_ID.fullmatch(port_id))]
112
+ floor = int(getattr(self.store, "arg_next", 1))
113
+ number = max([floor - 1, *used]) + 1
114
+ self.store.arg_next = number + 1
115
+ return f"arg_{number}"
116
+
117
+ def _accept_text(self) -> None:
118
+ """Remember the current config texts as the last ones that applied."""
119
+ for port_id in ("address", "arg_shape"):
120
+ if port_id in self.ports:
121
+ setattr(self.store, f"good_{port_id}", str(self.value(port_id) or ""))
122
+
123
+ def _reject_text(self, port: Any, error: Exception) -> None:
124
+ """Put ``port`` back to its last applied text and say why the new one was refused."""
125
+ self._report(f"not applied: {error}")
126
+ good = str(getattr(self.store, f"good_{getattr(port, 'id', '')}", ""))
127
+ if getattr(port, "id", None) in ("address", "arg_shape") and self.value(port.id) != good:
128
+ port.set_value(good)
129
+
130
+ def _write_shape(self, tokens: list[Token]) -> None:
131
+ """Set the Arg shape text; its ``on_change`` rebuilds the pins."""
132
+ self.ports["arg_shape"].set_value(" ".join(token.text for token in tokens))
133
+
134
+ # -- pin edits ------------------------------------------------------------------
135
+
136
+ def hb_grow(self, port: Any, edge_wrapper: Any) -> None:
137
+ """An edge on the empty slot becomes an argument of the edge's type."""
138
+
139
+ if port.type_cls is None or not issubclass(port.type_cls, ADD) or port.id != self._slot_id():
140
+ return
141
+ other = edge_wrapper._outlet_port if port.is_inlet() else edge_wrapper._inlet_port
142
+ if other is None:
143
+ return
144
+ type_name = getattr(other.stored_type, "__name__", "")
145
+ text = token_for_type(type_name)
146
+ if text is None:
147
+ self._report(f"OSC cannot carry {type_name}")
148
+ return
149
+ try:
150
+ tokens = self._tokens()
151
+ except ShapeError as exc:
152
+ self._report(f"not applied: {exc}")
153
+ return
154
+ self.store.arg_ids = [*self._arg_ids(), port.id]
155
+ self.store.arg_grown = [*getattr(self.store, "arg_grown", []), port.id]
156
+ self.store.arg_slot = self._new_arg_id(self._arg_ids())
157
+ self._write_shape([*tokens, *parse_shape(text, receive=self.receive)])
158
+
159
+ def release_pins(self, port_ids: list[str]) -> list[str]:
160
+ """Remove the grown argument pins whose edge is gone, with their tokens."""
161
+ grown = list(getattr(self.store, "arg_grown", []))
162
+ candidates = [
163
+ port_id
164
+ for port_id in port_ids
165
+ if port_id in grown and port_id in self.ports and not self.ports[port_id].is_linked()
166
+ ]
167
+ if not candidates:
168
+ return []
169
+ try:
170
+ tokens = self._tokens()
171
+ except ShapeError:
172
+ return []
173
+ ids = self._arg_ids()
174
+ stash = dict(getattr(self.store, "arg_released", {}))
175
+ for port_id in candidates:
176
+ index = ids.index(port_id)
177
+ position = _named_positions(tokens)[index]
178
+ stash[port_id] = [index, position, tokens[position].text]
179
+ del tokens[position]
180
+ del ids[index]
181
+ grown.remove(port_id)
182
+ self.store.arg_released = stash
183
+ self.store.arg_ids = ids
184
+ self.store.arg_grown = grown
185
+ self._write_shape(tokens)
186
+ return candidates
187
+
188
+ def restore_pins(self, port_ids: list[str]) -> None:
189
+ """Bring back pins ``release_pins`` removed, at their places in the shape."""
190
+ stash = dict(getattr(self.store, "arg_released", {}))
191
+ try:
192
+ tokens = self._tokens()
193
+ except ShapeError:
194
+ return
195
+ ids = self._arg_ids()
196
+ grown = list(getattr(self.store, "arg_grown", []))
197
+ for port_id in reversed(port_ids):
198
+ if port_id not in stash:
199
+ continue
200
+ index, position, text = stash.pop(port_id)
201
+ tokens.insert(position, *parse_shape(text, receive=self.receive))
202
+ ids.insert(index, port_id)
203
+ grown.append(port_id)
204
+ self.store.arg_released = stash
205
+ self.store.arg_ids = ids
206
+ self.store.arg_grown = grown
207
+ self._write_shape(tokens)
208
+
209
+ def reorder_ports(self, ordered_ids: list[str]) -> None:
210
+ """Reorder pins as the base does, then reorder the Arg shape to match."""
211
+ super().reorder_ports(ordered_ids) # type: ignore[misc]
212
+ try:
213
+ self._reorder_tokens()
214
+ finally:
215
+ self._move_slot_last()
216
+
217
+ def _reorder_tokens(self) -> None:
218
+ ids = self._arg_ids()
219
+ if any(port_id not in self.ports for port_id in ids):
220
+ return # a pin was removed from its menu; the status guard reports it
221
+ new_ids = sorted(ids, key=lambda port_id: self.ports[port_id].order)
222
+ if new_ids == ids:
223
+ return
224
+ try:
225
+ tokens = self._tokens()
226
+ except ShapeError:
227
+ return
228
+ positions = _named_positions(tokens)
229
+ named = {port_id: tokens[position] for port_id, position in zip(ids, positions, strict=True)}
230
+ for position, port_id in zip(positions, new_ids, strict=True):
231
+ tokens[position] = named[port_id]
232
+ self.store.arg_ids = new_ids
233
+ self._write_shape(tokens)
234
+
235
+ def _report(self, text: str) -> None:
236
+ """Show ``text`` on the node's status outlet."""
237
+ self.post_value("status", text)
238
+
239
+
240
+ def _named_positions(tokens: list[Token]) -> list[int]:
241
+ return [position for position, token in enumerate(tokens) if token.haywire_type is not None]
@@ -0,0 +1,50 @@
1
+ # OSC Receive
2
+
3
+ `haybale-protokol-osc:node:OscReceiveNode` · kind: node
4
+
5
+ Triggered for each OSC message on an input gate that matches the address pattern
6
+
7
+ ## Ports
8
+
9
+ | id | direction | type | description |
10
+ |---|---|---|---|
11
+ | `gate` | config | `haywire-core:type:STRING` | One or more input (or two-way) gates from the project's Gate file, .haywire/protokol.toml; pick them from the list, which offers this project's OSC gates that can receive, or type a name and press Enter. Make gates in the Gates editor. The node opens each gate's Line when the graph starts and gives it back when it stops; messages from every gate run the same flow, and the From gate outlet says which one a message came in on. A gate that is missing, of another protocol or whose port is busy is reported in status; the others still work. A change applies on the next start. |
12
+ | `address` | config | `haywire-core:type:STRING` | The OSC address pattern to listen to; empty takes every message on the gate. Wildcards: \* (any text within one /part), ? (one character), [0-9] or [!a] (one character from a set), {left,right} (one of the words). Each wildcard adds a capture outlet, match_1, match_2 … from left to right. A capture starts as an empty + pin and takes the type of the inlet you connect it to (text, int, float or bool); a part that does not convert is reported in status. Changing the pattern rebuilds the match_ outlets; an invalid pattern is put back. |
13
+ | `arg_shape` | config | `haywire-core:type:STRING` | The Arg shape: the arguments the node expects, as tokens separated by spaces. i or h: int; f or d: float; s: text; b: blob; T: bool; r: colour; vi2–vi4 and vf2–vf4: 2 to 4 ints or floats as one vector; m3, m4: 9 or 16 floats as a matrix; _ skips one argument, _3 skips three. The args outlet always carries every argument as a list; each token adds its own outlet, in order. The shape and the pins follow each other: connecting the + pin at the bottom adds a token for the connected type, deleting the edge that grew a pin removes the pin and its token, and dragging pins into another order reorders the tokens. A message that does not fit is skipped and counted in status; extra arguments are ignored. An invalid shape is put back. |
14
+ | `received` | outlet | `haywire-core:type:EXEC` | Signal for controlling execution flow between nodes |
15
+ | `address_out` | outlet | `haywire-core:type:STRING` | Text data |
16
+ | `from_gate` | outlet | `haywire-core:type:STRING` | The gate the message came in on |
17
+ | `args` | outlet | `haybale-core:type:LIST` | Every argument of the message, as a list |
18
+ | `arg_1` | outlet | `haywire-core:type:ADD` | Grows a new port from whatever connects to it |
19
+ | `timetag` | outlet | `haywire-core:type:FLOAT` | Decimal number |
20
+ | `status` | outlet | `haywire-core:type:STRING` | Text data |
21
+
22
+ ## Settings
23
+
24
+ | name | bag | default | description |
25
+ |---|---|---|---|
26
+ | `queue_mode` | dispatch | `'Auto (by message kind)'` | Block runs the flow for every message; a slow flow then falls behind, never the Line. Drop keeps only the newest message. Auto uses Drop for values (faders, axes, OSC) and Block for events (notes, buttons). |
27
+ | `max_queue_size` | dispatch | `100` | How many messages Block holds while the flow runs behind. Drop always holds one. |
28
+ | `start_value` | dispatch | `'Auto (by message kind)'` | Fire once with the last message the Line saw when the graph starts. Auto: on for values, off for events. |
29
+
30
+ ## Notes
31
+
32
+ Runs its flow for each OSC message on its gate that matches its address.
33
+
34
+ `Gate` names an input gate in the project's Gate file (`.haywire/protokol.toml`). `Address`
35
+ is an OSC address pattern (`*`, `?`, `[0-9]`, `{a,b}`); empty takes every message. Each
36
+ wildcard gets a capture outlet, `match_1`, `match_2`..., which takes the type of what it is
37
+ connected to. `args` always carries every argument as a list. `Arg Shape` lists the
38
+ arguments (`f i vf3 s`, `_` to skip one), each token adding its own outlet. A message that
39
+ does not fit the shape is skipped and counted in `status`.
40
+
41
+ The outlets keep the latest values, so other flows can read them.
42
+
43
+ Outputs:
44
+ received: Control flow for each matching message
45
+ address: The address that matched
46
+ args: Every argument, as a list
47
+ arg_N: One argument (or a group, such as a vector) per Arg Shape token
48
+ match_N: What each wildcard matched
49
+ timetag: The OSC bundle's timetag, 0 for a message outside a bundle
50
+ status: The gate's state and how many messages were skipped
@@ -0,0 +1,38 @@
1
+ # OSC Send
2
+
3
+ `haybale-protokol-osc:node:OscSendNode` · kind: node
4
+
5
+ Sends one OSC message to an output gate each time it runs
6
+
7
+ ## Ports
8
+
9
+ | id | direction | type | description |
10
+ |---|---|---|---|
11
+ | `exec` | inlet | `haywire-core:type:EXEC` | Signal for controlling execution flow between nodes |
12
+ | `address` | inlet | `haywire-core:type:STRING` | Text data |
13
+ | `arg_1` | inlet | `haywire-core:type:ADD` | Grows a new port from whatever connects to it |
14
+ | `gate` | config | `haywire-core:type:STRING` | One or more output (or two-way) gates from the project's Gate file, .haywire/protokol.toml; pick them from the list, which offers this project's OSC gates that can send, or type a name and press Enter. Make gates in the Gates editor. Each send goes to every listed gate. The node opens each gate's Line when the graph starts and gives it back when it stops. A gate that is missing, of another protocol or cannot reach its peer is reported in status and the others still get the message; a message no gate could take counts as dropped, never queued. A change applies on the next start. |
15
+ | `arg_shape` | config | `haywire-core:type:STRING` | The Arg shape: one token per argument inlet, in inlet order. i: int; f: float; s: text; b: blob; T: bool; r: colour; vi2–vi4 and vf2–vf4: a vector, sent as its components; m3, m4: a matrix, sent as 9 or 16 floats. Use d or h instead of f or i for 64-bit numbers. There is no _: every inlet is sent, and an unconnected inlet sends its own value. Typing the shape rebuilds the inlets; connecting the + pin at the bottom adds a token for the connected type, deleting the edge that grew a pin removes the pin and its token, and dragging pins into another order reorders the tokens. An int too large for i is dropped and named in status; an invalid shape is put back. |
16
+ | `next` | outlet | `haywire-core:type:EXEC` | Signal for controlling execution flow between nodes |
17
+ | `shape` | outlet | `haywire-core:type:STRING` | Text data |
18
+ | `status` | outlet | `haywire-core:type:STRING` | Text data |
19
+
20
+ ## Settings
21
+
22
+ | name | bag | default | description |
23
+ |---|---|---|---|
24
+ | `only_changes` | sending | `False` | Off: every control tick sends the message. On: a tick sends only when the address, type tags or values differ from the last message sent; the first tick of a run always sends, and a dropped send is retried on the next tick. Skipped ticks are counted as 'unchanged' in status. |
25
+
26
+ ## Notes
27
+
28
+ Sends `Address` with one argument per argument inlet, in inlet order, to its gate.
29
+
30
+ `Gate` names an output gate in the project's Gate file. Connect anything to the empty
31
+ inlet at the bottom to add an argument; `Arg Shape` lists the arguments (`f i vf3 s`) and
32
+ follows the inlets. A vector goes out as its components (`vf3` → three `f`); use `d` or `h`
33
+ in the shape for 64-bit numbers. An unconnected inlet sends its own value.
34
+
35
+ Outputs:
36
+ next: Control flow after sending
37
+ shape: The OSC type tags sent last
38
+ status: What was sent and dropped, or why nothing can be sent
@@ -0,0 +1,9 @@
1
+ # Protokol OSC
2
+
3
+ `haybale-protokol-osc:state:OscState` · kind: state
4
+
5
+ Registers the OSC driver with Protokol
6
+
7
+ ## Notes
8
+
9
+ Hands ``OscDriver`` to ``ProtokolState`` on enable and takes it back on disable.