From 228e4e4f0b125bf7c7edb3344c866ebd1d09598c Mon Sep 17 00:00:00 2001 From: Danil Iarosh Date: Fri, 31 Jul 2026 09:42:54 +0300 Subject: [PATCH] Add support for script generation strategies and block-level LED functions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен диалог выбора стратегий генерации (Сетка и Блоки) в GUI. - Добавлен C++ код для управления светодиодами в блоках по их ID и по количеству в vm_map.h и virtual_matrix_legacy.ino.txt. - Обновлены функции генератора, добавлена поддержка индивидуальных параметров генерации. - Написаны интеграционные тесты для проверки генерации стратегий и функций управления блоками. --- main_window.py | 55 ++++- panel_mapping_generator.py | 490 +++++++++++++++++++++++++------------ qa_smoke.py | 44 ++++ 3 files changed, 433 insertions(+), 156 deletions(-) diff --git a/main_window.py b/main_window.py index 6ec6217..ca6032b 100644 --- a/main_window.py +++ b/main_window.py @@ -9,6 +9,7 @@ from PySide6.QtWidgets import ( QAbstractItemView, QAbstractScrollArea, QApplication, + QCheckBox, QDialog, QDialogButtonBox, QDoubleSpinBox, @@ -147,6 +148,43 @@ class SidebarTab(QWidget): super().mousePressEvent(event) +class GenerationStrategyDialog(QDialog): + def __init__(self, parent: QWidget | None = None) -> None: + super().__init__(parent) + self.setWindowTitle("Выбор стратегии генерации") + self.setMinimumWidth(360) + + layout = QVBoxLayout(self) + + label = QLabel("Выберите элементы для включения в сгенерированный скрипт:") + layout.addWidget(label) + + self.grid_cb = QCheckBox("Сетка светодиодов (виртуальная матрица)") + self.grid_cb.setChecked(True) + layout.addWidget(self.grid_cb) + + self.blocks_cb = QCheckBox("Блоки светодиодов (подсветка по id и количеству)") + self.blocks_cb.setChecked(True) + layout.addWidget(self.blocks_cb) + + buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel) + localize_dialog_buttons(buttons) + buttons.accepted.connect(self.accept) + buttons.rejected.connect(self.reject) + layout.addWidget(buttons) + + self.ok_button = buttons.button(QDialogButtonBox.StandardButton.Ok) + + self.grid_cb.toggled.connect(self.validate) + self.blocks_cb.toggled.connect(self.validate) + + def validate(self) -> None: + if not self.grid_cb.isChecked() and not self.blocks_cb.isChecked(): + self.ok_button.setEnabled(False) + else: + self.ok_button.setEnabled(True) + + class MainWindow(QMainWindow): def __init__(self) -> None: super().__init__() @@ -906,8 +944,23 @@ class MainWindow(QMainWindow): if not selected_dir: return output_dir = selected_dir + + dialog = GenerationStrategyDialog(self) + if dialog.exec() != QDialog.Accepted: + self.status.setText("Генерация отменена.") + self._refresh_ui_state() + return + + generate_grid = dialog.grid_cb.isChecked() + generate_blocks = dialog.blocks_cb.isChecked() + try: - result = generate_panel_mapping_from_document(self.document, output_dir) + result = generate_panel_mapping_from_document( + self.document, + output_dir, + generate_grid=generate_grid, + generate_blocks=generate_blocks, + ) except Exception as exc: show_critical(self, "Ошибка генерации", str(exc)) self.status.setText(f"Ошибка генерации скрипта: {exc}") diff --git a/panel_mapping_generator.py b/panel_mapping_generator.py index be123c0..74910fd 100644 --- a/panel_mapping_generator.py +++ b/panel_mapping_generator.py @@ -69,7 +69,13 @@ class BuildResult: unassigned_count: int -def generate_panel_mapping_from_document(document: MappingDocument, output_dir: str | Path) -> BuildResult: +def generate_panel_mapping_from_document( + document: MappingDocument, + output_dir: str | Path, + *, + generate_grid: bool = True, + generate_blocks: bool = False, +) -> BuildResult: if not document.points: raise ValueError("Нет точек для генерации скрипта.") @@ -91,8 +97,23 @@ def generate_panel_mapping_from_document(document: MappingDocument, output_dir: "readme": output_dir / "README.md", } - write_vm_map_header(generated_files["vm_map"], points, mapping_points, cells, block_count) - write_legacy_ino(generated_files["legacy_ino"], mapping_points, cells) + write_vm_map_header( + generated_files["vm_map"], + points, + mapping_points, + cells, + document, + generate_grid=generate_grid, + generate_blocks=generate_blocks, + ) + write_legacy_ino( + generated_files["legacy_ino"], + mapping_points, + cells, + document, + generate_grid=generate_grid, + generate_blocks=generate_blocks, + ) write_ordered_json(generated_files["ordered_json"], points, document) write_validation_report(generated_files["validation_report"], points) write_readme(generated_files["readme"]) @@ -180,12 +201,35 @@ def write_vm_map_header( all_points: list[PanelLedPoint], mapping_points: list[PanelLedPoint], cells: dict[tuple[int, int], list[PanelLedPoint]], - block_count: int, + document: MappingDocument, + generate_grid: bool = True, + generate_blocks: bool = False, ) -> None: vm_rows, vm_cols, grid_rows, grid_cols = get_dimensions(all_points) min_row, max_row, min_col, max_col = get_source_bounds(all_points) timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + # Collect blocks info + all_blocks = [] + for line in document.lines: + all_blocks.extend(line.blocks) + block_count = len(all_blocks) + + point_by_input_index = {p.input_index: p for p in all_points} + flat_block_leds = [] + block_ranges = [] + for block in all_blocks: + block_points = [] + for point_uid in block.point_ids: + p = point_by_input_index.get(point_uid) + if p is not None and p.strip is not None and p.led_id is not None: + block_points.append(p) + + offset = len(flat_block_leds) + count = len(block_points) + flat_block_leds.extend(block_points) + block_ranges.append((offset, count, block.name)) + with path.open("w", encoding="utf-8", newline="\n") as file: file.write("#pragma once\n") file.write("// Auto-generated by LED Mapper GUI\n") @@ -193,94 +237,176 @@ def write_vm_map_header( file.write("// Virtual matrix mapping plus physical LED grid-cell metadata.\n\n") file.write("#include \n") file.write("#include \n\n") - file.write(f"#define VM_ROWS {vm_rows}\n") - file.write(f"#define VM_COLS {vm_cols}\n") - file.write("#define VM_CELLS (VM_ROWS * VM_COLS)\n") - file.write(f"#define VM_MAX_IDS_PER_CELL {MAX_LEDS_PER_CELL}\n") + file.write(f"#define VM_NUM_STRIPS {NUM_STRIPS}\n") file.write(f"#define LED_COUNT {len(mapping_points)}\n") file.write(f"#define SOURCE_POINT_COUNT {len(all_points)}\n") - file.write(f"#define GRID_ROWS {grid_rows}\n") - file.write(f"#define GRID_COLS {grid_cols}\n") - file.write(f"#define GRID_ROW_MIN {min_row}\n") - file.write(f"#define GRID_ROW_MAX {max_row}\n") - file.write(f"#define GRID_COL_MIN {min_col}\n") - file.write(f"#define GRID_COL_MAX {max_col}\n") - file.write(f"#define GRID_CELL_SIZE_MM {CELL_SIZE_MM:.1f}f\n") file.write(f"#define LED_BLOCK_COUNT {block_count}\n\n") - file.write("// Source grid -> virtual matrix orientation used by this file.\n") - file.write("// VM row is the source column flipped from right to left.\n") - file.write("// VM column is the source row shifted to zero-based coordinates.\n") - file.write("#define VM_ROW_FROM_GRID_COL(col) (GRID_COL_MAX - (col))\n") - file.write("#define VM_COL_FROM_GRID_ROW(row) ((row) - GRID_ROW_MIN)\n") - file.write("#define VM_GRID_ROW_FROM_VM_COL(col) ((col) + GRID_ROW_MIN)\n") - file.write("#define VM_GRID_COL_FROM_VM_ROW(row) (GRID_COL_MAX - (row))\n\n") + + if generate_grid: + file.write(f"#define VM_ROWS {vm_rows}\n") + file.write(f"#define VM_COLS {vm_cols}\n") + file.write("#define VM_CELLS (VM_ROWS * VM_COLS)\n") + file.write(f"#define VM_MAX_IDS_PER_CELL {MAX_LEDS_PER_CELL}\n") + file.write(f"#define GRID_ROWS {grid_rows}\n") + file.write(f"#define GRID_COLS {grid_cols}\n") + file.write(f"#define GRID_ROW_MIN {min_row}\n") + file.write(f"#define GRID_ROW_MAX {max_row}\n") + file.write(f"#define GRID_COL_MIN {min_col}\n") + file.write(f"#define GRID_COL_MAX {max_col}\n") + file.write(f"#define GRID_CELL_SIZE_MM {CELL_SIZE_MM:.1f}f\n\n") + file.write("// Source grid -> virtual matrix orientation used by this file.\n") + file.write("// VM row is the source column flipped from right to left.\n") + file.write("// VM column is the source row shifted to zero-based coordinates.\n") + file.write("#define VM_ROW_FROM_GRID_COL(col) (GRID_COL_MAX - (col))\n") + file.write("#define VM_COL_FROM_GRID_ROW(row) ((row) - GRID_ROW_MIN)\n") + file.write("#define VM_GRID_ROW_FROM_VM_COL(col) ((col) + GRID_ROW_MIN)\n") + file.write("#define VM_GRID_COL_FROM_VM_ROW(row) (GRID_COL_MAX - (row))\n\n") file.write("struct LEDMap {\n") file.write(" uint8_t strip;\n") file.write(" uint16_t led_id;\n") file.write("};\n\n") - file.write("struct GridCellRef {\n") - file.write(" uint8_t row;\n") - file.write(" uint8_t col;\n") - file.write("};\n\n") - file.write("// Number of LEDs in each virtual cell VM[row][col].\n") - file.write("const uint8_t PROGMEM VM_LED_COUNT[VM_ROWS][VM_COLS] = {\n") - for row in range(vm_rows): - counts = [len(cells.get((row, col), [])) for col in range(vm_cols)] - suffix = "," if row < vm_rows - 1 else "" - file.write(f" {format_int_row(counts)}{suffix}\n") - file.write("};\n\n") + if generate_grid: + file.write("struct GridCellRef {\n") + file.write(" uint8_t row;\n") + file.write(" uint8_t col;\n") + file.write("};\n\n") - file.write("// LED mappings for all virtual cells. Empty slots use {0xFF, 0xFFFF}.\n") - file.write("const LEDMap PROGMEM VM_MAPPINGS[VM_ROWS][VM_COLS][3] = {\n") - for row in range(vm_rows): - file.write(" {\n") - for col in range(vm_cols): - cell_points = cells.get((row, col), []) - entries = [] - for slot in range(MAX_LEDS_PER_CELL): - if slot < len(cell_points): - point = cell_points[slot] - entries.append(f"{{{point.strip}, {point.led_id}}}") - else: - entries.append(f"{{{EMPTY_STRIP}, {EMPTY_LED}}}") - suffix = "," if col < vm_cols - 1 else "" - file.write(f" {{ {', '.join(entries)} }}{suffix}\n") - suffix = "," if row < vm_rows - 1 else "" - file.write(f" }}{suffix}\n") - file.write("};\n\n") + file.write("// Number of LEDs in each virtual cell VM[row][col].\n") + file.write("const uint8_t PROGMEM VM_LED_COUNT[VM_ROWS][VM_COLS] = {\n") + for row in range(vm_rows): + counts = [len(cells.get((row, col), [])) for col in range(vm_cols)] + suffix = "," if row < vm_rows - 1 else "" + file.write(f" {format_int_row(counts)}{suffix}\n") + file.write("};\n\n") - file.write("// Original 1-based grid cell for each source point id.\n") - file.write("const GridCellRef PROGMEM LED_GRID_CELL_1BASED[SOURCE_POINT_COUNT] = {\n") - for index, point in enumerate(all_points): - suffix = "," if index < len(all_points) - 1 else "" - file.write(f" {{{point.row}, {point.col}}}{suffix} // id {point.generated_id}\n") - file.write("};\n\n") + file.write("// LED mappings for all virtual cells. Empty slots use {0xFF, 0xFFFF}.\n") + file.write("const LEDMap PROGMEM VM_MAPPINGS[VM_ROWS][VM_COLS][3] = {\n") + for row in range(vm_rows): + file.write(" {\n") + for col in range(vm_cols): + cell_points = cells.get((row, col), []) + entries = [] + for slot in range(MAX_LEDS_PER_CELL): + if slot < len(cell_points): + point = cell_points[slot] + entries.append(f"{{{point.strip}, {point.led_id}}}") + else: + entries.append(f"{{{EMPTY_STRIP}, {EMPTY_LED}}}") + suffix = "," if col < vm_cols - 1 else "" + file.write(f" {{ {', '.join(entries)} }}{suffix}\n") + suffix = "," if row < vm_rows - 1 else "" + file.write(f" }}{suffix}\n") + file.write("};\n\n") - file.write("// Zero-based virtual matrix cell for each source point id.\n") - file.write("const GridCellRef PROGMEM LED_GRID_CELL_ZERO_BASED[SOURCE_POINT_COUNT] = {\n") - for index, point in enumerate(all_points): - suffix = "," if index < len(all_points) - 1 else "" - file.write(f" {{{point.matrix_row}, {point.matrix_col}}}{suffix} // id {point.generated_id}\n") - file.write("};\n\n") + file.write("// Original 1-based grid cell for each source point id.\n") + file.write("const GridCellRef PROGMEM LED_GRID_CELL_1BASED[SOURCE_POINT_COUNT] = {\n") + for index, point in enumerate(all_points): + suffix = "," if index < len(all_points) - 1 else "" + file.write(f" {{{point.row}, {point.col}}}{suffix} // id {point.generated_id}\n") + file.write("};\n\n") - file.write("// Source ids from the input JSON, indexed by source point id.\n") - file.write("const uint16_t PROGMEM LED_SOURCE_ID[SOURCE_POINT_COUNT] = {\n") - for start in range(0, len(all_points), 16): - values = [point.source_id for point in all_points[start : start + 16]] - suffix = "," if start + 16 < len(all_points) else "" - file.write(f" {', '.join(str(value) for value in values)}{suffix}\n") - file.write("};\n\n") + file.write("// Zero-based virtual matrix cell for each source point id.\n") + file.write("const GridCellRef PROGMEM LED_GRID_CELL_ZERO_BASED[SOURCE_POINT_COUNT] = {\n") + for index, point in enumerate(all_points): + suffix = "," if index < len(all_points) - 1 else "" + file.write(f" {{{point.matrix_row}, {point.matrix_col}}}{suffix} // id {point.generated_id}\n") + file.write("};\n\n") + + file.write("// Source ids from the input JSON, indexed by source point id.\n") + file.write("const uint16_t PROGMEM LED_SOURCE_ID[SOURCE_POINT_COUNT] = {\n") + for start in range(0, len(all_points), 16): + values = [point.source_id for point in all_points[start : start + 16]] + suffix = "," if start + 16 < len(all_points) else "" + file.write(f" {', '.join(str(value) for value in values)}{suffix}\n") + file.write("};\n\n") + + if generate_blocks: + file.write(f"#define TOTAL_BLOCK_LEDS {len(flat_block_leds)}\n\n") + file.write("struct BlockRange {\n") + file.write(" uint16_t offset;\n") + file.write(" uint16_t count;\n") + file.write("};\n\n") + + file.write("// Flat list of all LEDs in blocks, ordered by block, and within block\n") + file.write("const LEDMap PROGMEM BLOCK_LEDS[TOTAL_BLOCK_LEDS] = {\n") + for index, point in enumerate(flat_block_leds): + suffix = "," if index < len(flat_block_leds) - 1 else "" + file.write(f" {{{point.strip}, {point.led_id}}}{suffix} // block point\n") + file.write("};\n\n") + + file.write("// Start offset and length for each block\n") + file.write("const BlockRange PROGMEM BLOCK_RANGES[LED_BLOCK_COUNT] = {\n") + for index, (offset, count, name) in enumerate(block_ranges): + suffix = "," if index < len(block_ranges) - 1 else "" + file.write(f" {{{offset}, {count}}}{suffix} // {name}\n") + file.write("};\n\n") + + # Inline helpers for blocks + file.write("// Control block LEDs by count inside a block\n") + file.write("inline void setBlockLEDsCount(uint16_t blockIdx, uint16_t count, uint32_t color) {\n") + file.write(" if (blockIdx >= LED_BLOCK_COUNT) return;\n") + file.write(" uint16_t offset = pgm_read_word(&BLOCK_RANGES[blockIdx].offset);\n") + file.write(" uint16_t total_count = pgm_read_word(&BLOCK_RANGES[blockIdx].count);\n") + file.write(" if (count > total_count) count = total_count;\n") + file.write(f" extern Adafruit_NeoPixel hwStrips[VM_NUM_STRIPS];\n") + file.write(" for (uint16_t i = 0; i < count; i++) {\n") + file.write(" uint8_t strip = pgm_read_byte(&BLOCK_LEDS[offset + i].strip);\n") + file.write(" uint16_t led_id = pgm_read_word(&BLOCK_LEDS[offset + i].led_id);\n") + file.write(" if (strip < VM_NUM_STRIPS) {\n") + file.write(" hwStrips[strip].setPixelColor(led_id, color);\n") + file.write(" }\n") + file.write(" }\n") + file.write("}\n\n") + + file.write("// Control single block LED by 0-based index inside a block\n") + file.write("inline void setBlockLED(uint16_t blockIdx, uint16_t ledIdxInBlock, uint32_t color) {\n") + file.write(" if (blockIdx >= LED_BLOCK_COUNT) return;\n") + file.write(" uint16_t offset = pgm_read_word(&BLOCK_RANGES[blockIdx].offset);\n") + file.write(" uint16_t count = pgm_read_word(&BLOCK_RANGES[blockIdx].count);\n") + file.write(" if (ledIdxInBlock >= count) return;\n") + file.write(" uint8_t strip = pgm_read_byte(&BLOCK_LEDS[offset + ledIdxInBlock].strip);\n") + file.write(" uint16_t led_id = pgm_read_word(&BLOCK_LEDS[offset + ledIdxInBlock].led_id);\n") + file.write(f" extern Adafruit_NeoPixel hwStrips[VM_NUM_STRIPS];\n") + file.write(" if (strip < VM_NUM_STRIPS) {\n") + file.write(" hwStrips[strip].setPixelColor(led_id, color);\n") + file.write(" }\n") + file.write("}\n") def write_legacy_ino( path: Path, points: list[PanelLedPoint], cells: dict[tuple[int, int], list[PanelLedPoint]], + document: MappingDocument, + generate_grid: bool = True, + generate_blocks: bool = False, ) -> None: vm_rows, vm_cols, _grid_rows, _grid_cols = get_dimensions(points) + + # Collect blocks info + all_blocks = [] + for line in document.lines: + all_blocks.extend(line.blocks) + block_count = len(all_blocks) + + point_by_input_index = {p.input_index: p for p in points} + flat_block_leds = [] + block_ranges = [] + for block in all_blocks: + block_points = [] + for point_uid in block.point_ids: + p = point_by_input_index.get(point_uid) + if p is not None and p.strip is not None and p.led_id is not None: + block_points.append(p) + + offset = len(flat_block_leds) + count = len(block_points) + flat_block_leds.extend(block_points) + block_ranges.append((offset, count, block.name)) + with path.open("w", encoding="utf-8", newline="\n") as file: file.write("// ============================================================================\n") file.write("// ARCHIVE COPY - VIRTUAL MATRIX GENERATED BY LED Mapper GUI\n") @@ -293,93 +419,147 @@ def write_legacy_ino( file.write(f"#define NUM_STRIPS {NUM_STRIPS}\n") file.write("#endif\n") file.write("extern Adafruit_NeoPixel hwStrips[NUM_STRIPS];\n\n") - file.write("#ifndef VM_MAX_PHYS_LEDS\n") - file.write(f"#define VM_MAX_PHYS_LEDS {max(point.led_id for point in points if point.led_id is not None) + 1}\n") - file.write("#endif\n") - file.write("#ifndef VM_MAX_IDS_PER_CELL\n") - file.write(f"#define VM_MAX_IDS_PER_CELL {MAX_LEDS_PER_CELL}\n") - file.write("#endif\n") - file.write("#ifndef MATRIX_ROWS\n") - file.write(f"#define MATRIX_ROWS {vm_rows}\n") - file.write("#endif\n") - file.write("#ifndef MATRIX_COLS\n") - file.write(f"#define MATRIX_COLS {vm_cols}\n") - file.write("#endif\n\n") - file.write("struct VMPhys { uint16_t idx; uint8_t strip; };\n\n") - file.write("static uint8_t vmCount[MATRIX_ROWS][MATRIX_COLS];\n") - file.write("static VMPhys vmCell[MATRIX_ROWS][MATRIX_COLS][VM_MAX_IDS_PER_CELL];\n") - file.write("static int16_t vmRow[NUM_STRIPS][VM_MAX_PHYS_LEDS];\n") - file.write("static int16_t vmCol[NUM_STRIPS][VM_MAX_PHYS_LEDS];\n") - file.write("static uint8_t vmUsed[NUM_STRIPS][VM_MAX_PHYS_LEDS];\n") - file.write("static uint16_t vmMaxId[NUM_STRIPS];\n\n") - file.write("void clearVirtualMatrix();\n") - file.write("void setLEDs(uint16_t row, uint16_t col, uint8_t strip1based, std::initializer_list ids);\n") - file.write("void vmSetByPhys(uint8_t strip, uint16_t led, uint32_t color);\n") + + if generate_grid: + file.write("#ifndef VM_MAX_PHYS_LEDS\n") + file.write(f"#define VM_MAX_PHYS_LEDS {max((point.led_id for point in points if point.led_id is not None), default=0) + 1}\n") + file.write("#endif\n") + file.write("#ifndef VM_MAX_IDS_PER_CELL\n") + file.write(f"#define VM_MAX_IDS_PER_CELL {MAX_LEDS_PER_CELL}\n") + file.write("#endif\n") + file.write("#ifndef MATRIX_ROWS\n") + file.write(f"#define MATRIX_ROWS {vm_rows}\n") + file.write("#endif\n") + file.write("#ifndef MATRIX_COLS\n") + file.write(f"#define MATRIX_COLS {vm_cols}\n") + file.write("#endif\n\n") + file.write("struct VMPhys { uint16_t idx; uint8_t strip; };\n\n") + file.write("static uint8_t vmCount[MATRIX_ROWS][MATRIX_COLS];\n") + file.write("static VMPhys vmCell[MATRIX_ROWS][MATRIX_COLS][VM_MAX_IDS_PER_CELL];\n") + file.write("static int16_t vmRow[NUM_STRIPS][VM_MAX_PHYS_LEDS];\n") + file.write("static int16_t vmCol[NUM_STRIPS][VM_MAX_PHYS_LEDS];\n") + file.write("static uint8_t vmUsed[NUM_STRIPS][VM_MAX_PHYS_LEDS];\n") + file.write("static uint16_t vmMaxId[NUM_STRIPS];\n\n") + file.write("void clearVirtualMatrix();\n") + file.write("void setLEDs(uint16_t row, uint16_t col, uint8_t strip1based, std::initializer_list ids);\n") + file.write("void vmSetByPhys(uint8_t strip, uint16_t led, uint32_t color);\n") + file.write("void initVirtualMatrix();\n\n") - file.write("void clearVirtualMatrix() {\n") - file.write(" for (int r = 0; r < MATRIX_ROWS; ++r)\n") - file.write(" for (int c = 0; c < MATRIX_COLS; ++c) vmCount[r][c] = 0;\n") - file.write(" for (int s = 0; s < NUM_STRIPS; ++s) {\n") - file.write(" vmMaxId[s] = 0;\n") - file.write(" for (int i = 0; i < VM_MAX_PHYS_LEDS; ++i) {\n") - file.write(" vmRow[s][i] = -1;\n") - file.write(" vmCol[s][i] = -1;\n") - file.write(" vmUsed[s][i] = 0;\n") - file.write(" }\n") - file.write(" }\n") - file.write("}\n\n") - file.write("void setLEDs(uint16_t row, uint16_t col, uint8_t strip1based, std::initializer_list ids) {\n") - file.write(" if (row >= MATRIX_ROWS || col >= MATRIX_COLS) return;\n") - file.write(" uint8_t strip = (strip1based == 0) ? 0 : (uint8_t)(strip1based - 1);\n") - file.write(" if (strip >= NUM_STRIPS) return;\n") - file.write(" for (uint16_t id : ids) {\n") - file.write(" if (id >= VM_MAX_PHYS_LEDS) continue;\n") - file.write(" uint8_t &count = vmCount[row][col];\n") - file.write(" if (count < VM_MAX_IDS_PER_CELL) {\n") - file.write(" vmCell[row][col][count].idx = id;\n") - file.write(" vmCell[row][col][count].strip = strip;\n") - file.write(" ++count;\n") - file.write(" }\n") - file.write(" vmRow[strip][id] = (int16_t)row;\n") - file.write(" vmCol[strip][id] = (int16_t)col;\n") - file.write(" vmUsed[strip][id] = 1;\n") - file.write(" if (id > vmMaxId[strip]) vmMaxId[strip] = id;\n") - file.write(" }\n") - file.write("}\n\n") - file.write("void vmSetByPhys(uint8_t strip, uint16_t led, uint32_t color) {\n") - file.write(" if (strip >= NUM_STRIPS || led >= VM_MAX_PHYS_LEDS) return;\n") - file.write(" int16_t row = vmRow[strip][led];\n") - file.write(" int16_t col = vmCol[strip][led];\n") - file.write(" if (row < 0 || col < 0) {\n") - file.write(" if (led < hwStrips[strip].numPixels()) hwStrips[strip].setPixelColor(led, color);\n") - file.write(" return;\n") - file.write(" }\n") - file.write(" uint8_t count = vmCount[row][col];\n") - file.write(" for (uint8_t i = 0; i < count; ++i) {\n") - file.write(" const VMPhys &phys = vmCell[row][col][i];\n") - file.write(" if (phys.strip < NUM_STRIPS && phys.idx < hwStrips[phys.strip].numPixels()) {\n") - file.write(" hwStrips[phys.strip].setPixelColor(phys.idx, color);\n") - file.write(" }\n") - file.write(" }\n") - file.write("}\n\n") + + if generate_grid: + file.write("void clearVirtualMatrix() {\n") + file.write(" for (int r = 0; r < MATRIX_ROWS; ++r)\n") + file.write(" for (int c = 0; c < MATRIX_COLS; ++c) vmCount[r][c] = 0;\n") + file.write(" for (int s = 0; s < NUM_STRIPS; ++s) {\n") + file.write(" vmMaxId[s] = 0;\n") + file.write(" for (int i = 0; i < VM_MAX_PHYS_LEDS; ++i) {\n") + file.write(" vmRow[s][i] = -1;\n") + file.write(" vmCol[s][i] = -1;\n") + file.write(" vmUsed[s][i] = 0;\n") + file.write(" }\n") + file.write(" }\n") + file.write("}\n\n") + file.write("void setLEDs(uint16_t row, uint16_t col, uint8_t strip1based, std::initializer_list ids) {\n") + file.write(" if (row >= MATRIX_ROWS || col >= MATRIX_COLS) return;\n") + file.write(" uint8_t strip = (strip1based == 0) ? 0 : (uint8_t)(strip1based - 1);\n") + file.write(" if (strip >= NUM_STRIPS) return;\n") + file.write(" for (uint16_t id : ids) {\n") + file.write(" if (id >= VM_MAX_PHYS_LEDS) continue;\n") + file.write(" uint8_t &count = vmCount[row][col];\n") + file.write(" if (count < VM_MAX_IDS_PER_CELL) {\n") + file.write(" vmCell[row][col][count].idx = id;\n") + file.write(" vmCell[row][col][count].strip = strip;\n") + file.write(" ++count;\n") + file.write(" }\n") + file.write(" vmRow[strip][id] = (int16_t)row;\n") + file.write(" vmCol[strip][id] = (int16_t)col;\n") + file.write(" vmUsed[strip][id] = 1;\n") + file.write(" if (id > vmMaxId[strip]) vmMaxId[strip] = id;\n") + file.write(" }\n") + file.write("}\n\n") + file.write("void vmSetByPhys(uint8_t strip, uint16_t led, uint32_t color) {\n") + file.write(" if (strip >= NUM_STRIPS || led >= VM_MAX_PHYS_LEDS) return;\n") + file.write(" int16_t row = vmRow[strip][led];\n") + file.write(" int16_t col = vmCol[strip][led];\n") + file.write(" if (row < 0 || col < 0) {\n") + file.write(" if (led < hwStrips[strip].numPixels()) hwStrips[strip].setPixelColor(led, color);\n") + file.write(" return;\n") + file.write(" }\n") + file.write(" uint8_t count = vmCount[row][col];\n") + file.write(" for (uint8_t i = 0; i < count; ++i) {\n") + file.write(" const VMPhys &phys = vmCell[row][col][i];\n") + file.write(" if (phys.strip < NUM_STRIPS && phys.idx < hwStrips[phys.strip].numPixels()) {\n") + file.write(" hwStrips[phys.strip].setPixelColor(phys.idx, color);\n") + file.write(" }\n") + file.write(" }\n") + file.write("}\n\n") + + if generate_blocks: + file.write("#ifndef LED_BLOCK_COUNT\n") + file.write(f"#define LED_BLOCK_COUNT {block_count}\n") + file.write("#endif\n") + file.write("#ifndef TOTAL_BLOCK_LEDS\n") + file.write(f"#define TOTAL_BLOCK_LEDS {len(flat_block_leds)}\n") + file.write("#endif\n\n") + if not generate_grid: + file.write("struct VMPhys { uint16_t idx; uint8_t strip; };\n\n") + file.write("struct BlockRange { uint16_t offset; uint16_t count; };\n\n") + file.write("static const VMPhys PROGMEM BLOCK_LEDS[TOTAL_BLOCK_LEDS] = {\n") + for index, point in enumerate(flat_block_leds): + suffix = "," if index < len(flat_block_leds) - 1 else "" + file.write(f" {{{point.led_id}, {point.strip}}}{suffix} // block point\n") + file.write("};\n\n") + file.write("static const BlockRange PROGMEM BLOCK_RANGES[LED_BLOCK_COUNT] = {\n") + for index, (offset, count, name) in enumerate(block_ranges): + suffix = "," if index < len(block_ranges) - 1 else "" + file.write(f" {{{offset}, {count}}}{suffix} // {name}\n") + file.write("};\n\n") + file.write("void setBlockLEDsCount(uint16_t blockIdx, uint16_t count, uint32_t color);\n") + file.write("void setBlockLED(uint16_t blockIdx, uint16_t ledIdxInBlock, uint32_t color);\n\n") + file.write("void setBlockLEDsCount(uint16_t blockIdx, uint16_t count, uint32_t color) {\n") + file.write(" if (blockIdx >= LED_BLOCK_COUNT) return;\n") + file.write(" uint16_t offset = pgm_read_word(&BLOCK_RANGES[blockIdx].offset);\n") + file.write(" uint16_t total_count = pgm_read_word(&BLOCK_RANGES[blockIdx].count);\n") + file.write(" if (count > total_count) count = total_count;\n") + file.write(" for (uint16_t i = 0; i < count; i++) {\n") + file.write(" uint8_t strip = pgm_read_byte(&BLOCK_LEDS[offset + i].strip);\n") + file.write(" uint16_t led_id = pgm_read_word(&BLOCK_LEDS[offset + i].idx);\n") + file.write(" if (strip < NUM_STRIPS) {\n") + file.write(" hwStrips[strip].setPixelColor(led_id, color);\n") + file.write(" }\n") + file.write(" }\n") + file.write("}\n\n") + file.write("void setBlockLED(uint16_t blockIdx, uint16_t ledIdxInBlock, uint32_t color) {\n") + file.write(" if (blockIdx >= LED_BLOCK_COUNT) return;\n") + file.write(" uint16_t offset = pgm_read_word(&BLOCK_RANGES[blockIdx].offset);\n") + file.write(" uint16_t count = pgm_read_word(&BLOCK_RANGES[blockIdx].count);\n") + file.write(" if (ledIdxInBlock >= count) return;\n") + file.write(" uint8_t strip = pgm_read_byte(&BLOCK_LEDS[offset + ledIdxInBlock].strip);\n") + file.write(" uint16_t led_id = pgm_read_word(&BLOCK_LEDS[offset + ledIdxInBlock].idx);\n") + file.write(" if (strip < NUM_STRIPS) {\n") + file.write(" hwStrips[strip].setPixelColor(led_id, color);\n") + file.write(" }\n") + file.write("}\n\n") + file.write("void initVirtualMatrix() {\n") - file.write(" clearVirtualMatrix();\n") - last_row = None - for row in range(vm_rows): - row_cells = [(col, cells[(row, col)]) for col in range(vm_cols) if (row, col) in cells] - if not row_cells: - continue - if last_row != row: - file.write(f"\n // Row {row}\n") - last_row = row - for col, cell_points in row_cells: - by_strip: dict[int, list[int]] = defaultdict(list) - for point in cell_points: - if point.strip is not None and point.led_id is not None: - by_strip[point.strip + 1].append(point.led_id) - for strip1based, led_ids_list in sorted(by_strip.items()): - led_ids = ", ".join(str(led_id) for led_id in led_ids_list) - file.write(f" setLEDs({row}, {col:2d}, {strip1based}, {{ {led_ids} }});\n") + if generate_grid: + file.write(" clearVirtualMatrix();\n") + last_row = None + for row in range(vm_rows): + row_cells = [(col, cells[(row, col)]) for col in range(vm_cols) if (row, col) in cells] + if not row_cells: + continue + if last_row != row: + file.write(f"\n // Row {row}\n") + last_row = row + for col, cell_points in row_cells: + by_strip: dict[int, list[int]] = defaultdict(list) + for point in cell_points: + if point.strip is not None and point.led_id is not None: + by_strip[point.strip + 1].append(point.led_id) + for strip1based, led_ids_list in sorted(by_strip.items()): + led_ids = ", ".join(str(led_id) for led_id in led_ids_list) + file.write(f" setLEDs({row}, {col:2d}, {strip1based}, {{ {led_ids} }});\n") file.write("}\n") diff --git a/qa_smoke.py b/qa_smoke.py index 066aa1e..87c792d 100644 --- a/qa_smoke.py +++ b/qa_smoke.py @@ -118,6 +118,7 @@ def main() -> None: test_saved_coordinate_unit_is_reused, test_uid_point_id_mode_does_not_fallback_to_source_ids, test_panel_mapping_generation_from_document, + test_panel_mapping_generation_with_strategies, test_save_preserves_unknown_source_fields, test_ordered_json_preserves_document_and_remaps_uids, test_background_load_save_roundtrip, @@ -1020,6 +1021,49 @@ def test_panel_mapping_generation_from_document() -> None: assert ordered_payload["circles"][0]["strip"] == 0 +def test_panel_mapping_generation_with_strategies() -> None: + with temp_document_path() as path, tempfile.TemporaryDirectory() as temp_dir: + doc = MappingDocument() + doc.load(path) + line = doc.add_line() + # Add a block with two points + doc.add_block(line.id, [0, 1]) + + # 1. Test Grid=False, Blocks=True + result = generate_panel_mapping_from_document( + doc, Path(temp_dir), generate_grid=False, generate_blocks=True + ) + vm_map = result.generated_files["vm_map"].read_text(encoding="utf-8") + assert "BLOCK_LEDS" in vm_map + assert "BLOCK_RANGES" in vm_map + assert "setBlockLED" in vm_map + assert "VM_ROWS" not in vm_map + assert "VM_LED_COUNT" not in vm_map + + legacy_ino = result.generated_files["legacy_ino"].read_text(encoding="utf-8") + assert "BLOCK_LEDS" in legacy_ino + assert "setBlockLED" in legacy_ino + assert "MATRIX_ROWS" not in legacy_ino + assert "vmCount" not in legacy_ino + + # 2. Test Grid=True, Blocks=True + result = generate_panel_mapping_from_document( + doc, Path(temp_dir), generate_grid=True, generate_blocks=True + ) + vm_map = result.generated_files["vm_map"].read_text(encoding="utf-8") + assert "BLOCK_LEDS" in vm_map + assert "BLOCK_RANGES" in vm_map + assert "setBlockLED" in vm_map + assert "VM_ROWS" in vm_map + assert "VM_LED_COUNT" in vm_map + + legacy_ino = result.generated_files["legacy_ino"].read_text(encoding="utf-8") + assert "BLOCK_LEDS" in legacy_ino + assert "setBlockLED" in legacy_ino + assert "MATRIX_ROWS" in legacy_ino + assert "vmCount" in legacy_ino + + def test_save_preserves_unknown_source_fields() -> None: with tempfile.TemporaryDirectory() as temp_dir: source = Path(temp_dir) / "custom.json"