pynode-flow 0.2.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.
Files changed (227) hide show
  1. pynode/__init__.py +37 -0
  2. pynode/__main__.py +9 -0
  3. pynode/_perf.py +65 -0
  4. pynode/_version.py +24 -0
  5. pynode/api/__init__.py +15 -0
  6. pynode/api/helpers.py +79 -0
  7. pynode/api/nodes.py +423 -0
  8. pynode/api/services.py +124 -0
  9. pynode/api/sse.py +58 -0
  10. pynode/api/uploads.py +59 -0
  11. pynode/api/workflows.py +466 -0
  12. pynode/config.py +93 -0
  13. pynode/install_nodes.py +143 -0
  14. pynode/main.py +118 -0
  15. pynode/node_registry.py +156 -0
  16. pynode/nodes/AutoSyncNode/auto_sync_node.py +151 -0
  17. pynode/nodes/BBoxMetricsNode/__init__.py +3 -0
  18. pynode/nodes/BBoxMetricsNode/bbox_metrics_node.py +250 -0
  19. pynode/nodes/BBoxMetricsNode/requirements.txt +1 -0
  20. pynode/nodes/BatchNode/__init__.py +3 -0
  21. pynode/nodes/BatchNode/batch_node.py +86 -0
  22. pynode/nodes/BatchNode/requirements.txt +1 -0
  23. pynode/nodes/CameraNode/__init__.py +4 -0
  24. pynode/nodes/CameraNode/camera_node.py +204 -0
  25. pynode/nodes/CameraNode/requirements.txt +2 -0
  26. pynode/nodes/ChangeNode/__init__.py +1 -0
  27. pynode/nodes/ChangeNode/change_node.py +255 -0
  28. pynode/nodes/ChangeNode/requirements.txt +1 -0
  29. pynode/nodes/ConfidenceFilterNode/__init__.py +4 -0
  30. pynode/nodes/ConfidenceFilterNode/confidence_filter_node.py +208 -0
  31. pynode/nodes/ConfidenceFilterNode/requirements.txt +1 -0
  32. pynode/nodes/CounterNode/__init__.py +3 -0
  33. pynode/nodes/CounterNode/counter_node.py +191 -0
  34. pynode/nodes/CropNode/__init__.py +4 -0
  35. pynode/nodes/CropNode/crop_node.py +317 -0
  36. pynode/nodes/CropNode/requirements.txt +2 -0
  37. pynode/nodes/DebugNode/__init__.py +4 -0
  38. pynode/nodes/DebugNode/debug_node.py +169 -0
  39. pynode/nodes/DebugNode/requirements.txt +1 -0
  40. pynode/nodes/DelayNode/delay_node.py +218 -0
  41. pynode/nodes/DelayNode/requirements.txt +1 -0
  42. pynode/nodes/DrawPredictionsNode/__init__.py +3 -0
  43. pynode/nodes/DrawPredictionsNode/draw_predictions_node.py +365 -0
  44. pynode/nodes/DrawPredictionsNode/requirements.txt +2 -0
  45. pynode/nodes/ErrorNode/error_node.py +94 -0
  46. pynode/nodes/ErrorNode/requirements.txt +1 -0
  47. pynode/nodes/FilterNode/__init__.py +3 -0
  48. pynode/nodes/FilterNode/filter_node.py +102 -0
  49. pynode/nodes/FilterNode/requirements.txt +1 -0
  50. pynode/nodes/FrameSourceNode/__init__.py +4 -0
  51. pynode/nodes/FrameSourceNode/frame_source_node.py +319 -0
  52. pynode/nodes/FrameSourceNode/requirements.txt +1 -0
  53. pynode/nodes/FunctionNode/__init__.py +3 -0
  54. pynode/nodes/FunctionNode/function_node.py +168 -0
  55. pynode/nodes/FunctionNode/requirements.txt +1 -0
  56. pynode/nodes/GateNode/gate_node.py +60 -0
  57. pynode/nodes/GateNode/requirements.txt +1 -0
  58. pynode/nodes/GetiUploadNode/__init__.py +3 -0
  59. pynode/nodes/GetiUploadNode/geti_upload_node.py +278 -0
  60. pynode/nodes/GetiUploadNode/requirements.txt +1 -0
  61. pynode/nodes/ImageFormatNode/__init__.py +4 -0
  62. pynode/nodes/ImageFormatNode/image_format_node.py +310 -0
  63. pynode/nodes/ImageFormatNode/requirements.txt +2 -0
  64. pynode/nodes/ImageUploadNode/image_upload_node.py +96 -0
  65. pynode/nodes/ImageViewerNode/__init__.py +4 -0
  66. pynode/nodes/ImageViewerNode/image_viewer_node.py +162 -0
  67. pynode/nodes/ImageViewerNode/requirements.txt +2 -0
  68. pynode/nodes/ImageWriterNode/__init__.py +3 -0
  69. pynode/nodes/ImageWriterNode/imagewriter_node.py +352 -0
  70. pynode/nodes/ImageWriterNode/requirements.txt +1 -0
  71. pynode/nodes/InferenceNode/InferenceEngine/__init__.py +15 -0
  72. pynode/nodes/InferenceNode/InferenceEngine/device_detection.py +149 -0
  73. pynode/nodes/InferenceNode/InferenceEngine/engines/base_engine.py +155 -0
  74. pynode/nodes/InferenceNode/InferenceEngine/engines/example_engine_template.py +272 -0
  75. pynode/nodes/InferenceNode/InferenceEngine/engines/geti_engine.py +309 -0
  76. pynode/nodes/InferenceNode/InferenceEngine/engines/onnx_engine.py +346 -0
  77. pynode/nodes/InferenceNode/InferenceEngine/engines/pass_engine.py +86 -0
  78. pynode/nodes/InferenceNode/InferenceEngine/engines/ultralytics_engine.py +863 -0
  79. pynode/nodes/InferenceNode/InferenceEngine/inference_engine_factory.py +533 -0
  80. pynode/nodes/InferenceNode/InferenceEngine/readme.md +107 -0
  81. pynode/nodes/InferenceNode/InferenceEngine/result_converters.py +121 -0
  82. pynode/nodes/InferenceNode/inference_node.py +451 -0
  83. pynode/nodes/InferenceNode/requirements.txt +13 -0
  84. pynode/nodes/InjectNode/inject_node.py +245 -0
  85. pynode/nodes/InjectNode/requirements.txt +1 -0
  86. pynode/nodes/JoinNode/__init__.py +3 -0
  87. pynode/nodes/JoinNode/join_node.py +156 -0
  88. pynode/nodes/JoinNode/requirements.txt +1 -0
  89. pynode/nodes/LabelFilterNode/__init__.py +3 -0
  90. pynode/nodes/LabelFilterNode/label_filter_node.py +188 -0
  91. pynode/nodes/LabelFilterNode/requirements.txt +1 -0
  92. pynode/nodes/MQTTNode/__init__.py +8 -0
  93. pynode/nodes/MQTTNode/mqtt_in_node.py +148 -0
  94. pynode/nodes/MQTTNode/mqtt_out_node.py +157 -0
  95. pynode/nodes/MQTTNode/mqtt_service.py +367 -0
  96. pynode/nodes/MQTTNode/requirements.txt +1 -0
  97. pynode/nodes/MessageWriterNode/__init__.py +7 -0
  98. pynode/nodes/MessageWriterNode/messagereader_node.py +568 -0
  99. pynode/nodes/MessageWriterNode/messagewriter_node.py +505 -0
  100. pynode/nodes/OmronCameraNode/__init__.py +3 -0
  101. pynode/nodes/OmronCameraNode/omron_camera_node.py +652 -0
  102. pynode/nodes/OmronCameraNode/requirements.txt +4 -0
  103. pynode/nodes/OpenCV/__init__.py +1 -0
  104. pynode/nodes/OpenCV/arithmetic_node.py +376 -0
  105. pynode/nodes/OpenCV/bitwise_node.py +127 -0
  106. pynode/nodes/OpenCV/blend_node.py +127 -0
  107. pynode/nodes/OpenCV/blob_detector_node.py +233 -0
  108. pynode/nodes/OpenCV/blur_node.py +141 -0
  109. pynode/nodes/OpenCV/color_space_node.py +139 -0
  110. pynode/nodes/OpenCV/colormap_node.py +262 -0
  111. pynode/nodes/OpenCV/contours_node.py +258 -0
  112. pynode/nodes/OpenCV/denormalize_coords_node.py +365 -0
  113. pynode/nodes/OpenCV/draw_node.py +312 -0
  114. pynode/nodes/OpenCV/edge_detector_node.py +177 -0
  115. pynode/nodes/OpenCV/equirectangular_node.py +447 -0
  116. pynode/nodes/OpenCV/fft_node.py +102 -0
  117. pynode/nodes/OpenCV/focus_estimation_node.py +189 -0
  118. pynode/nodes/OpenCV/frequency_filter_node.py +241 -0
  119. pynode/nodes/OpenCV/histogram_node.py +160 -0
  120. pynode/nodes/OpenCV/hough_circles_node.py +224 -0
  121. pynode/nodes/OpenCV/hough_lines_node.py +243 -0
  122. pynode/nodes/OpenCV/in_range_node.py +186 -0
  123. pynode/nodes/OpenCV/morphology_node.py +136 -0
  124. pynode/nodes/OpenCV/normalize_coords_node.py +344 -0
  125. pynode/nodes/OpenCV/paste_node.py +244 -0
  126. pynode/nodes/OpenCV/perspective_node.py +174 -0
  127. pynode/nodes/OpenCV/realsense_depth_node.py +253 -0
  128. pynode/nodes/OpenCV/requirements.txt +2 -0
  129. pynode/nodes/OpenCV/resize_node.py +175 -0
  130. pynode/nodes/OpenCV/rotate_node.py +159 -0
  131. pynode/nodes/OpenCV/template_match_node.py +227 -0
  132. pynode/nodes/OpenCV/threshold_node.py +167 -0
  133. pynode/nodes/PointInShapeNode/__init__.py +3 -0
  134. pynode/nodes/PointInShapeNode/point_in_shape_node.py +313 -0
  135. pynode/nodes/PointInShapeNode/requirements.txt +2 -0
  136. pynode/nodes/PolygonMetricsNode/__init__.py +3 -0
  137. pynode/nodes/PolygonMetricsNode/polygon_metrics_node.py +331 -0
  138. pynode/nodes/PolygonMetricsNode/requirements.txt +2 -0
  139. pynode/nodes/QueueLengthProbeNode/__init__.py +3 -0
  140. pynode/nodes/QueueLengthProbeNode/queue_length_probe_node.py +116 -0
  141. pynode/nodes/Qwen3VLMNode/__init__.py +3 -0
  142. pynode/nodes/Qwen3VLMNode/qwen3_vlm_node.py +519 -0
  143. pynode/nodes/Qwen3VLMNode/requirements.txt +5 -0
  144. pynode/nodes/RESTEndpointNode/__init__.py +3 -0
  145. pynode/nodes/RESTEndpointNode/rest_endpoint_node.py +491 -0
  146. pynode/nodes/RangeNode/__init__.py +3 -0
  147. pynode/nodes/RangeNode/range_node.py +115 -0
  148. pynode/nodes/RangeNode/requirements.txt +1 -0
  149. pynode/nodes/RateProbeNode/__init__.py +3 -0
  150. pynode/nodes/RateProbeNode/rate_probe_node.py +183 -0
  151. pynode/nodes/RateProbeNode/requirements.txt +1 -0
  152. pynode/nodes/RoboflowUploadNode/__init__.py +3 -0
  153. pynode/nodes/RoboflowUploadNode/requirements.txt +1 -0
  154. pynode/nodes/RoboflowUploadNode/roboflow_upload_node.py +229 -0
  155. pynode/nodes/SliceNode/README.md +149 -0
  156. pynode/nodes/SliceNode/__init__.py +5 -0
  157. pynode/nodes/SliceNode/merge_predictions_node.py +444 -0
  158. pynode/nodes/SliceNode/requirements.txt +3 -0
  159. pynode/nodes/SliceNode/slice_collector_node.py +370 -0
  160. pynode/nodes/SliceNode/slice_image_node.py +380 -0
  161. pynode/nodes/SplitNode/__init__.py +3 -0
  162. pynode/nodes/SplitNode/requirements.txt +1 -0
  163. pynode/nodes/SplitNode/split_node.py +155 -0
  164. pynode/nodes/SwitchNode/requirements.txt +1 -0
  165. pynode/nodes/SwitchNode/switch_node.py +259 -0
  166. pynode/nodes/SyncNode/__init__.py +3 -0
  167. pynode/nodes/SyncNode/sync_node.py +287 -0
  168. pynode/nodes/TemplateNode/requirements.txt +1 -0
  169. pynode/nodes/TemplateNode/template_node.py +86 -0
  170. pynode/nodes/TrackerNode/__init__.py +2 -0
  171. pynode/nodes/TrackerNode/supervision_tracker_node.py +248 -0
  172. pynode/nodes/TrackerNode/tracker_node.py +745 -0
  173. pynode/nodes/UltralyticsNode/__init__.py +4 -0
  174. pynode/nodes/UltralyticsNode/requirements.txt +2 -0
  175. pynode/nodes/UltralyticsNode/ultralytics_node.py +364 -0
  176. pynode/nodes/UnknownNode/__init__.py +93 -0
  177. pynode/nodes/VLLMNode/__init__.py +3 -0
  178. pynode/nodes/VLLMNode/vllm_node.py +345 -0
  179. pynode/nodes/VideoReaderNode/__init__.py +3 -0
  180. pynode/nodes/VideoReaderNode/video_reader_node.py +425 -0
  181. pynode/nodes/VideoWriterNode/__init__.py +3 -0
  182. pynode/nodes/VideoWriterNode/video_writer_node.py +441 -0
  183. pynode/nodes/WebhookNode/__init__.py +3 -0
  184. pynode/nodes/WebhookNode/webhook_node.py +591 -0
  185. pynode/nodes/__init__.py +109 -0
  186. pynode/nodes/base_node.py +562 -0
  187. pynode/nodes/image_utils.py +291 -0
  188. pynode/nodes/info.py +109 -0
  189. pynode/nodes/mDNSNode/__init__.py +13 -0
  190. pynode/nodes/mDNSNode/mdns_broadcast_node.py +214 -0
  191. pynode/nodes/mDNSNode/mdns_discovery_node.py +362 -0
  192. pynode/nodes/mDNSNode/mdns_manager.py +399 -0
  193. pynode/nodes/mDNSNode/requirements.txt +1 -0
  194. pynode/nodes/messages.py +81 -0
  195. pynode/server.py +187 -0
  196. pynode/static/images/favicon4.png +0 -0
  197. pynode/static/index.html +200 -0
  198. pynode/static/js/README.md +139 -0
  199. pynode/static/js/auth.js +89 -0
  200. pynode/static/js/clipboard.js +152 -0
  201. pynode/static/js/config.js +14 -0
  202. pynode/static/js/connections.js +844 -0
  203. pynode/static/js/debug.js +524 -0
  204. pynode/static/js/events.js +775 -0
  205. pynode/static/js/history.js +199 -0
  206. pynode/static/js/main.js +55 -0
  207. pynode/static/js/minimap.js +242 -0
  208. pynode/static/js/nodes.js +995 -0
  209. pynode/static/js/palette.js +255 -0
  210. pynode/static/js/properties.js +1270 -0
  211. pynode/static/js/selection.js +241 -0
  212. pynode/static/js/state.js +211 -0
  213. pynode/static/js/ui-utils.js +28 -0
  214. pynode/static/js/viewport.js +204 -0
  215. pynode/static/js/workflow.js +469 -0
  216. pynode/static/js/workflows.js +344 -0
  217. pynode/static/style.css +2751 -0
  218. pynode/workflow_engine.py +483 -0
  219. pynode/workflow_manager.py +440 -0
  220. pynode_flow-0.2.0.dist-info/METADATA +520 -0
  221. pynode_flow-0.2.0.dist-info/RECORD +227 -0
  222. pynode_flow-0.2.0.dist-info/WHEEL +5 -0
  223. pynode_flow-0.2.0.dist-info/entry_points.txt +3 -0
  224. pynode_flow-0.2.0.dist-info/licenses/LICENSE +21 -0
  225. pynode_flow-0.2.0.dist-info/scm_file_list.json +270 -0
  226. pynode_flow-0.2.0.dist-info/scm_version.json +8 -0
  227. pynode_flow-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,505 @@
