linuxcnc-grpc 0.5.0__py3-none-any.whl
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.
- linuxcnc_grpc/__init__.py +71 -0
- linuxcnc_grpc/_generated/__init__.py +12 -0
- linuxcnc_grpc/hal_mapper.py +305 -0
- linuxcnc_grpc/hal_service.py +512 -0
- linuxcnc_grpc/linuxcnc_mapper.py +416 -0
- linuxcnc_grpc/linuxcnc_service.py +783 -0
- linuxcnc_grpc/server.py +184 -0
- linuxcnc_grpc-0.5.0.dist-info/METADATA +234 -0
- linuxcnc_grpc-0.5.0.dist-info/RECORD +18 -0
- linuxcnc_grpc-0.5.0.dist-info/WHEEL +5 -0
- linuxcnc_grpc-0.5.0.dist-info/entry_points.txt +2 -0
- linuxcnc_grpc-0.5.0.dist-info/licenses/LICENSE +21 -0
- linuxcnc_grpc-0.5.0.dist-info/top_level.txt +2 -0
- linuxcnc_pb/__init__.py +6 -0
- linuxcnc_pb/hal_pb2.py +127 -0
- linuxcnc_pb/hal_pb2_grpc.py +462 -0
- linuxcnc_pb/linuxcnc_pb2.py +183 -0
- linuxcnc_pb/linuxcnc_pb2_grpc.py +418 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""LinuxCNC gRPC Server - Remote machine control via gRPC.
|
|
2
|
+
|
|
3
|
+
This package provides both the gRPC server implementation and re-exports all
|
|
4
|
+
protobuf types for convenient client-side usage.
|
|
5
|
+
|
|
6
|
+
Server Usage (requires LinuxCNC environment):
|
|
7
|
+
from linuxcnc_grpc import create_server, serve
|
|
8
|
+
server = create_server()
|
|
9
|
+
serve(server)
|
|
10
|
+
|
|
11
|
+
Client Usage (works anywhere):
|
|
12
|
+
from linuxcnc_grpc import (
|
|
13
|
+
LinuxCNCStatus, LinuxCNCCommand, StateCommand,
|
|
14
|
+
LinuxCNCServiceStub, HalServiceStub,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
Direct proto access:
|
|
18
|
+
from linuxcnc_pb import linuxcnc_pb2, hal_pb2
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
__version__ = "0.5.0"
|
|
22
|
+
|
|
23
|
+
# =============================================================================
|
|
24
|
+
# RE-EXPORT ALL PROTOBUF TYPES FROM linuxcnc_pb
|
|
25
|
+
# =============================================================================
|
|
26
|
+
|
|
27
|
+
# Import all types from the generated package
|
|
28
|
+
from linuxcnc_pb import *
|
|
29
|
+
|
|
30
|
+
# Also export the modules for direct access
|
|
31
|
+
from linuxcnc_pb import linuxcnc_pb2
|
|
32
|
+
from linuxcnc_pb import linuxcnc_pb2_grpc
|
|
33
|
+
from linuxcnc_pb import hal_pb2
|
|
34
|
+
from linuxcnc_pb import hal_pb2_grpc
|
|
35
|
+
|
|
36
|
+
# =============================================================================
|
|
37
|
+
# SERVER COMPONENTS (lazy-loaded - require LinuxCNC environment)
|
|
38
|
+
# =============================================================================
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def __getattr__(name):
|
|
42
|
+
"""Lazy load server components only when accessed."""
|
|
43
|
+
if name == "create_server":
|
|
44
|
+
from .server import create_server
|
|
45
|
+
return create_server
|
|
46
|
+
elif name == "serve":
|
|
47
|
+
from .server import serve
|
|
48
|
+
return serve
|
|
49
|
+
elif name == "LinuxCNCServiceServicer":
|
|
50
|
+
from .linuxcnc_service import LinuxCNCServiceServicer
|
|
51
|
+
return LinuxCNCServiceServicer
|
|
52
|
+
elif name == "HalServiceServicer":
|
|
53
|
+
from .hal_service import HalServiceServicer
|
|
54
|
+
return HalServiceServicer
|
|
55
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
__all__ = [
|
|
59
|
+
# Version
|
|
60
|
+
"__version__",
|
|
61
|
+
# Server components
|
|
62
|
+
"create_server",
|
|
63
|
+
"serve",
|
|
64
|
+
"LinuxCNCServiceServicer",
|
|
65
|
+
"HalServiceServicer",
|
|
66
|
+
# Proto modules
|
|
67
|
+
"linuxcnc_pb2",
|
|
68
|
+
"linuxcnc_pb2_grpc",
|
|
69
|
+
"hal_pb2",
|
|
70
|
+
"hal_pb2_grpc",
|
|
71
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Re-export from linuxcnc_pb for backwards compatibility
|
|
2
|
+
# The actual generated code lives in packages/python/linuxcnc_pb/
|
|
3
|
+
from linuxcnc_pb.linuxcnc_pb2 import *
|
|
4
|
+
from linuxcnc_pb.linuxcnc_pb2_grpc import *
|
|
5
|
+
from linuxcnc_pb.hal_pb2 import *
|
|
6
|
+
from linuxcnc_pb.hal_pb2_grpc import *
|
|
7
|
+
|
|
8
|
+
# Also expose the modules directly for `from linuxcnc_grpc._generated import linuxcnc_pb2` style imports
|
|
9
|
+
from linuxcnc_pb import linuxcnc_pb2
|
|
10
|
+
from linuxcnc_pb import linuxcnc_pb2_grpc
|
|
11
|
+
from linuxcnc_pb import hal_pb2
|
|
12
|
+
from linuxcnc_pb import hal_pb2_grpc
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""
|
|
2
|
+
HAL Python API data to protobuf mapper.
|
|
3
|
+
|
|
4
|
+
Maps hal.get_info_*() dictionaries to HalSystemStatus protobuf messages.
|
|
5
|
+
|
|
6
|
+
Note: The HAL Python API returns dicts with UPPERCASE keys:
|
|
7
|
+
- Pins: NAME, VALUE, DIRECTION, TYPE
|
|
8
|
+
- Signals: NAME, VALUE, DRIVER, TYPE
|
|
9
|
+
- Params: NAME, DIRECTION, VALUE (no TYPE field - infer from value)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import time
|
|
13
|
+
from typing import List, Dict, Any, Optional
|
|
14
|
+
|
|
15
|
+
from linuxcnc_pb import hal_pb2
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HalMapper:
|
|
19
|
+
"""Maps HAL introspection data to protobuf messages."""
|
|
20
|
+
|
|
21
|
+
# HAL type constants (from hal module, with fallback values)
|
|
22
|
+
HAL_BIT = 1
|
|
23
|
+
HAL_FLOAT = 2
|
|
24
|
+
HAL_S32 = 3
|
|
25
|
+
HAL_U32 = 4
|
|
26
|
+
HAL_S64 = 5
|
|
27
|
+
HAL_U64 = 6
|
|
28
|
+
HAL_PORT = 7
|
|
29
|
+
|
|
30
|
+
# HAL pin direction constants
|
|
31
|
+
HAL_IN = 16
|
|
32
|
+
HAL_OUT = 32
|
|
33
|
+
HAL_IO = 48
|
|
34
|
+
|
|
35
|
+
# HAL param direction constants
|
|
36
|
+
HAL_RO = 64
|
|
37
|
+
HAL_RW = 192
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
pins: List[Dict[str, Any]],
|
|
42
|
+
signals: List[Dict[str, Any]],
|
|
43
|
+
params: List[Dict[str, Any]]
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Initialize mapper with HAL introspection data.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
pins: Result from hal.get_info_pins()
|
|
50
|
+
signals: Result from hal.get_info_signals()
|
|
51
|
+
params: Result from hal.get_info_params()
|
|
52
|
+
"""
|
|
53
|
+
self._pins = pins
|
|
54
|
+
self._signals = signals
|
|
55
|
+
self._params = params
|
|
56
|
+
self._signal_pins = self._build_signal_pin_map()
|
|
57
|
+
self._components = self._derive_components()
|
|
58
|
+
|
|
59
|
+
@staticmethod
|
|
60
|
+
def _get(d: Dict[str, Any], key: str, default: Any = None) -> Any:
|
|
61
|
+
"""Get value from dict, trying UPPERCASE first (HAL API convention), then lowercase."""
|
|
62
|
+
return d.get(key.upper(), d.get(key.lower(), d.get(key, default)))
|
|
63
|
+
|
|
64
|
+
def _build_signal_pin_map(self) -> Dict[str, Dict[str, Any]]:
|
|
65
|
+
"""Build mapping from signal names to connected pins."""
|
|
66
|
+
signal_map: Dict[str, Dict[str, Any]] = {}
|
|
67
|
+
|
|
68
|
+
# Initialize all signals with their driver from the signal info
|
|
69
|
+
for sig in self._signals:
|
|
70
|
+
sig_name = self._get(sig, 'name', '')
|
|
71
|
+
driver = self._get(sig, 'driver', '')
|
|
72
|
+
signal_map[sig_name] = {
|
|
73
|
+
'driver': driver,
|
|
74
|
+
'readers': []
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# Note: HAL doesn't provide signal connection info in pin dicts,
|
|
78
|
+
# so we can't determine readers from the pin data directly.
|
|
79
|
+
# The driver comes from the signal info itself.
|
|
80
|
+
|
|
81
|
+
return signal_map
|
|
82
|
+
|
|
83
|
+
def _derive_components(self) -> Dict[str, Dict[str, Any]]:
|
|
84
|
+
"""Derive component information from pins and params."""
|
|
85
|
+
components: Dict[str, Dict[str, Any]] = {}
|
|
86
|
+
|
|
87
|
+
# Extract components from pins
|
|
88
|
+
for pin in self._pins:
|
|
89
|
+
name = self._get(pin, 'name', '')
|
|
90
|
+
# Component name is everything before the last dot
|
|
91
|
+
if '.' in name:
|
|
92
|
+
comp_name = name.rsplit('.', 1)[0]
|
|
93
|
+
else:
|
|
94
|
+
comp_name = name
|
|
95
|
+
owner_id = self._get(pin, 'owner', 0)
|
|
96
|
+
|
|
97
|
+
if comp_name not in components:
|
|
98
|
+
components[comp_name] = {
|
|
99
|
+
'name': comp_name,
|
|
100
|
+
'id': owner_id,
|
|
101
|
+
'pins': [],
|
|
102
|
+
'params': [],
|
|
103
|
+
'ready': True # Assume ready if has pins
|
|
104
|
+
}
|
|
105
|
+
components[comp_name]['pins'].append(name)
|
|
106
|
+
|
|
107
|
+
# Add params to their components
|
|
108
|
+
for param in self._params:
|
|
109
|
+
name = self._get(param, 'name', '')
|
|
110
|
+
if '.' in name:
|
|
111
|
+
comp_name = name.rsplit('.', 1)[0]
|
|
112
|
+
else:
|
|
113
|
+
comp_name = name
|
|
114
|
+
|
|
115
|
+
if comp_name in components:
|
|
116
|
+
components[comp_name]['params'].append(name)
|
|
117
|
+
else:
|
|
118
|
+
# Component only has params, no pins
|
|
119
|
+
owner_id = self._get(param, 'owner', 0)
|
|
120
|
+
components[comp_name] = {
|
|
121
|
+
'name': comp_name,
|
|
122
|
+
'id': owner_id,
|
|
123
|
+
'pins': [],
|
|
124
|
+
'params': [name],
|
|
125
|
+
'ready': True
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return components
|
|
129
|
+
|
|
130
|
+
def map_to_proto(self) -> hal_pb2.HalSystemStatus:
|
|
131
|
+
"""Map all HAL data to a complete HalSystemStatus message."""
|
|
132
|
+
return hal_pb2.HalSystemStatus(
|
|
133
|
+
timestamp=int(time.time() * 1e9),
|
|
134
|
+
pins=[self.map_pin(p) for p in self._pins],
|
|
135
|
+
signals=[self.map_signal(s) for s in self._signals],
|
|
136
|
+
params=[self.map_param(p) for p in self._params],
|
|
137
|
+
components=[self.map_component(c) for c in self._components.values()],
|
|
138
|
+
message_level=hal_pb2.MSG_INFO,
|
|
139
|
+
is_sim=False,
|
|
140
|
+
is_rt=True,
|
|
141
|
+
is_userspace=False,
|
|
142
|
+
kernel_version=""
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
def map_pin(self, pin_info: Dict[str, Any]) -> hal_pb2.HalPinInfo:
|
|
146
|
+
"""Map a single pin info dict to HalPinInfo proto."""
|
|
147
|
+
name = self._get(pin_info, 'name', '')
|
|
148
|
+
short_name = name.rsplit('.', 1)[-1] if '.' in name else name
|
|
149
|
+
component = name.rsplit('.', 1)[0] if '.' in name else ''
|
|
150
|
+
hal_type = self._get(pin_info, 'type', self.HAL_FLOAT)
|
|
151
|
+
direction = self._get(pin_info, 'direction', self.HAL_IN)
|
|
152
|
+
value = self._get(pin_info, 'value', 0)
|
|
153
|
+
signal = self._get(pin_info, 'signal', '')
|
|
154
|
+
|
|
155
|
+
return hal_pb2.HalPinInfo(
|
|
156
|
+
name=name,
|
|
157
|
+
short_name=short_name,
|
|
158
|
+
component=component,
|
|
159
|
+
type=self._map_hal_type(hal_type),
|
|
160
|
+
direction=self._map_pin_direction(direction),
|
|
161
|
+
value=self.map_value(value, hal_type),
|
|
162
|
+
signal=signal if signal else '',
|
|
163
|
+
has_writer=(direction == self.HAL_OUT)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
def map_signal(self, signal_info: Dict[str, Any]) -> hal_pb2.HalSignalInfo:
|
|
167
|
+
"""Map a single signal info dict to HalSignalInfo proto."""
|
|
168
|
+
name = self._get(signal_info, 'name', '')
|
|
169
|
+
hal_type = self._get(signal_info, 'type', self.HAL_FLOAT)
|
|
170
|
+
value = self._get(signal_info, 'value', 0)
|
|
171
|
+
driver = self._get(signal_info, 'driver', '')
|
|
172
|
+
|
|
173
|
+
# Get connected pins from our map
|
|
174
|
+
# Note: readers list is always empty because the HAL API doesn't
|
|
175
|
+
# expose signal-to-reader-pin connections. reader_count will be 0.
|
|
176
|
+
conn = self._signal_pins.get(name, {'driver': driver, 'readers': []})
|
|
177
|
+
|
|
178
|
+
return hal_pb2.HalSignalInfo(
|
|
179
|
+
name=name,
|
|
180
|
+
type=self._map_hal_type(hal_type),
|
|
181
|
+
value=self.map_value(value, hal_type),
|
|
182
|
+
driver=conn['driver'],
|
|
183
|
+
readers=conn['readers'],
|
|
184
|
+
reader_count=len(conn['readers'])
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
def map_param(self, param_info: Dict[str, Any]) -> hal_pb2.HalParamInfo:
|
|
188
|
+
"""Map a single param info dict to HalParamInfo proto."""
|
|
189
|
+
name = self._get(param_info, 'name', '')
|
|
190
|
+
short_name = name.rsplit('.', 1)[-1] if '.' in name else name
|
|
191
|
+
component = name.rsplit('.', 1)[0] if '.' in name else ''
|
|
192
|
+
direction = self._get(param_info, 'direction', self.HAL_RW)
|
|
193
|
+
value = self._get(param_info, 'value', 0)
|
|
194
|
+
|
|
195
|
+
# HAL params don't have a TYPE field - infer from value
|
|
196
|
+
hal_type = self._infer_type_from_value(value)
|
|
197
|
+
|
|
198
|
+
return hal_pb2.HalParamInfo(
|
|
199
|
+
name=name,
|
|
200
|
+
short_name=short_name,
|
|
201
|
+
component=component,
|
|
202
|
+
type=self._map_hal_type(hal_type),
|
|
203
|
+
direction=self._map_param_direction(direction),
|
|
204
|
+
value=self.map_value(value, hal_type)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def _infer_type_from_value(self, value: Any) -> int:
|
|
208
|
+
"""Infer HAL type from a Python value."""
|
|
209
|
+
if isinstance(value, bool):
|
|
210
|
+
return self.HAL_BIT
|
|
211
|
+
elif isinstance(value, float):
|
|
212
|
+
return self.HAL_FLOAT
|
|
213
|
+
elif isinstance(value, int):
|
|
214
|
+
# Could be s32, u32, s64, u64 - default to s32 for simplicity
|
|
215
|
+
if value < 0:
|
|
216
|
+
return self.HAL_S32
|
|
217
|
+
elif value > 0xFFFFFFFF:
|
|
218
|
+
return self.HAL_U64
|
|
219
|
+
else:
|
|
220
|
+
return self.HAL_S32
|
|
221
|
+
else:
|
|
222
|
+
return self.HAL_FLOAT
|
|
223
|
+
|
|
224
|
+
def map_component(self, comp_info: Dict[str, Any]) -> hal_pb2.HalComponentInfo:
|
|
225
|
+
"""Map component info dict to HalComponentInfo proto."""
|
|
226
|
+
return hal_pb2.HalComponentInfo(
|
|
227
|
+
name=comp_info.get('name', ''),
|
|
228
|
+
id=comp_info.get('id', 0),
|
|
229
|
+
ready=comp_info.get('ready', True),
|
|
230
|
+
type=comp_info.get('type', 1), # 1 = user component
|
|
231
|
+
pid=comp_info.get('pid', 0),
|
|
232
|
+
pins=comp_info.get('pins', []),
|
|
233
|
+
params=comp_info.get('params', [])
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
def map_value(self, value: Any, hal_type: int) -> hal_pb2.HalValue:
|
|
237
|
+
"""Map a raw HAL value to HalValue proto based on type."""
|
|
238
|
+
if hal_type == self.HAL_BIT:
|
|
239
|
+
return hal_pb2.HalValue(bit_value=bool(value))
|
|
240
|
+
elif hal_type == self.HAL_FLOAT:
|
|
241
|
+
return hal_pb2.HalValue(float_value=float(value) if value is not None else 0.0)
|
|
242
|
+
elif hal_type == self.HAL_S32:
|
|
243
|
+
return hal_pb2.HalValue(s32_value=int(value) if value is not None else 0)
|
|
244
|
+
elif hal_type == self.HAL_U32:
|
|
245
|
+
val = int(value) if value is not None else 0
|
|
246
|
+
return hal_pb2.HalValue(u32_value=val & 0xFFFFFFFF)
|
|
247
|
+
elif hal_type == self.HAL_S64:
|
|
248
|
+
return hal_pb2.HalValue(s64_value=int(value) if value is not None else 0)
|
|
249
|
+
elif hal_type == self.HAL_U64:
|
|
250
|
+
val = int(value) if value is not None else 0
|
|
251
|
+
return hal_pb2.HalValue(u64_value=val & 0xFFFFFFFFFFFFFFFF)
|
|
252
|
+
else:
|
|
253
|
+
# Default to float for unknown types
|
|
254
|
+
return hal_pb2.HalValue(float_value=float(value) if value is not None else 0.0)
|
|
255
|
+
|
|
256
|
+
def _map_hal_type(self, hal_type: int) -> int:
|
|
257
|
+
"""Map HAL type constant to proto HalType enum."""
|
|
258
|
+
mapping = {
|
|
259
|
+
self.HAL_BIT: hal_pb2.HAL_BIT,
|
|
260
|
+
self.HAL_FLOAT: hal_pb2.HAL_FLOAT,
|
|
261
|
+
self.HAL_S32: hal_pb2.HAL_S32,
|
|
262
|
+
self.HAL_U32: hal_pb2.HAL_U32,
|
|
263
|
+
self.HAL_S64: hal_pb2.HAL_S64,
|
|
264
|
+
self.HAL_U64: hal_pb2.HAL_U64,
|
|
265
|
+
self.HAL_PORT: hal_pb2.HAL_PORT,
|
|
266
|
+
}
|
|
267
|
+
return mapping.get(hal_type, hal_pb2.HAL_TYPE_UNSPECIFIED)
|
|
268
|
+
|
|
269
|
+
def _map_pin_direction(self, direction: int) -> int:
|
|
270
|
+
"""Map HAL pin direction to proto PinDirection enum."""
|
|
271
|
+
mapping = {
|
|
272
|
+
self.HAL_IN: hal_pb2.HAL_IN,
|
|
273
|
+
self.HAL_OUT: hal_pb2.HAL_OUT,
|
|
274
|
+
self.HAL_IO: hal_pb2.HAL_IO,
|
|
275
|
+
}
|
|
276
|
+
return mapping.get(direction, hal_pb2.PIN_DIR_UNSPECIFIED)
|
|
277
|
+
|
|
278
|
+
def _map_param_direction(self, direction: int) -> int:
|
|
279
|
+
"""Map HAL param direction to proto ParamDirection enum."""
|
|
280
|
+
mapping = {
|
|
281
|
+
self.HAL_RO: hal_pb2.HAL_RO,
|
|
282
|
+
self.HAL_RW: hal_pb2.HAL_RW,
|
|
283
|
+
}
|
|
284
|
+
return mapping.get(direction, hal_pb2.PARAM_DIR_UNSPECIFIED)
|
|
285
|
+
|
|
286
|
+
def get_type_for_name(self, name: str) -> int:
|
|
287
|
+
"""Look up the HAL type for a pin, signal, or param by name."""
|
|
288
|
+
# Check pins
|
|
289
|
+
for pin in self._pins:
|
|
290
|
+
if self._get(pin, 'name') == name:
|
|
291
|
+
return self._get(pin, 'type', self.HAL_FLOAT)
|
|
292
|
+
|
|
293
|
+
# Check signals
|
|
294
|
+
for sig in self._signals:
|
|
295
|
+
if self._get(sig, 'name') == name:
|
|
296
|
+
return self._get(sig, 'type', self.HAL_FLOAT)
|
|
297
|
+
|
|
298
|
+
# Check params (no type field, infer from value)
|
|
299
|
+
for param in self._params:
|
|
300
|
+
if self._get(param, 'name') == name:
|
|
301
|
+
value = self._get(param, 'value', 0)
|
|
302
|
+
return self._infer_type_from_value(value)
|
|
303
|
+
|
|
304
|
+
# Default to float
|
|
305
|
+
return self.HAL_FLOAT
|