Add block renaming in GUI and support block name-based addressing in generated scripts
- Реализовано переименование блоков по двойному клику и в контекстном меню дерева. - Добавлена функция транслитерации и санитарной очистки имён блоков для C++ идентификаторов. - Реализован массив строк имён блоков в flash-памяти (PROGMEM) и метод поиска getBlockIndexByName. - Добавлены перегрузки функций управления светодиодами, принимающие оригинальные строковые названия блоков. - Добавлены интеграционные тесты для проверки санитации имён, генерации макросов и функций поиска.
This commit is contained in:
parent
228e4e4f0b
commit
8f814bc1fc
@ -15,6 +15,7 @@ from PySide6.QtWidgets import (
|
||||
QDoubleSpinBox,
|
||||
QFileDialog,
|
||||
QFormLayout,
|
||||
QInputDialog,
|
||||
QHBoxLayout,
|
||||
QHeaderView,
|
||||
QLabel,
|
||||
@ -242,6 +243,7 @@ class MainWindow(QMainWindow):
|
||||
self.tree.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||
self.tree.itemSelectionChanged.connect(self._on_tree_selection)
|
||||
self.tree.customContextMenuRequested.connect(self._show_tree_menu)
|
||||
self.tree.itemDoubleClicked.connect(self._on_tree_item_double_clicked)
|
||||
|
||||
self.table = QTableWidget(0, 8)
|
||||
self.table.setHorizontalHeaderLabels(
|
||||
@ -1681,12 +1683,17 @@ class MainWindow(QMainWindow):
|
||||
menu = QMenu(self)
|
||||
merge_action = menu.addAction("Объединить блоки")
|
||||
merge_action.setEnabled(self._can_merge_selected_tree_blocks())
|
||||
label = "Удалить выбранную фиксацию"
|
||||
rename_block_action = None
|
||||
data = None
|
||||
if item is not None:
|
||||
data = item.data(0, Qt.UserRole)
|
||||
if data and data[0] == "line":
|
||||
if data and data[0] == "block":
|
||||
rename_block_action = menu.addAction("Переименовать блок")
|
||||
label = "Удалить выбранную фиксацию"
|
||||
if item is not None and data is not None:
|
||||
if data[0] == "line":
|
||||
label = "Удалить линию"
|
||||
elif data and data[0] == "block":
|
||||
elif data[0] == "block":
|
||||
label = "Удалить блок"
|
||||
delete_action = menu.addAction(label)
|
||||
delete_action.setEnabled(item is not None)
|
||||
@ -1697,6 +1704,9 @@ class MainWindow(QMainWindow):
|
||||
chosen = menu.exec(self.tree.viewport().mapToGlobal(pos))
|
||||
if chosen == merge_action:
|
||||
self.merge_selected_blocks()
|
||||
elif rename_block_action and chosen == rename_block_action:
|
||||
line_id, block_id = data[1], data[2]
|
||||
self.rename_block(line_id, block_id)
|
||||
elif chosen == delete_action:
|
||||
self.remove_selected_tree_item()
|
||||
elif chosen == clear_all_action:
|
||||
@ -1704,6 +1714,30 @@ class MainWindow(QMainWindow):
|
||||
elif chosen == fit_action:
|
||||
self.canvas.fit_to_content()
|
||||
|
||||
def _on_tree_item_double_clicked(self, item: QTreeWidgetItem, column: int) -> None:
|
||||
data = item.data(0, Qt.UserRole)
|
||||
if data and data[0] == "block":
|
||||
line_id, block_id = data[1], data[2]
|
||||
self.rename_block(line_id, block_id)
|
||||
|
||||
def rename_block(self, line_id: int, block_id: int) -> None:
|
||||
block = self._find_block_or_none(line_id, block_id)
|
||||
if block is None:
|
||||
return
|
||||
new_name, ok = QInputDialog.getText(
|
||||
self,
|
||||
"Переименовать блок",
|
||||
"Введите новое имя блока:",
|
||||
QLineEdit.EchoMode.Normal,
|
||||
block.name
|
||||
)
|
||||
if ok and new_name.strip() and new_name.strip() != block.name:
|
||||
self._push_undo()
|
||||
block.name = new_name.strip()
|
||||
self._dirty = True
|
||||
self.status.setText(f"Блок переименован в '{block.name}'.")
|
||||
self._refresh_ui_state()
|
||||
|
||||
def merge_selected_blocks(self) -> None:
|
||||
selected_blocks = self._selected_tree_blocks()
|
||||
if len(selected_blocks) < 2:
|
||||
|
||||
@ -69,6 +69,51 @@ class BuildResult:
|
||||
unassigned_count: int
|
||||
|
||||
|
||||
def sanitize_cpp_identifier(name: str) -> str:
|
||||
# Transliteration dictionary for Russian Cyrillic
|
||||
cyrillic_translit = {
|
||||
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo',
|
||||
'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm',
|
||||
'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u',
|
||||
'ф': 'f', 'х': 'kh', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'shch',
|
||||
'ы': 'y', 'э': 'e', 'ю': 'yu', 'я': 'ya',
|
||||
'А': 'A', 'Б': 'B', 'В': 'V', 'Г': 'G', 'Д': 'D', 'Е': 'E', 'Ё': 'Yo',
|
||||
'Ж': 'Zh', 'З': 'Z', 'И': 'I', 'Й': 'Y', 'К': 'K', 'Л': 'L', 'М': 'M',
|
||||
'Н': 'N', 'О': 'O', 'П': 'P', 'Р': 'R', 'С': 'S', 'Т': 'T', 'У': 'U',
|
||||
'Ф': 'F', 'Х': 'Kh', 'Ц': 'Ts', 'Ч': 'Ch', 'Ш': 'Sh', 'Щ': 'Shch',
|
||||
'Ы': 'Y', 'Э': 'E', 'Ю': 'Yu', 'Я': 'Ya',
|
||||
'ь': '', 'Ь': '', 'ъ': '', 'Ъ': ''
|
||||
}
|
||||
|
||||
# Transliterate Cyrillic to Latin
|
||||
transliterated = "".join(cyrillic_translit.get(c, c) for c in name)
|
||||
|
||||
# Replace non-alphanumeric chars with underscore
|
||||
sanitized = ""
|
||||
for c in transliterated:
|
||||
if c.isalnum() or c == '_':
|
||||
sanitized += c
|
||||
else:
|
||||
sanitized += '_'
|
||||
|
||||
# Remove multiple consecutive underscores
|
||||
while "__" in sanitized:
|
||||
sanitized = sanitized.replace("__", "_")
|
||||
|
||||
# Strip leading/trailing underscores
|
||||
sanitized = sanitized.strip("_")
|
||||
|
||||
# Prepend underscore if starts with a digit
|
||||
if sanitized and sanitized[0].isdigit():
|
||||
sanitized = "_" + sanitized
|
||||
|
||||
# If empty, return a default prefix
|
||||
if not sanitized:
|
||||
sanitized = "BLOCK"
|
||||
|
||||
return sanitized
|
||||
|
||||
|
||||
def generate_panel_mapping_from_document(
|
||||
document: MappingDocument,
|
||||
output_dir: str | Path,
|
||||
@ -324,6 +369,13 @@ def write_vm_map_header(
|
||||
file.write("};\n\n")
|
||||
|
||||
if generate_blocks:
|
||||
# Generate compile-time block macros/constants
|
||||
file.write("// Compile-time block indices\n")
|
||||
for index, (offset, count, name) in enumerate(block_ranges):
|
||||
identifier = sanitize_cpp_identifier(name)
|
||||
file.write(f"#define BLOCK_{identifier} {index}\n")
|
||||
file.write("\n")
|
||||
|
||||
file.write(f"#define TOTAL_BLOCK_LEDS {len(flat_block_leds)}\n\n")
|
||||
file.write("struct BlockRange {\n")
|
||||
file.write(" uint16_t offset;\n")
|
||||
@ -344,6 +396,19 @@ def write_vm_map_header(
|
||||
file.write(f" {{{offset}, {count}}}{suffix} // {name}\n")
|
||||
file.write("};\n\n")
|
||||
|
||||
# String names array in PROGMEM
|
||||
file.write("// Original string names of blocks in flash memory\n")
|
||||
for index, (offset, count, name) in enumerate(block_ranges):
|
||||
escaped_name = name.replace('"', '\\"')
|
||||
file.write(f"const char BLOCK_NAME_{index}[] PROGMEM = \"{escaped_name}\";\n")
|
||||
file.write("\n")
|
||||
|
||||
file.write("const char* const BLOCK_NAMES[LED_BLOCK_COUNT] PROGMEM = {\n")
|
||||
for index in range(len(block_ranges)):
|
||||
suffix = "," if index < len(block_ranges) - 1 else ""
|
||||
file.write(f" BLOCK_NAME_{index}{suffix}\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")
|
||||
@ -373,6 +438,35 @@ def write_vm_map_header(
|
||||
file.write(" if (strip < VM_NUM_STRIPS) {\n")
|
||||
file.write(" hwStrips[strip].setPixelColor(led_id, color);\n")
|
||||
file.write(" }\n")
|
||||
file.write("}\n\n")
|
||||
|
||||
# Add getBlockIndexByName
|
||||
file.write("// Find block index by its string name (case-sensitive, original name)\n")
|
||||
file.write("inline int16_t getBlockIndexByName(const char* name) {\n")
|
||||
file.write(" for (uint16_t i = 0; i < LED_BLOCK_COUNT; i++) {\n")
|
||||
file.write(" const char* blockNamePtr = (const char*)pgm_read_word(&BLOCK_NAMES[i]);\n")
|
||||
file.write(" if (strcmp_P(name, blockNamePtr) == 0) {\n")
|
||||
file.write(" return (int16_t)i;\n")
|
||||
file.write(" }\n")
|
||||
file.write(" }\n")
|
||||
file.write(" return -1;\n")
|
||||
file.write("}\n\n")
|
||||
|
||||
# Add string-based control overloads
|
||||
file.write("// Control block LEDs by string name and count\n")
|
||||
file.write("inline void setBlockLEDsCount(const char* name, uint16_t count, uint32_t color) {\n")
|
||||
file.write(" int16_t idx = getBlockIndexByName(name);\n")
|
||||
file.write(" if (idx >= 0) {\n")
|
||||
file.write(" setBlockLEDsCount((uint16_t)idx, count, color);\n")
|
||||
file.write(" }\n")
|
||||
file.write("}\n\n")
|
||||
|
||||
file.write("// Control single block LED by block string name and 0-based index\n")
|
||||
file.write("inline void setBlockLED(const char* name, uint16_t ledIdxInBlock, uint32_t color) {\n")
|
||||
file.write(" int16_t idx = getBlockIndexByName(name);\n")
|
||||
file.write(" if (idx >= 0) {\n")
|
||||
file.write(" setBlockLED((uint16_t)idx, ledIdxInBlock, color);\n")
|
||||
file.write(" }\n")
|
||||
file.write("}\n")
|
||||
|
||||
|
||||
@ -495,6 +589,12 @@ def write_legacy_ino(
|
||||
file.write("}\n\n")
|
||||
|
||||
if generate_blocks:
|
||||
file.write("// Compile-time block indices\n")
|
||||
for index, (offset, count, name) in enumerate(block_ranges):
|
||||
identifier = sanitize_cpp_identifier(name)
|
||||
file.write(f"#define BLOCK_{identifier} {index}\n")
|
||||
file.write("\n")
|
||||
|
||||
file.write("#ifndef LED_BLOCK_COUNT\n")
|
||||
file.write(f"#define LED_BLOCK_COUNT {block_count}\n")
|
||||
file.write("#endif\n")
|
||||
@ -514,8 +614,26 @@ def write_legacy_ino(
|
||||
suffix = "," if index < len(block_ranges) - 1 else ""
|
||||
file.write(f" {{{offset}, {count}}}{suffix} // {name}\n")
|
||||
file.write("};\n\n")
|
||||
|
||||
# String names array in PROGMEM
|
||||
file.write("// Original string names of blocks in flash memory\n")
|
||||
for index, (offset, count, name) in enumerate(block_ranges):
|
||||
escaped_name = name.replace('"', '\\"')
|
||||
file.write(f"static const char BLOCK_NAME_{index}[] PROGMEM = \"{escaped_name}\";\n")
|
||||
file.write("\n")
|
||||
|
||||
file.write("static const char* const BLOCK_NAMES[LED_BLOCK_COUNT] PROGMEM = {\n")
|
||||
for index in range(len(block_ranges)):
|
||||
suffix = "," if index < len(block_ranges) - 1 else ""
|
||||
file.write(f" BLOCK_NAME_{index}{suffix}\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 setBlockLED(uint16_t blockIdx, uint16_t ledIdxInBlock, uint32_t color);\n")
|
||||
file.write("int16_t getBlockIndexByName(const char* name);\n")
|
||||
file.write("void setBlockLEDsCount(const char* name, uint16_t count, uint32_t color);\n")
|
||||
file.write("void setBlockLED(const char* name, 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")
|
||||
@ -529,6 +647,7 @@ def write_legacy_ino(
|
||||
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")
|
||||
@ -541,6 +660,30 @@ def write_legacy_ino(
|
||||
file.write(" }\n")
|
||||
file.write("}\n\n")
|
||||
|
||||
file.write("int16_t getBlockIndexByName(const char* name) {\n")
|
||||
file.write(" for (uint16_t i = 0; i < LED_BLOCK_COUNT; i++) {\n")
|
||||
file.write(" const char* blockNamePtr = (const char*)pgm_read_word(&BLOCK_NAMES[i]);\n")
|
||||
file.write(" if (strcmp_P(name, blockNamePtr) == 0) {\n")
|
||||
file.write(" return (int16_t)i;\n")
|
||||
file.write(" }\n")
|
||||
file.write(" }\n")
|
||||
file.write(" return -1;\n")
|
||||
file.write("}\n\n")
|
||||
|
||||
file.write("void setBlockLEDsCount(const char* name, uint16_t count, uint32_t color) {\n")
|
||||
file.write(" int16_t idx = getBlockIndexByName(name);\n")
|
||||
file.write(" if (idx >= 0) {\n")
|
||||
file.write(" setBlockLEDsCount((uint16_t)idx, count, color);\n")
|
||||
file.write(" }\n")
|
||||
file.write("}\n\n")
|
||||
|
||||
file.write("void setBlockLED(const char* name, uint16_t ledIdxInBlock, uint32_t color) {\n")
|
||||
file.write(" int16_t idx = getBlockIndexByName(name);\n")
|
||||
file.write(" if (idx >= 0) {\n")
|
||||
file.write(" setBlockLED((uint16_t)idx, ledIdxInBlock, color);\n")
|
||||
file.write(" }\n")
|
||||
file.write("}\n\n")
|
||||
|
||||
file.write("void initVirtualMatrix() {\n")
|
||||
if generate_grid:
|
||||
file.write(" clearVirtualMatrix();\n")
|
||||
|
||||
29
qa_smoke.py
29
qa_smoke.py
@ -119,6 +119,7 @@ def main() -> None:
|
||||
test_uid_point_id_mode_does_not_fallback_to_source_ids,
|
||||
test_panel_mapping_generation_from_document,
|
||||
test_panel_mapping_generation_with_strategies,
|
||||
test_block_renaming_and_naming_strategy,
|
||||
test_save_preserves_unknown_source_fields,
|
||||
test_ordered_json_preserves_document_and_remaps_uids,
|
||||
test_background_load_save_roundtrip,
|
||||
@ -1064,6 +1065,34 @@ def test_panel_mapping_generation_with_strategies() -> None:
|
||||
assert "vmCount" in legacy_ino
|
||||
|
||||
|
||||
def test_block_renaming_and_naming_strategy() -> None:
|
||||
from panel_mapping_generator import sanitize_cpp_identifier
|
||||
assert sanitize_cpp_identifier("Блок 1") == "Blok_1"
|
||||
assert sanitize_cpp_identifier("Left Wing") == "Left_Wing"
|
||||
assert sanitize_cpp_identifier("123-Block") == "_123_Block"
|
||||
assert sanitize_cpp_identifier("") == "BLOCK"
|
||||
|
||||
with temp_document_path() as path, tempfile.TemporaryDirectory() as temp_dir:
|
||||
doc = MappingDocument()
|
||||
doc.load(path)
|
||||
line = doc.add_line()
|
||||
block = doc.add_block(line.id, [0, 1])
|
||||
block.name = "Левое Крыло"
|
||||
|
||||
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 "#define BLOCK_Levoe_Krylo 0" in vm_map
|
||||
assert "const char BLOCK_NAME_0[] PROGMEM = \"Левое Крыло\";" in vm_map
|
||||
assert "getBlockIndexByName" in vm_map
|
||||
|
||||
legacy_ino = result.generated_files["legacy_ino"].read_text(encoding="utf-8")
|
||||
assert "#define BLOCK_Levoe_Krylo 0" in legacy_ino
|
||||
assert "static const char BLOCK_NAME_0[] PROGMEM = \"Левое Крыло\";" in legacy_ino
|
||||
assert "getBlockIndexByName" in legacy_ino
|
||||
|
||||
|
||||
def test_save_preserves_unknown_source_fields() -> None:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
source = Path(temp_dir) / "custom.json"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user