harnice 0.3.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.
- harnice/__init__.py +0 -0
- harnice/__main__.py +4 -0
- harnice/cli.py +234 -0
- harnice/fileio.py +295 -0
- harnice/gui/launcher.py +426 -0
- harnice/lists/channel_map.py +182 -0
- harnice/lists/circuits_list.py +302 -0
- harnice/lists/disconnect_map.py +237 -0
- harnice/lists/formboard_graph.py +63 -0
- harnice/lists/instances_list.py +280 -0
- harnice/lists/library_history.py +40 -0
- harnice/lists/manifest.py +93 -0
- harnice/lists/post_harness_instances_list.py +66 -0
- harnice/lists/rev_history.py +325 -0
- harnice/lists/signals_list.py +135 -0
- harnice/products/__init__.py +1 -0
- harnice/products/cable.py +152 -0
- harnice/products/chtype.py +80 -0
- harnice/products/device.py +844 -0
- harnice/products/disconnect.py +225 -0
- harnice/products/flagnote.py +139 -0
- harnice/products/harness.py +522 -0
- harnice/products/macro.py +10 -0
- harnice/products/part.py +640 -0
- harnice/products/system.py +125 -0
- harnice/products/tblock.py +270 -0
- harnice/state.py +57 -0
- harnice/utils/appearance.py +51 -0
- harnice/utils/circuit_utils.py +326 -0
- harnice/utils/feature_tree_utils.py +183 -0
- harnice/utils/formboard_utils.py +973 -0
- harnice/utils/library_utils.py +333 -0
- harnice/utils/note_utils.py +417 -0
- harnice/utils/svg_utils.py +819 -0
- harnice/utils/system_utils.py +563 -0
- harnice-0.3.0.dist-info/METADATA +32 -0
- harnice-0.3.0.dist-info/RECORD +41 -0
- harnice-0.3.0.dist-info/WHEEL +5 -0
- harnice-0.3.0.dist-info/entry_points.txt +3 -0
- harnice-0.3.0.dist-info/licenses/LICENSE +19 -0
- harnice-0.3.0.dist-info/top_level.txt +1 -0
harnice/gui/launcher.py
ADDED
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from PySide6.QtWidgets import (
|
|
7
|
+
QApplication,
|
|
8
|
+
QWidget,
|
|
9
|
+
QPushButton,
|
|
10
|
+
QFileDialog,
|
|
11
|
+
QMenu,
|
|
12
|
+
)
|
|
13
|
+
from PySide6.QtCore import Qt, QThread, Signal, QObject
|
|
14
|
+
from PySide6.QtGui import QPainter, QPen
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def layout_config_path():
|
|
18
|
+
"""
|
|
19
|
+
Save layout JSON into the root of the harnice project directory.
|
|
20
|
+
"""
|
|
21
|
+
return Path(__file__).resolve().parents[2] / "gui_layout.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_harnice_render(cwd, lightweight=False):
|
|
25
|
+
"""
|
|
26
|
+
Safely run harnice.cli.main() without sys.exit closing the GUI.
|
|
27
|
+
"""
|
|
28
|
+
import harnice.cli
|
|
29
|
+
|
|
30
|
+
old_cwd = os.getcwd()
|
|
31
|
+
os.chdir(cwd)
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
sys.argv = ["harnice", "-l" if lightweight else "-r"]
|
|
35
|
+
try:
|
|
36
|
+
harnice.cli.main()
|
|
37
|
+
except SystemExit:
|
|
38
|
+
pass
|
|
39
|
+
finally:
|
|
40
|
+
os.chdir(old_cwd)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class RenderWorker(QObject):
|
|
44
|
+
finished = Signal(bool, str) # True = success, False = error, error_message
|
|
45
|
+
|
|
46
|
+
def __init__(self, cwd, lightweight):
|
|
47
|
+
super().__init__()
|
|
48
|
+
self.cwd = cwd
|
|
49
|
+
self.lightweight = lightweight
|
|
50
|
+
|
|
51
|
+
def run(self):
|
|
52
|
+
try:
|
|
53
|
+
run_harnice_render(self.cwd, self.lightweight)
|
|
54
|
+
self.finished.emit(True, "")
|
|
55
|
+
except Exception as e:
|
|
56
|
+
import traceback
|
|
57
|
+
|
|
58
|
+
error_msg = f"{type(e).__name__}: {str(e)}\n{traceback.format_exc()}"
|
|
59
|
+
self.finished.emit(False, error_msg)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class GridWidget(QWidget):
|
|
63
|
+
BUTTON_WIDTH = 180
|
|
64
|
+
BUTTON_HEIGHT = 40
|
|
65
|
+
BUTTON_WIDTH_MARGIN = 20
|
|
66
|
+
BUTTON_HEIGHT_MARGIN = 20
|
|
67
|
+
|
|
68
|
+
def __init__(self, parent=None):
|
|
69
|
+
super().__init__(parent)
|
|
70
|
+
self.grid_buttons = {}
|
|
71
|
+
self.setStyleSheet("background-color: white;")
|
|
72
|
+
self.setAutoFillBackground(True)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def GRID_SPACING_X(self):
|
|
76
|
+
return self.BUTTON_WIDTH + self.BUTTON_WIDTH_MARGIN
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def GRID_SPACING_Y(self):
|
|
80
|
+
return self.BUTTON_HEIGHT + self.BUTTON_HEIGHT_MARGIN
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def OFFSET_X(self):
|
|
84
|
+
return self.GRID_SPACING_X / 2
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def OFFSET_Y(self):
|
|
88
|
+
return self.GRID_SPACING_Y / 2
|
|
89
|
+
|
|
90
|
+
def paintEvent(self, event):
|
|
91
|
+
painter = QPainter(self)
|
|
92
|
+
painter.setRenderHint(QPainter.Antialiasing)
|
|
93
|
+
pen = QPen(Qt.GlobalColor.gray, 1, Qt.PenStyle.DotLine)
|
|
94
|
+
painter.setPen(pen)
|
|
95
|
+
|
|
96
|
+
width = self.width()
|
|
97
|
+
height = self.height()
|
|
98
|
+
|
|
99
|
+
x = self.OFFSET_X
|
|
100
|
+
while x < width:
|
|
101
|
+
painter.drawLine(x, 0, x, height)
|
|
102
|
+
x += self.GRID_SPACING_X
|
|
103
|
+
|
|
104
|
+
y = self.OFFSET_Y
|
|
105
|
+
while y < height:
|
|
106
|
+
painter.drawLine(0, y, width, y)
|
|
107
|
+
y += self.GRID_SPACING_Y
|
|
108
|
+
|
|
109
|
+
def grid_to_screen(self, grid_x, grid_y):
|
|
110
|
+
return (
|
|
111
|
+
grid_x * self.GRID_SPACING_X + self.OFFSET_X,
|
|
112
|
+
grid_y * self.GRID_SPACING_Y + self.OFFSET_Y,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def screen_to_grid(self, screen_x, screen_y):
|
|
116
|
+
return (
|
|
117
|
+
int((screen_x - self.OFFSET_X) / self.GRID_SPACING_X),
|
|
118
|
+
int((screen_y - self.OFFSET_Y) / self.GRID_SPACING_Y),
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
def is_grid_occupied(self, grid_x, grid_y, exclude=None):
|
|
122
|
+
btn = self.grid_buttons.get((grid_x, grid_y))
|
|
123
|
+
return btn is not None and btn is not exclude
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class PartButton(QPushButton):
|
|
127
|
+
def __init__(self, parent, label, path, grid_x, grid_y, main_window=None):
|
|
128
|
+
super().__init__(label, parent)
|
|
129
|
+
self.parent_grid = parent
|
|
130
|
+
self.path = path
|
|
131
|
+
self.grid_x = grid_x
|
|
132
|
+
self.grid_y = grid_y
|
|
133
|
+
self.main_window = main_window
|
|
134
|
+
|
|
135
|
+
# ✅ Store the intended "default / unclicked" theme
|
|
136
|
+
self.default_style = """
|
|
137
|
+
QPushButton {
|
|
138
|
+
background-color: #e6e6e6;
|
|
139
|
+
border: 1px solid #666;
|
|
140
|
+
border-radius: 4px;
|
|
141
|
+
}
|
|
142
|
+
QPushButton:hover {
|
|
143
|
+
background-color: #f2f2f2;
|
|
144
|
+
}
|
|
145
|
+
QPushButton:pressed {
|
|
146
|
+
background-color: #d0d0d0;
|
|
147
|
+
}
|
|
148
|
+
"""
|
|
149
|
+
self.setStyleSheet(self.default_style)
|
|
150
|
+
|
|
151
|
+
self.setFixedSize(parent.BUTTON_WIDTH, parent.BUTTON_HEIGHT)
|
|
152
|
+
self.dragStartPosition = None
|
|
153
|
+
self.is_dragging = False
|
|
154
|
+
self.show()
|
|
155
|
+
self.update_position()
|
|
156
|
+
|
|
157
|
+
def update_position(self):
|
|
158
|
+
x, y = self.parent_grid.grid_to_screen(self.grid_x, self.grid_y)
|
|
159
|
+
self.move(x - self.width() // 2, y - self.height() // 2)
|
|
160
|
+
self.raise_()
|
|
161
|
+
|
|
162
|
+
def mousePressEvent(self, event):
|
|
163
|
+
if event.button() == Qt.MouseButton.LeftButton:
|
|
164
|
+
self.dragStartPosition = event.position().toPoint()
|
|
165
|
+
self.is_dragging = False
|
|
166
|
+
super().mousePressEvent(event)
|
|
167
|
+
|
|
168
|
+
def mouseMoveEvent(self, event):
|
|
169
|
+
if (
|
|
170
|
+
not (event.buttons() & Qt.MouseButton.LeftButton)
|
|
171
|
+
or not self.dragStartPosition
|
|
172
|
+
):
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
if (event.position().toPoint() - self.dragStartPosition).manhattanLength() < 8:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
self.is_dragging = True
|
|
179
|
+
old_pos = (self.grid_x, self.grid_y)
|
|
180
|
+
|
|
181
|
+
global_pos = self.mapToGlobal(event.position().toPoint())
|
|
182
|
+
local_pos = self.parent_grid.mapFromGlobal(global_pos)
|
|
183
|
+
new_x, new_y = self.parent_grid.screen_to_grid(local_pos.x(), local_pos.y())
|
|
184
|
+
|
|
185
|
+
if (new_x, new_y) == old_pos:
|
|
186
|
+
return
|
|
187
|
+
|
|
188
|
+
if self.parent_grid.is_grid_occupied(new_x, new_y, exclude=self):
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
self.grid_x, self.grid_y = new_x, new_y
|
|
192
|
+
self.update_position()
|
|
193
|
+
|
|
194
|
+
self.parent_grid.grid_buttons.pop(old_pos, None)
|
|
195
|
+
self.parent_grid.grid_buttons[(new_x, new_y)] = self
|
|
196
|
+
|
|
197
|
+
def mouseReleaseEvent(self, event):
|
|
198
|
+
if self.is_dragging:
|
|
199
|
+
if self.main_window:
|
|
200
|
+
self.main_window.save_layout()
|
|
201
|
+
self.is_dragging = False
|
|
202
|
+
event.accept()
|
|
203
|
+
return
|
|
204
|
+
super().mouseReleaseEvent(event)
|
|
205
|
+
|
|
206
|
+
def contextMenuEvent(self, event):
|
|
207
|
+
if not self.main_window:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
menu = QMenu(self)
|
|
211
|
+
remove = menu.addAction("Remove button")
|
|
212
|
+
remove.triggered.connect(lambda: self.main_window.remove_button(self))
|
|
213
|
+
|
|
214
|
+
newrev = menu.addAction("Create new revision")
|
|
215
|
+
newrev.triggered.connect(lambda: self.main_window.new_rev(self))
|
|
216
|
+
|
|
217
|
+
menu.exec(event.globalPos())
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class HarniceGUI(QWidget):
|
|
221
|
+
def __init__(self):
|
|
222
|
+
super().__init__()
|
|
223
|
+
self.setWindowTitle("Harnice")
|
|
224
|
+
self.setWindowFlags(Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Window)
|
|
225
|
+
|
|
226
|
+
self.grid = GridWidget(self)
|
|
227
|
+
|
|
228
|
+
default_width = self.grid.GRID_SPACING_X * 6
|
|
229
|
+
default_height = self.grid.GRID_SPACING_Y * 2
|
|
230
|
+
self.resize(default_width, default_height)
|
|
231
|
+
self.grid.setGeometry(0, 0, default_width, default_height)
|
|
232
|
+
|
|
233
|
+
self.load_button = QPushButton("Load part for render...", self.grid)
|
|
234
|
+
self.load_button.setFixedSize(self.grid.BUTTON_WIDTH, self.grid.BUTTON_HEIGHT)
|
|
235
|
+
self.load_button.clicked.connect(self.pick_folder)
|
|
236
|
+
|
|
237
|
+
x, y = self.grid.grid_to_screen(0, 0)
|
|
238
|
+
self.load_button.move(
|
|
239
|
+
x - self.load_button.width() // 2, y - self.load_button.height() // 2
|
|
240
|
+
)
|
|
241
|
+
self.grid.grid_buttons[(0, 0)] = self.load_button
|
|
242
|
+
|
|
243
|
+
self._is_initializing = True
|
|
244
|
+
self.load_layout()
|
|
245
|
+
|
|
246
|
+
# Set window size after loading layout (if any)
|
|
247
|
+
self.apply_window_size()
|
|
248
|
+
self._is_initializing = False
|
|
249
|
+
|
|
250
|
+
def resizeEvent(self, event):
|
|
251
|
+
self.grid.setGeometry(0, 0, self.width(), self.height())
|
|
252
|
+
super().resizeEvent(event)
|
|
253
|
+
# Save layout when window is manually resized
|
|
254
|
+
if hasattr(self, "load_button") and not getattr(
|
|
255
|
+
self, "_is_initializing", False
|
|
256
|
+
):
|
|
257
|
+
self.save_layout()
|
|
258
|
+
|
|
259
|
+
def pick_folder(self):
|
|
260
|
+
folder = QFileDialog.getExistingDirectory(self, "Select revision folder")
|
|
261
|
+
if not folder:
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
gx, gy = self.find_next_grid_position()
|
|
265
|
+
label = os.path.basename(folder)
|
|
266
|
+
|
|
267
|
+
btn = PartButton(self.grid, label, folder, gx, gy, main_window=self)
|
|
268
|
+
btn.clicked.connect(lambda checked=False, p=folder: self.run_render(p))
|
|
269
|
+
self.grid.grid_buttons[(gx, gy)] = btn
|
|
270
|
+
self.save_layout()
|
|
271
|
+
|
|
272
|
+
def find_next_grid_position(self):
|
|
273
|
+
for y in range(200):
|
|
274
|
+
for x in range(200):
|
|
275
|
+
if not self.grid.is_grid_occupied(x, y):
|
|
276
|
+
return (x, y)
|
|
277
|
+
|
|
278
|
+
def run_render(self, cwd):
|
|
279
|
+
btn = next(
|
|
280
|
+
(
|
|
281
|
+
b
|
|
282
|
+
for b in self.grid.grid_buttons.values()
|
|
283
|
+
if isinstance(b, PartButton) and b.path == cwd
|
|
284
|
+
),
|
|
285
|
+
None,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
if btn:
|
|
289
|
+
btn.setStyleSheet("background-color: #b1ffb1;") # Green while running
|
|
290
|
+
|
|
291
|
+
self.thread = QThread()
|
|
292
|
+
self.worker = RenderWorker(cwd, False)
|
|
293
|
+
self.worker.moveToThread(self.thread)
|
|
294
|
+
|
|
295
|
+
self.thread.started.connect(self.worker.run)
|
|
296
|
+
self.worker.finished.connect(
|
|
297
|
+
lambda success, error_msg: self.on_render_finished(btn, success, error_msg)
|
|
298
|
+
)
|
|
299
|
+
self.worker.finished.connect(self.thread.quit)
|
|
300
|
+
self.worker.finished.connect(self.worker.deleteLater)
|
|
301
|
+
self.thread.finished.connect(self.thread.deleteLater)
|
|
302
|
+
self.thread.start()
|
|
303
|
+
|
|
304
|
+
def on_render_finished(self, btn, success, error_msg):
|
|
305
|
+
if not btn:
|
|
306
|
+
return
|
|
307
|
+
|
|
308
|
+
if success:
|
|
309
|
+
# ✅ Restore original intended appearance
|
|
310
|
+
btn.setStyleSheet(btn.default_style)
|
|
311
|
+
btn.update()
|
|
312
|
+
else:
|
|
313
|
+
btn.setStyleSheet("background-color: #ffb1b1;") # Error red
|
|
314
|
+
# Print error message and traceback to console in color
|
|
315
|
+
if error_msg:
|
|
316
|
+
# ANSI color codes
|
|
317
|
+
RED = "\033[91m"
|
|
318
|
+
BOLD = "\033[1m"
|
|
319
|
+
RESET = "\033[0m"
|
|
320
|
+
YELLOW = "\033[93m"
|
|
321
|
+
|
|
322
|
+
print(f"\n{RED}{BOLD}{'=' * 80}{RESET}")
|
|
323
|
+
print(f"{RED}{BOLD}ERROR in {YELLOW}{btn.path}{RESET}{RED}:{RESET}")
|
|
324
|
+
print(f"{RED}{BOLD}{'=' * 80}{RESET}")
|
|
325
|
+
print(f"{RED}{error_msg}{RESET}")
|
|
326
|
+
print(f"{RED}{BOLD}{'=' * 80}{RESET}\n")
|
|
327
|
+
|
|
328
|
+
def remove_button(self, button):
|
|
329
|
+
self.grid.grid_buttons.pop((button.grid_x, button.grid_y), None)
|
|
330
|
+
button.deleteLater()
|
|
331
|
+
self.save_layout()
|
|
332
|
+
|
|
333
|
+
def new_rev(self, button):
|
|
334
|
+
"""
|
|
335
|
+
Run harnice --newrev from the button's directory.
|
|
336
|
+
"""
|
|
337
|
+
import harnice.cli
|
|
338
|
+
|
|
339
|
+
old_cwd = os.getcwd()
|
|
340
|
+
os.chdir(button.path)
|
|
341
|
+
|
|
342
|
+
try:
|
|
343
|
+
sys.argv = ["harnice", "--newrev"]
|
|
344
|
+
try:
|
|
345
|
+
harnice.cli.main()
|
|
346
|
+
except SystemExit:
|
|
347
|
+
pass
|
|
348
|
+
finally:
|
|
349
|
+
os.chdir(old_cwd)
|
|
350
|
+
|
|
351
|
+
def save_layout(self):
|
|
352
|
+
data = {
|
|
353
|
+
"window": {
|
|
354
|
+
"width": self.width(),
|
|
355
|
+
"height": self.height(),
|
|
356
|
+
},
|
|
357
|
+
"buttons": [
|
|
358
|
+
{
|
|
359
|
+
"label": b.text(),
|
|
360
|
+
"path": b.path,
|
|
361
|
+
"grid_x": b.grid_x,
|
|
362
|
+
"grid_y": b.grid_y,
|
|
363
|
+
}
|
|
364
|
+
for (gx, gy), b in self.grid.grid_buttons.items()
|
|
365
|
+
if isinstance(b, PartButton)
|
|
366
|
+
],
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
with open(layout_config_path(), "w", encoding="utf-8") as f:
|
|
370
|
+
json.dump(data, f, indent=2)
|
|
371
|
+
|
|
372
|
+
def load_layout(self):
|
|
373
|
+
cfg = layout_config_path()
|
|
374
|
+
if not cfg.exists():
|
|
375
|
+
return
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
data = json.loads(cfg.read_text(encoding="utf-8"))
|
|
379
|
+
except Exception:
|
|
380
|
+
return
|
|
381
|
+
|
|
382
|
+
# Handle old format (array of buttons) vs new format (dict with buttons and window)
|
|
383
|
+
if isinstance(data, list):
|
|
384
|
+
items = data
|
|
385
|
+
self.saved_window_size = None
|
|
386
|
+
else:
|
|
387
|
+
items = data.get("buttons", [])
|
|
388
|
+
window_info = data.get("window")
|
|
389
|
+
if window_info:
|
|
390
|
+
self.saved_window_size = (
|
|
391
|
+
window_info.get("width"),
|
|
392
|
+
window_info.get("height"),
|
|
393
|
+
)
|
|
394
|
+
else:
|
|
395
|
+
self.saved_window_size = None
|
|
396
|
+
|
|
397
|
+
for item in items:
|
|
398
|
+
btn = PartButton(
|
|
399
|
+
self.grid,
|
|
400
|
+
item["label"],
|
|
401
|
+
item["path"],
|
|
402
|
+
item["grid_x"],
|
|
403
|
+
item["grid_y"],
|
|
404
|
+
main_window=self,
|
|
405
|
+
)
|
|
406
|
+
btn.clicked.connect(
|
|
407
|
+
lambda checked=False, p=item["path"]: self.run_render(p)
|
|
408
|
+
)
|
|
409
|
+
self.grid.grid_buttons[(item["grid_x"], item["grid_y"])] = btn
|
|
410
|
+
|
|
411
|
+
def apply_window_size(self):
|
|
412
|
+
if hasattr(self, "saved_window_size") and self.saved_window_size:
|
|
413
|
+
width, height = self.saved_window_size
|
|
414
|
+
self.resize(width, height)
|
|
415
|
+
self.grid.setGeometry(0, 0, width, height)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def main():
|
|
419
|
+
app = QApplication([])
|
|
420
|
+
gui = HarniceGUI()
|
|
421
|
+
gui.show()
|
|
422
|
+
app.exec()
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
if __name__ == "__main__":
|
|
426
|
+
main()
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import os
|
|
3
|
+
from harnice import fileio
|
|
4
|
+
|
|
5
|
+
COLUMNS = [
|
|
6
|
+
"merged_net", #documentation needed
|
|
7
|
+
"from_device_refdes", #documentation needed
|
|
8
|
+
"from_device_channel_id", #documentation needed
|
|
9
|
+
"from_channel_type", #documentation needed
|
|
10
|
+
"to_device_refdes", #documentation needed
|
|
11
|
+
"to_device_channel_id", #documentation needed
|
|
12
|
+
"to_channel_type", #documentation needed
|
|
13
|
+
"multi_ch_junction_id", #documentation needed
|
|
14
|
+
"disconnect_refdes_requirement", #documentation needed
|
|
15
|
+
"chain_of_connectors", #documentation needed
|
|
16
|
+
"chain_of_nets", #documentation needed
|
|
17
|
+
"manual_map_channel_python_equiv", #documentation needed
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def new():
|
|
22
|
+
"""
|
|
23
|
+
Makes a new blank channel map. Overwrites existing channel map.
|
|
24
|
+
|
|
25
|
+
Args: none
|
|
26
|
+
|
|
27
|
+
Returns: none
|
|
28
|
+
"""
|
|
29
|
+
channel_map = []
|
|
30
|
+
|
|
31
|
+
for connector in fileio.read_tsv("system connector list"):
|
|
32
|
+
device_refdes = connector.get("device_refdes")
|
|
33
|
+
|
|
34
|
+
if connector.get("disconnect") == "TRUE":
|
|
35
|
+
continue
|
|
36
|
+
|
|
37
|
+
device_signals_list_path = os.path.join(
|
|
38
|
+
fileio.dirpath("instance_data"),
|
|
39
|
+
"device",
|
|
40
|
+
device_refdes,
|
|
41
|
+
f"{device_refdes}-signals_list.tsv",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
for signal in fileio.read_tsv(device_signals_list_path):
|
|
45
|
+
sig_channel = signal.get("channel_id")
|
|
46
|
+
|
|
47
|
+
already = any(
|
|
48
|
+
row.get("from_device_refdes") == device_refdes
|
|
49
|
+
and row.get("from_device_channel_id") == sig_channel
|
|
50
|
+
for row in channel_map
|
|
51
|
+
)
|
|
52
|
+
if already:
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
if not signal.get("connector_name") == connector.get("connector"):
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
channel_map_row = {
|
|
59
|
+
"merged_net": connector.get("merged_net", ""),
|
|
60
|
+
"from_channel_type": signal.get("channel_type", ""),
|
|
61
|
+
"from_device_refdes": device_refdes,
|
|
62
|
+
"from_device_channel_id": sig_channel,
|
|
63
|
+
}
|
|
64
|
+
channel_map.append(channel_map_row)
|
|
65
|
+
|
|
66
|
+
# write channel map TSV
|
|
67
|
+
with open(fileio.path("channel map"), "w", newline="", encoding="utf-8") as f:
|
|
68
|
+
writer = csv.DictWriter(f, fieldnames=COLUMNS, delimiter="\t")
|
|
69
|
+
writer.writeheader()
|
|
70
|
+
writer.writerows(channel_map)
|
|
71
|
+
|
|
72
|
+
# initialize mapped channels set TSV (empty, single column)
|
|
73
|
+
with open(
|
|
74
|
+
fileio.path("mapped channels set"), "w", newline="", encoding="utf-8"
|
|
75
|
+
) as f:
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
return channel_map
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def map(from_key, to_key=None, multi_ch_junction_key=""):
|
|
82
|
+
if from_key in already_mapped_set():
|
|
83
|
+
raise ValueError(f"from_key {from_key} already mapped")
|
|
84
|
+
if to_key and to_key in already_mapped_set():
|
|
85
|
+
raise ValueError(f"to_key {to_key} already mapped")
|
|
86
|
+
|
|
87
|
+
channels = fileio.read_tsv("channel map")
|
|
88
|
+
|
|
89
|
+
to_channel = None
|
|
90
|
+
for channel in channels:
|
|
91
|
+
if (
|
|
92
|
+
channel.get("from_device_refdes") == to_key[0]
|
|
93
|
+
and channel.get("from_device_channel_id") == to_key[1]
|
|
94
|
+
):
|
|
95
|
+
to_channel = channel
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
from_channel = None
|
|
99
|
+
for channel in channels:
|
|
100
|
+
if (
|
|
101
|
+
channel.get("from_device_refdes") == from_key[0]
|
|
102
|
+
and channel.get("from_device_channel_id") == from_key[1]
|
|
103
|
+
):
|
|
104
|
+
from_channel = channel
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
if not to_channel and multi_ch_junction_key == "":
|
|
108
|
+
raise ValueError(f"to_key {to_key} not found in channel map")
|
|
109
|
+
else:
|
|
110
|
+
require_to = bool(to_key[0] or to_key[1])
|
|
111
|
+
|
|
112
|
+
updated_channels, found_from, found_to = [], False, False
|
|
113
|
+
|
|
114
|
+
for from_channel in channels:
|
|
115
|
+
if (
|
|
116
|
+
from_channel.get("from_device_refdes") == from_key[0]
|
|
117
|
+
and from_channel.get("from_device_channel_id") == from_key[1]
|
|
118
|
+
):
|
|
119
|
+
from_channel["to_device_refdes"] = to_key[0]
|
|
120
|
+
from_channel["to_device_channel_id"] = to_key[1]
|
|
121
|
+
from_channel["to_channel_type"] = to_channel.get("from_channel_type")
|
|
122
|
+
if multi_ch_junction_key:
|
|
123
|
+
from_channel["multi_ch_junction_id"] = multi_ch_junction_key
|
|
124
|
+
found_from = True
|
|
125
|
+
|
|
126
|
+
if require_to:
|
|
127
|
+
from_channel["manual_map_channel_python_equiv"] = (
|
|
128
|
+
f"channel_map.map({from_key}, {to_key})"
|
|
129
|
+
)
|
|
130
|
+
elif multi_ch_junction_key:
|
|
131
|
+
from_channel["manual_map_channel_python_equiv"] = (
|
|
132
|
+
f"channel_map.map({from_key}, multi_ch_junction_key={multi_ch_junction_key})"
|
|
133
|
+
)
|
|
134
|
+
elif (
|
|
135
|
+
require_to
|
|
136
|
+
and from_channel.get("from_device_refdes") == to_key[0]
|
|
137
|
+
and from_channel.get("from_device_channel_id") == to_key[1]
|
|
138
|
+
):
|
|
139
|
+
found_to = True
|
|
140
|
+
continue
|
|
141
|
+
updated_channels.append(from_channel)
|
|
142
|
+
|
|
143
|
+
if not found_from:
|
|
144
|
+
raise ValueError(f"from_key {from_key} not found in channel map")
|
|
145
|
+
if require_to and not found_to:
|
|
146
|
+
raise ValueError(f"to_key {to_key} not found in channel map")
|
|
147
|
+
|
|
148
|
+
already_mapped_set_append(from_key)
|
|
149
|
+
already_mapped_set_append(to_key)
|
|
150
|
+
|
|
151
|
+
with open(fileio.path("channel map"), "w", newline="", encoding="utf-8") as f:
|
|
152
|
+
writer = csv.DictWriter(f, fieldnames=COLUMNS, delimiter="\t")
|
|
153
|
+
writer.writeheader()
|
|
154
|
+
writer.writerows(updated_channels)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def already_mapped_set_append(key):
|
|
158
|
+
items = already_mapped_set()
|
|
159
|
+
if str(key) in items:
|
|
160
|
+
raise ValueError(f"key {key} already mapped")
|
|
161
|
+
items.add(str(key))
|
|
162
|
+
with open(
|
|
163
|
+
fileio.path("mapped channels set"), "w", newline="", encoding="utf-8"
|
|
164
|
+
) as f:
|
|
165
|
+
writer = csv.writer(f, delimiter="\t")
|
|
166
|
+
for item in sorted(items):
|
|
167
|
+
writer.writerow([item])
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def already_mapped_set():
|
|
171
|
+
if not os.path.exists(fileio.path("mapped channels set")):
|
|
172
|
+
return set()
|
|
173
|
+
with open(fileio.path("mapped channels set"), newline="", encoding="utf-8") as f:
|
|
174
|
+
reader = csv.reader(f, delimiter="\t")
|
|
175
|
+
return set(row[0] for row in reader if row)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def already_mapped(key):
|
|
179
|
+
if str(key) in already_mapped_set():
|
|
180
|
+
return True
|
|
181
|
+
else:
|
|
182
|
+
return False
|