1
+ """
2
+ Message Writer Node - writes messages or message components to disk.
3
+ Supports dynamic filenames and flexible data extraction from messages.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import pickle
9
+ import time
10
+ import base64
11
+ from datetime import datetime
12
+ from typing import Any, Dict
13
+ from pynode.nodes.base_node import BaseNode, Info, MessageKeys
14
+
15
+ _info = Info()
16
+ _info.add_text("Writes messages or message components to disk. Supports JSON, binary, or text formats with dynamic filenames.")
17
+ _info.add_header("Inputs")
18
+ _info.add_bullets(
19
+ ("Input 0:", "Message with data to write. Can write whole message or specific components.")
20
+ )
21
+ _info.add_header("Outputs")
22
+ _info.add_bullets(
23
+ ("Output 0:", "Status message with path, filename, bytes written, and counter.")
24
+ )
25
+ _info.add_header("Message Properties")
26
+ _info.add_bullets(
27
+ ("msg.fname:", "Override filename from message."),
28
+ ("msg.extension:", "Override file extension from message.")
29
+ )
30
+ _info.add_header("Data Source Options")
31
+ _info.add_bullets(
32
+ ("whole_message:", "Write entire message object"),
33
+ ("msg.payload:", "Write only the payload"),
34
+ ("msg.topic:", "Write only the topic"),
35
+ ("custom:", "Write custom message path (e.g., msg.payload.detections)")
36
+ )
37
+ _info.add_header("Naming Modes")
38
+ _info.add_bullets(
39
+ ("Counter:", "data_0001.json, data_0002.json, ..."),
40
+ ("Timestamp:", "data_1234567890.json (Unix timestamp)"),
41
+ ("DateTime:", "data_2024-12-03_153045.json"),
42
+ ("Message:", "Use msg.fname from incoming message")
43
+ )
44
+
45
+
46
+ class MessageWriterNode(BaseNode):
47
+ """
48
+ Message Writer node - writes messages or message components to disk.
49
+ Supports flexible data extraction and multiple output formats.
50
+ """
51
+ display_name = 'Message Writer'
52
+ icon = '📝'
53
+ category = 'output'
54
+ color = '#9B7CB6'
55
+ border_color = '#6B4C7B'
56
+ text_color = '#FFFFFF'
57
+ input_count = 1
58
+ output_count = 1
59
+ info = str(_info)
60
+
61
+ DEFAULT_CONFIG = {
62
+ 'directory': './output',
63
+ 'filename': 'message',
64
+ 'extension': 'json',
65
+ 'naming_mode': 'counter',
66
+ 'counter_digits': '4',
67
+ 'overwrite': 'false',
68
+ 'create_subdirs': 'true',
69
+ 'data_source': 'whole_message',
70
+ 'custom_path': f'{MessageKeys.MSG}.{MessageKeys.PAYLOAD}',
71
+ 'output_format': 'json'
72
+ }
73
+
74
+ properties = [
75
+ {
76
+ 'name': 'directory',
77
+ 'label': 'Output Directory',
78
+ 'type': 'text',
79
+ 'default': DEFAULT_CONFIG['directory'],
80
+ 'help': 'Directory where files will be saved (created if doesn\'t exist)'
81
+ },
82
+ {
83
+ 'name': 'filename',
84
+ 'label': 'Filename/Prefix',
85
+ 'type': 'text',
86
+ 'default': DEFAULT_CONFIG['filename'],
87
+ 'help': 'Filename or prefix. Can be overridden by msg.fname. Supports {timestamp}, {counter}'
88
+ },
89
+ {
90
+ 'name': 'extension',
91
+ 'label': 'File Extension',
92
+ 'type': 'select',
93
+ 'options': [
94
+ {'value': 'json', 'label': '.json'},
95
+ {'value': 'txt', 'label': '.txt'},
96
+ {'value': 'bin', 'label': '.bin (binary)'},
97
+ {'value': 'pkl', 'label': '.pkl (pickle)'},
98
+ {'value': 'npy', 'label': '.npy (numpy)'},
99
+ {'value': 'csv', 'label': '.csv'},
100
+ {'value': 'log', 'label': '.log'}
101
+ ],
102
+ 'default': DEFAULT_CONFIG['extension'],
103
+ 'help': 'File extension. Can be overridden by msg.extension'
104
+ },
105
+ {
106
+ 'name': 'naming_mode',
107
+ 'label': 'Naming Mode',
108
+ 'type': 'select',
109
+ 'options': [
110
+ {'value': 'counter', 'label': 'Counter (message_0001.json)'},
111
+ {'value': 'timestamp', 'label': 'Timestamp (message_1234567890.json)'},
112
+ {'value': 'datetime', 'label': 'DateTime (message_2024-12-03_153045.json)'},
113
+ {'value': 'message', 'label': 'From Message (msg.fname)'}
114
+ ],
115
+ 'default': DEFAULT_CONFIG['naming_mode'],
116
+ 'help': 'How to generate filenames'
117
+ },
118
+ {
119
+ 'name': 'counter_digits',
120
+ 'label': 'Counter Digits',
121
+ 'type': 'text',
122
+ 'default': DEFAULT_CONFIG['counter_digits'],
123
+ 'help': 'Number of digits for counter padding (e.g., 4 = 0001, 0002, ...)'
124
+ },
125
+ {
126
+ 'name': 'data_source',
127
+ 'label': 'Data Source',
128
+ 'type': 'select',
129
+ 'options': [
130
+ {'value': 'whole_message', 'label': 'Whole Message'},
131
+ {'value': MessageKeys.PAYLOAD, 'label': f'{MessageKeys.MSG}.{MessageKeys.PAYLOAD}'},
132
+ {'value': MessageKeys.TOPIC, 'label': f'{MessageKeys.MSG}.{MessageKeys.TOPIC}'},
133
+ {'value': 'custom', 'label': 'Custom Path'}
134
+ ],
135
+ 'default': DEFAULT_CONFIG['data_source'],
136
+ 'help': 'What part of the message to write'
137
+ },
138
+ {
139
+ 'name': 'custom_path',
140
+ 'label': 'Custom Message Path',
141
+ 'type': 'text',
142
+ 'default': DEFAULT_CONFIG['custom_path'],
143
+ 'help': 'Custom path when data_source is "custom" (e.g., msg.payload.detections, msg.cv.image)',
144
+ 'showIf': {'data_source': 'custom'}
145
+ },
146
+ {
147
+ 'name': 'output_format',
148
+ 'label': 'Output Format',
149
+ 'type': 'select',
150
+ 'options': [
151
+ {'value': 'json', 'label': 'JSON (human readable)'},
152
+ {'value': 'json_compact', 'label': 'JSON (compact)'},
153
+ {'value': 'pickle', 'label': 'Pickle (binary)'},
154
+ {'value': 'numpy', 'label': 'NumPy (.npy binary)'},
155
+ {'value': 'text', 'label': 'Text (string representation)'},
156
+ {'value': 'raw', 'label': 'Raw (bytes as-is)'}
157
+ ],
158
+ 'default': DEFAULT_CONFIG['output_format'],
159
+ 'help': 'How to format the data before writing'
160
+ },
161
+ {
162
+ 'name': 'overwrite',
163
+ 'label': 'Overwrite Existing',
164
+ 'type': 'select',
165
+ 'options': [
166
+ {'value': 'false', 'label': 'No (skip if exists)'},
167
+ {'value': 'true', 'label': 'Yes (overwrite)'}
168
+ ],
169
+ 'default': DEFAULT_CONFIG['overwrite'],
170
+ 'help': 'Overwrite existing files or skip them'
171
+ },
172
+ {
173
+ 'name': 'create_subdirs',
174
+ 'label': 'Create Subdirectories',
175
+ 'type': 'select',
176
+ 'options': [
177
+ {'value': 'true', 'label': 'Yes'},
178
+ {'value': 'false', 'label': 'No'}
179
+ ],
180
+ 'default': DEFAULT_CONFIG['create_subdirs'],
181
+ 'help': 'Create directory structure if it doesn\'t exist'
182
+ }
183
+ ]
184
+
185
+ def __init__(self, node_id=None, name="message writer"):
186
+ super().__init__(node_id, name)
187
+ self._counter = 0
188
+ self._last_written = None
189
+
190
+ def on_input(self, msg: Dict[str, Any], input_index: int = 0):
191
+ """
192
+ Write message data to disk. Can write whole message or specific components.
193
+ """
194
+ try:
195
+ # Get directory from config
196
+ directory = self.config.get('directory', './output')
197
+
198
+ # Get filename/prefix - prioritize msg.fname over config
199
+ if 'fname' in msg:
200
+ filename = str(msg['fname'])
201
+ else:
202
+ filename = self.config.get('filename', 'message')
203
+
204
+ # Get extension - prioritize msg.extension over config
205
+ if 'extension' in msg:
206
+ extension = str(msg['extension']).lstrip('.')
207
+ else:
208
+ extension = self.config.get('extension', 'json')
209
+
210
+ # Generate full filename based on naming mode
211
+ naming_mode = self.config.get('naming_mode', 'counter')
212
+ full_filename = self._generate_filename(filename, extension, naming_mode, msg)
213
+
214
+ # Build full path
215
+ full_path = os.path.join(directory, full_filename)
216
+
217
+ # Create directory if needed
218
+ create_subdirs = self.get_config_bool('create_subdirs', True)
219
+ if create_subdirs:
220
+ os.makedirs(directory, exist_ok=True)
221
+ elif not os.path.exists(directory):
222
+ self.report_error(f"Directory does not exist: {directory}")
223
+ return
224
+
225
+ # Check if file exists and handle overwrite setting
226
+ overwrite = self.get_config_bool('overwrite', False)
227
+ if os.path.exists(full_path) and not overwrite:
228
+ output_msg = self.create_message(
229
+ payload={
230
+ 'status': 'skipped',
231
+ 'reason': 'file_exists',
232
+ 'path': full_path
233
+ },
234
+ topic=msg.get(MessageKeys.TOPIC, 'MessageWriter')
235
+ )
236
+ self.send(output_msg)
237
+ return
238
+
239
+ # Extract data to write based on data_source setting
240
+ data_to_write = self._extract_data(msg)
241
+
242
+ if data_to_write is None:
243
+ self.report_error("No data found to write")
244
+ return
245
+
246
+ # Write data based on output format
247
+ bytes_written = self._write_data(full_path, data_to_write, extension)
248
+
249
+ # Track last written file
250
+ self._last_written = full_path
251
+
252
+ # Send success message
253
+ output_msg = self.create_message(
254
+ payload={
255
+ 'status': 'success',
256
+ 'path': full_path,
257
+ 'filename': full_filename,
258
+ 'bytes': bytes_written,
259
+ 'counter': self._counter,
260
+ 'data_source': self.config.get('data_source', 'whole_message')
261
+ },
262
+ topic=msg.get(MessageKeys.TOPIC, 'MessageWriter')
263
+ )
264
+ self.send(output_msg)
265
+
266
+ except Exception as e:
267
+ self.report_error(f"Failed to write file: {str(e)}")
268
+ output_msg = self.create_message(
269
+ payload={
270
+ 'status': 'error',
271
+ 'error': str(e)
272
+ },
273
+ topic=msg.get(MessageKeys.TOPIC, 'MessageWriter/error')
274
+ )
275
+ self.send(output_msg)
276
+
277
+ def _extract_data(self, msg: Dict[str, Any]) -> Any:
278
+ """Extract data from message based on data_source configuration."""
279
+ data_source = self.config.get('data_source', 'whole_message')
280
+
281
+ if data_source == 'whole_message':
282
+ return msg
283
+ elif data_source == MessageKeys.PAYLOAD:
284
+ return msg.get(MessageKeys.PAYLOAD)
285
+ elif data_source == MessageKeys.TOPIC:
286
+ return msg.get(MessageKeys.TOPIC)
287
+ elif data_source == 'custom':
288
+ custom_path = self.config.get('custom_path', 'msg.payload')
289
+ return self._extract_custom_path(msg, custom_path)
290
+ else:
291
+ return msg.get(MessageKeys.PAYLOAD) # Default fallback
292
+
293
+ def _extract_custom_path(self, msg: Dict[str, Any], path: str) -> Any:
294
+ """Extract data using custom path like 'msg.payload.detections'."""
295
+ try:
296
+ # Remove 'msg.' prefix if present
297
+ if path.startswith('msg.'):
298
+ path = path[4:]
299
+
300
+ # Split path into components
301
+ parts = path.split('.')
302
+
303
+ # Start with the message
304
+ current = msg
305
+
306
+ # Navigate through the path
307
+ for part in parts:
308
+ if isinstance(current, dict):
309
+ current = current.get(part)
310
+ elif hasattr(current, part):
311
+ current = getattr(current, part)
312
+ else:
313
+ return None
314
+
315
+ if current is None:
316
+ return None
317
+
318
+ return current
319
+
320
+ except Exception as e:
321
+ self.report_error(f"Failed to extract custom path '{path}': {str(e)}")
322
+ return None
323
+
324
+ def _generate_filename(self, base: str, extension: str, mode: str, msg: Dict[str, Any]) -> str:
325
+ """Generate filename based on naming mode."""
326
+
327
+ if mode == 'message':
328
+ # Use filename from message as-is (msg.fname already used for base)
329
+ return f"{base}.{extension}"
330
+
331
+ elif mode == 'counter':
332
+ # Increment counter
333
+ self._counter += 1
334
+ digits = self.get_config_int('counter_digits', 4)
335
+ counter_str = str(self._counter).zfill(digits)
336
+
337
+ # Replace {counter} placeholder if present, or append
338
+ if '{counter}' in base:
339
+ filename = base.replace('{counter}', counter_str)
340
+ else:
341
+ filename = f"{base}_{counter_str}"
342
+
343
+ return f"{filename}.{extension}"
344
+
345
+ elif mode == 'timestamp':
346
+ # Unix timestamp
347
+ timestamp = int(time.time() * 1000) # milliseconds
348
+
349
+ # Replace {timestamp} placeholder if present, or append
350
+ if '{timestamp}' in base:
351
+ filename = base.replace('{timestamp}', str(timestamp))
352
+ else:
353
+ filename = f"{base}_{timestamp}"
354
+
355
+ return f"{filename}.{extension}"
356
+
357
+ elif mode == 'datetime':
358
+ # Human-readable datetime
359
+ dt = datetime.now()
360
+ datetime_str = dt.strftime('%Y%m%d_%H%M%S_%f')[:-3] # Include milliseconds
361
+
362
+ # Replace {datetime} placeholder if present, or append
363
+ if '{datetime}' in base:
364
+ filename = base.replace('{datetime}', datetime_str)
365
+ else:
366
+ filename = f"{base}_{datetime_str}"
367
+
368
+ return f"{filename}.{extension}"
369
+
370
+ else:
371
+ # Default: just append extension
372
+ return f"{base}.{extension}"
373
+
374
+ def _write_data(self, path: str, data: Any, extension: str) -> int:
375
+ """Write data to file based on output_format configuration."""
376
+ output_format = self.config.get('output_format', 'json')
377
+
378
+ try:
379
+ if output_format == 'json':
380
+ # Pretty-printed JSON with proper numpy array handling
381
+ json_str = json.dumps(data, indent=2, default=self._json_serializer)
382
+ with open(path, 'w', encoding='utf-8') as f:
383
+ f.write(json_str)
384
+ return len(json_str.encode('utf-8'))
385
+
386
+ elif output_format == 'json_compact':
387
+ # Compact JSON with proper numpy array handling
388
+ json_str = json.dumps(data, separators=(',', ':'), default=self._json_serializer)
389
+ with open(path, 'w', encoding='utf-8') as f:
390
+ f.write(json_str)
391
+ return len(json_str.encode('utf-8'))
392
+
393
+ elif output_format == 'pickle':
394
+ # Pickle binary format
395
+ with open(path, 'wb') as f:
396
+ pickle.dump(data, f)
397
+ return os.path.getsize(path)
398
+
399
+ elif output_format == 'numpy':
400
+ # NumPy binary format (.npy)
401
+ try:
402
+ import numpy as np
403
+ if isinstance(data, np.ndarray):
404
+ np.save(path, data)
405
+ return os.path.getsize(path)
406
+ else:
407
+ # If not a numpy array, try to convert it
408
+ np_data = np.array(data)
409
+ np.save(path, np_data)
410
+ return os.path.getsize(path)
411
+ except ImportError:
412
+ # Fallback to pickle if numpy not available
413
+ with open(path, 'wb') as f:
414
+ pickle.dump(data, f)
415
+ return os.path.getsize(path)
416
+
417
+ elif output_format == 'text':
418
+ # String representation
419
+ text_data = str(data)
420
+ with open(path, 'w', encoding='utf-8') as f:
421
+ f.write(text_data)
422
+ return len(text_data.encode('utf-8'))
423
+
424
+ elif output_format == 'raw':
425
+ # Raw bytes - data must be bytes-like
426
+ if isinstance(data, bytes):
427
+ with open(path, 'wb') as f:
428
+ f.write(data)
429
+ return len(data)
430
+ elif isinstance(data, str):
431
+ # Convert string to bytes
432
+ data_bytes = data.encode('utf-8')
433
+ with open(path, 'wb') as f:
434
+ f.write(data_bytes)
435
+ return len(data_bytes)
436
+ else:
437
+ # Check if it's a numpy array
438
+ try:
439
+ import numpy as np
440
+ if isinstance(data, np.ndarray):
441
+ # Save as raw numpy bytes
442
+ data_bytes = data.tobytes()
443
+ with open(path, 'wb') as f:
444
+ f.write(data_bytes)
445
+ return len(data_bytes)
446
+ except ImportError:
447
+ pass
448
+
449
+ # Try to convert to bytes via pickle
450
+ data_bytes = pickle.dumps(data)
451
+ with open(path, 'wb') as f:
452
+ f.write(data_bytes)
453
+ return len(data_bytes)
454
+
455
+ else:
456
+ # Default to JSON
457
+ json_str = json.dumps(data, indent=2, default=self._json_serializer)
458
+ with open(path, 'w', encoding='utf-8') as f:
459
+ f.write(json_str)
460
+ return len(json_str.encode('utf-8'))
461
+
462
+ except Exception as e:
463
+ # Fallback to string representation if serialization fails
464
+ text_data = str(data)
465
+ with open(path, 'w', encoding='utf-8') as f:
466
+ f.write(text_data)
467
+ return len(text_data.encode('utf-8'))
468
+
469
+ def _json_serializer(self, obj):
470
+ """Custom JSON serializer to handle numpy arrays and other special types."""
471
+ try:
472
+ import numpy as np
473
+ if isinstance(obj, np.ndarray):
474
+ # Convert numpy array to base64 string for JSON serialization
475
+ return {
476
+ "_numpy_array": True,
477
+ "data": base64.b64encode(obj.tobytes()).decode('utf-8'),
478
+ "dtype": str(obj.dtype),
479
+ "shape": obj.shape
480
+ }
481
+ elif isinstance(obj, np.integer):
482
+ return int(obj)
483
+ elif isinstance(obj, np.floating):
484
+ return float(obj)
485
+ except ImportError:
486
+ pass
487
+
488
+ # Handle other types that aren't JSON serializable
489
+ if hasattr(obj, '__dict__'):
490
+ return obj.__dict__
491
+
492
+ # Default fallback
493
+ return str(obj)
494
+
495
+ def get_counter(self) -> int:
496
+ """Get current counter value."""
497
+ return self._counter
498
+
499
+ def reset_counter(self):
500
+ """Reset counter to 0."""
501
+ self._counter = 0
502
+
503
+ def get_last_written(self):
504
+ """Get path of last written file."""
505
+ return self._last_written
@@ -0,0 +1,3 @@
1
+ from .omron_camera_node import OmronCameraNode
2
+
3
+ __all__ = ['OmronCameraNode']