Add support for script generation strategies and block-level LED functions

- Добавлен диалог выбора стратегий генерации (Сетка и Блоки) в GUI.
- Добавлен C++ код для управления светодиодами в блоках по их ID и по количеству в vm_map.h и virtual_matrix_legacy.ino.txt.
- Обновлены функции генератора, добавлена поддержка индивидуальных параметров генерации.
- Написаны интеграционные тесты для проверки генерации стратегий и функций управления блоками.
This commit is contained in:
Данил Ярош 2026-07-31 09:42:54 +03:00
parent 2832914343
commit 228e4e4f0b
3 changed files with 433 additions and 156 deletions

View File

@ -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}")

View File

@ -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 <stdint.h>\n")
file.write("#include <Arduino.h>\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<uint16_t> 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<uint16_t> 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<uint16_t> 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<uint16_t> 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")

View File

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