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.
@@ -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