@tool extends Panel ## Выполнение функций информационного ЭМС (ЭМС-ТГ) #region Определяемое при использовании ## Индекс -> Имя устройства. Нужен для предсказуемого ## порядка отображения: верх->вниз или лево->право. ## Имя устройства должно быть взято из settings.gd @export var index_to_unit_names: Array[StringName] ## Минимальная частота, МГЦ @export var freq_min: float ## Максимальная частота, МГЦ @export var freq_max: float ## Время достоверности кластера по умолчанию (если не указано в докладе) @export var default_ttl: float ## Цвет диапазона если включен (разрешён) @export_color_no_alpha var color_enabled: Color ## Цвет диапазона если выключен (запрещён) @export_color_no_alpha var color_disabled: Color ## Прозрачность диапазон на шкале (диапазоны могут накладываться, что бы было видно наложения) @export_range(0, 1.0) var color_alpha: float ## Цвет нечётных строк шкалы @export_color_no_alpha var color_odd: Color ## Цвет чётных строк шкалы @export_color_no_alpha var color_even: Color ## Индекс колонки в таблице для управления работой приёмников @export var column_cmd_rx: int ## Индекс колонки в таблице для управления работой передатчиков @export var column_cmd_tx: int ## Максимальное количество кластеров на одно устройство @export var max_clasters_per_unit: int ## Сообщение пользователю @export var msg_select_unit: String ## Сообщение пользователю @export var msg_waiting_data: String #endregion #region Загружаемые ресурсы @export_file('*.tscn') var table_headers: Array[String] @export_file('*.tscn') var table_cells: Array[String] @export_file('*.png') var image_file_rx: String @export_file('*.png') var image_file_tx: String @export var table_rows_hight: int @export var table_column_widths: Array[int] @export var table_header_widths: Array[int] @export var table_header_text: Array[String] #endregion #region Заполняется автоматически при запуске ## Имя устройства -> Индекс устройства var name_to_index: Dictionary[StringName, int] ## Имя устройства -> Шкала var name_to_scale: Dictionary[StringName, GridContainer] var table_cell_row: Array ## Образец параметров кластера var ref_cls: = {'cname': '<имя>', 'ttl': 1.0, 'daa': 360.0, 'dap': 0.0, 'cfb': (freq_min + freq_max) / 2.0, 'wfb': freq_max - freq_min} #endregion #region Заполняется во время работы var yems_info: Dictionary[StringName, Dictionary] ## Доклады var yems_cmd: Dictionary[StringName, Dictionary] ## Команды ## Порядок следования кластеров в таблице. ## {имя_устройства: {имя_кластера: индекс_в_таблице}} var cnames_table: Dictionary[StringName, Dictionary] ## Текущее выбранное оператором устройство на шкале var selected_unit: Dictionary[StringName, bool] ## Состояния кластеров в соответствии с докладами и командами #endregion ## Состояние кластера enum ClasterStatus { DELETED, ## Удалён (время ожидания обновления истекло) RUNNING ## Работает (информация обновляется) } ## Режим управления кластером enum ClasterMode { MANUAL, ## Ручной, управляется операторром AUTO, ## Автоматический, управляется программой IGNORE ## Игнорируемый, не управляется } ## Состояние кластера. Значения должны соответствовать ## состояниям [b]six-state-button[/b] enum ClasterResult { WAITING = 0, ## Ожидание даннных DONE = 1, ## Выполнено MISSMATCH = 2 ## Несоответствие } func _ready() -> void: # Инициализировать переменные color_enabled.a = color_alpha color_disabled.a = color_alpha for i in index_to_unit_names.size(): var nm: = index_to_unit_names[i] name_to_index[nm] = i yems_info[nm] = {'mcls': []} yems_cmd[nm] = {} cnames_table[nm] = {} selected_unit[nm] = false var btns_scales = get_tree().get_nodes_in_group('кнопки-шкалы') for node in btns_scales: var nm: = node.get_meta('unit_name') as StringName node.connect('toggled', _on_scale_selected.bind(nm)) var claster_scales = get_tree().get_nodes_in_group('шкалы-кластеров') for node in claster_scales: var nm: = node.get_meta('unit_name') as StringName name_to_scale[nm] = node # Расскрасить чётные-нечётные for node in get_tree().get_nodes_in_group('нечётные'): node.get_node('panel_rx').back_color = color_odd node.get_node('panel_tx').back_color = color_odd for node in get_tree().get_nodes_in_group('чётные'): node.get_node('panel_rx').back_color = color_even node.get_node('panel_tx').back_color = color_even # Подключить обработчик для приёма данных for nm in yems_info: var yems_unit = network.get_unit_instance(nm) yems_unit.connect('data_received', on_data_received.bind(nm)) # Настроить таблицу var header_row: = [] for cell_scn in table_headers: header_row.append(load(cell_scn)) for cell_scn in table_cells: table_cell_row.append(load(cell_scn)) var ctable: = $'таблица/сетка' ctable.set_header(header_row) ctable.set_header_text(table_header_text) ctable.set_header_property(column_cmd_rx, 'image_file', image_file_rx) ctable.set_header_property(column_cmd_tx, 'image_file', image_file_tx) ctable.set_headers_min_size(table_header_widths) ctable.set_columns_min_size(table_column_widths) ctable.set_rows_min_hight(table_rows_hight) ctable.set_header_min_hight(table_rows_hight) ctable.pin_header2() # Описатель для отображения импульсного ЭМС: # { идентификатор_входа: { 'node_path': путь_к_узлу, 'pname': название_устройства, 'dcls': { имя_кластера: {параметры_кластера}}}} # Здесь поля txe и rxe - отвечают за конфигурацию: # txe - вход связан с управлением передатчиком # rxe - вход связан с управлением приёмника ##TODO: Должна быть возможность для изменения оператором var imp_yems_descr = { 0: { 'pname': 'эмс-изделие7', # соответствие названия устройства идентификатору входа 'dcls': {'УИЗП1': {'wfb': 300.0, 'cfb': 4000, 'ttl': -1.0, 'txe': false, 'rxe': false}} }, 1: { 'pname': 'эмс-изделие8', # соответствие названия устройства идентификатору входа 'dcls': {'УИЗП2': {'wfb': 200.0, 'cfb': 5000, 'ttl': -1.0, 'txe': false, 'rxe': false}} } } signaller.connect('yems_input_changed', _on_yems_input_changed.bind(imp_yems_descr)) for id in imp_yems_descr: var descr = imp_yems_descr[id] var unit_info = yems_info[descr.pname] for cname in descr.dcls: var incls = descr.dcls[cname] var cls = {'cname': cname, 'status': ClasterStatus.RUNNING} cls.merge(incls) unit_info.mcls.append(cls) call_deferred('_on_defer_ready', imp_yems_descr) func _on_defer_ready(imp_yems_descr: Dictionary): for id in imp_yems_descr: var descr = imp_yems_descr[id] var unit_info = yems_info[descr.pname] on_data_received(unit_info, descr.pname) ## Вызывается при изменении состояния входа ЭМС.[br] ## [param id] - идентификатор входа ЭМС[br] ## [param state] - новое состояние входа ЭМС[br] ## [param imp_yems_sescr] - описатель входов ЭМС[br] func _on_yems_input_changed(id: int, state: int, imp_yems_descr): # Проверить, есть ли в описателе этот вход if not imp_yems_descr.has(id): return var descr = imp_yems_descr[id] # Проверить, есть ли такое устройство if not yems_info.has(descr.pname): return # Получить доклад устройства var unit_info = yems_info[descr.pname] # Вход один, но кластеров может быть несколько, # поэтому нужно пробежать по всем кластерам for cls in unit_info.mcls: var incls = descr.dcls[cls.cname] # Выставить в докладе разрешение работы приёмника # или передатчика соответственно состоянию входа if incls.rxe: cls.rxe = true if state == 1 else false if incls.txe: cls.txe = true if state == 1 else false # Выполнить то же, что и для приёма данных, поэтому: on_data_received(unit_info, descr.pname) ## Вызывается при выборе оператором устройства на шкале ## [param val] - состояние кнопки переключателя устройств ## [param nm] - имя выбранного устройства func _on_scale_selected(val: bool, nm: StringName): selected_unit[nm] = val if not val: $'таблица/сетка'.clear_rows() cnames_table[nm].clear() $label_msg.text = msg_select_unit $label_msg.show() return var unit_info = yems_info[nm] if not unit_info.mcls: # проверить, есть ли данные $label_msg.text = msg_waiting_data $label_msg.show() return # данных ещё нет, ничего не далать else: $label_msg.hide() var ctable = $'таблица/сетка' var indexes: = cnames_table[nm] update_cvi(nm) var new_indexes = update_indexes(unit_info, indexes) update_table(ctable, new_indexes) map_yems_info_to_table(unit_info, ctable, indexes) connect_cmd_handlers(nm, ctable, new_indexes) ## Обновляет результат выполнения команды оператора ## [param nm] - имя устройства func update_cvi(nm: StringName): var unit_info = yems_info[nm] var unit_cmd = yems_cmd[nm] for cls_info in unit_info.mcls: var result_rxe: = ClasterResult.WAITING var result_txe: = ClasterResult.WAITING if unit_cmd.has(cls_info.cname): var cls_cmd = unit_cmd[cls_info.cname] if cls_info.status == ClasterStatus.RUNNING: if cls_cmd.has('rxe'): result_rxe = ClasterResult.DONE \ if (cls_info.rxe == cls_cmd.rxe) \ else ClasterResult.MISSMATCH else: result_rxe = ClasterResult.DONE if cls_cmd.has('txe'): result_txe = ClasterResult.DONE \ if (cls_info.txe == cls_cmd.txe) \ else ClasterResult.MISSMATCH else: result_txe = ClasterResult.DONE else: result_rxe = ClasterResult.DONE result_txe = ClasterResult.DONE cls_info['result_rxe'] = result_rxe cls_info['result_txe'] = result_txe ## Подключает обработчики команд к таблице кластеров ## [param nm] - имя устройства ## [param new_indexes] - новые индексы рядов таблицы ## [param cls_table] - таблица для отображения кластеров func connect_cmd_handlers(nm: StringName, ctable: GDTable, new_indexes: Array): for icls in new_indexes: var i_row = icls[0] # Имя устройства и имя кластера var cls = icls[1] # Подключить обработчки разрешения передатчиков var node = ctable.get_node2(column_cmd_tx, i_row) node.connect_check('toggled', _on_user_cmd.bind(nm, cls.cname, 'txe')) # Подключить обработчки разрешения приёмников node = ctable.get_node2(column_cmd_rx, i_row) node.connect_check('toggled', _on_user_cmd.bind(nm, cls.cname, 'rxe')) ## Вызывается при переключении оператором разрешений для кластера func _on_user_cmd(val: bool, nm: StringName, cname: String, param_name: StringName): var unit_cmd = yems_cmd[nm] if cname in unit_cmd: unit_cmd[cname][param_name] = val else: unit_cmd[cname] = {param_name: val} ## Обрабатывает событие [b]data_received[/b] ## [param data] - данные принятые от устройства ## [param nm] - имя устройства func on_data_received(data: Dictionary, nm: StringName): set_timeouts(nm, data) var unit_info: = yems_info[nm] unit_info_merge(unit_info, data) if not unit_info: # для устройства нет данных return var ctable: GDTable = $'таблица/сетка' var cscale: = name_to_scale[nm] as GridContainer # Шкала устройства var indexes: = cnames_table[nm] # Индексы строк устройства в таблице update_cvi(nm) # Если текущее устройство отображается в таблице, # обновить таблицу новыми данными if selected_unit[nm]: var new_indexes = update_indexes(unit_info, indexes) update_table(ctable, new_indexes) map_yems_info_to_table(unit_info, ctable, indexes) connect_cmd_handlers(nm, ctable, new_indexes) map_yems_info_to_scale(unit_info, cscale) var inters: = find_inters() reset_matrix() map_inters_to_matrix(inters, $'матрица') cmd_to_unit(nm) ## Создаёт новые ряды в таблице func update_table(ctable: GDTable, new_indexes: Array): # Добавить новые ряды. Два прохода нужно потому, # что порядок элементов в словаре не определён. for icls in new_indexes: ctable.add_row(table_cell_row) # Заполнить новые ряды данными for icls in new_indexes: var i_row = icls[0] var cls = icls[1] # Установить высоту ряда (можно пропустить, если нужная высота будет выставлена по умолчанию) ctable.set_row_min_hight(i_row, table_rows_hight) # Установить новый текст ctable.set_node_property(column_cmd_rx, i_row, 'pressed', cls.rxe) ctable.set_node_property(column_cmd_tx, i_row, 'pressed', cls.txe) ctable.set_node_property(column_cmd_rx, i_row, 'state', cls.result_rxe) ctable.set_node_property(column_cmd_tx, i_row, 'state', cls.result_txe) ## Обновляет индексы строк в таблице func update_indexes(unit_info: Dictionary, indexes: Dictionary) -> Array: var new_indexes: = [] for cls in unit_info.mcls: # Проверить, есть ли уже такой индекс if indexes.has(cls.cname): continue # Записать индекс нового ряда (связать с именем кластера) var i_row = indexes.size() indexes[cls.cname] = i_row new_indexes.append([i_row, cls]) return new_indexes ## Объединяет список кластеров принятых от устройства со списком сохранённым ## [param unit_info] - доклад ранее полученный ## [param data_rx] - доклад принятый от устройства func unit_info_merge(unit_info: Dictionary, data_rx: Dictionary): var drx: = {} for cls_rx in data_rx.mcls: ref_cls.cname = 'неопр.№%d' % (drx.size() + 1) cls_rx.merge(ref_cls, false) # Объеденить без перезаписи drx[cls_rx.cname] = cls_rx var dinfo: = {} for cls in unit_info.mcls: dinfo[cls.cname] = cls dinfo.merge(drx, true) unit_info.merge(data_rx, true) unit_info.mcls = dinfo.values() ## Отправляет команду в сетевой стек ## [param nm] - имя устройства func cmd_to_unit(nm: StringName): var unit_yemstg: = network.get_unit_instance(nm) # Проверить, что сетевой стек отправил предидущую команду if unit_yemstg.cmd_state != unit.Unit.CmdState.DONE: return # Сформировать команду из докладов (данных принятых из сети от устройств) var unit_info_copy = yems_info[nm].duplicate(true) var unit_cmd = yems_cmd[nm] unit_info_copy.mtype = 'cmd' # Выбрать только работающие кластеры (время которых ещё не истекло) var mcls = unit_info_copy.mcls.filter(func(c): return c.status == ClasterStatus.RUNNING) for cls_info in mcls: # Если для этого кластера есть команда оператора ... if unit_cmd.has(cls_info.cname): # ... переписать значениями из команды оператора var cmd_cls = unit_cmd[cls_info.cname] cls_info.rxe = unit_cmd.get('rxe', cls_info.rxe) cls_info.txe = unit_cmd.get('txe', cls_info.txe) # Передать команду сетевому стеку unit_yemstg.cmd.merge(unit_info_copy, true) unit_yemstg.cmd_state = unit.Unit.CmdState.SEND ## Запускает таймер достоверности кластера ## [param nm] - имя устройства ## [param unit_info] - доклады устройства func set_timeouts(nm: StringName, unit_info: Dictionary): for cls in unit_info.mcls: var timer_name = NodePath('%s_%s' % [nm, cls.cname]) var timer = get_node_or_null(timer_name) cls['status'] = ClasterStatus.RUNNING if cls.ttl < 0.0: continue if not timer: timer = Timer.new() timer.name = timer_name timer.one_shot = true timer.connect('timeout', _on_timeout_ttl.bind(nm, cls.cname)) add_child(timer) timer.start(cls.ttl) ## Обработчик истечения времени достоверности кластера ## [param nm] - имя устройства ## [param cname] - имя кластера func _on_timeout_ttl(nm: StringName, cname: StringName): var timer_name = NodePath('%s_%s' % [nm, cname]) var timer = get_node_or_null(timer_name) if timer: remove_child(timer) timer.queue_free() var unit_info = yems_info[nm] for cls in unit_info.mcls: if cname == cls.cname: cls.status = ClasterStatus.DELETED cls.result_rxe = ClasterResult.WAITING cls.result_txe = ClasterResult.WAITING var ctable: GDTable = $'таблица/сетка' var indexes: = cnames_table[nm] if indexes: map_yems_info_to_table(unit_info, ctable, indexes) var claster_scale: = name_to_scale[nm] as GridContainer map_yems_info_to_scale(unit_info, claster_scale) reset_matrix() ## Сбрасывает элементы матрицы в исходное состояние func reset_matrix(): var nodes = get_tree().get_nodes_in_group('матрица-к') for node in nodes: node.button_pressed = false ## Отображает пересечения на матрице ## [param inters] - пересечения ## [param matrix] - матрица func map_inters_to_matrix(inters: Array, matrix: GridContainer) -> void: var nodes = matrix.get_children() for item in inters: var tx_unit_name = item[0] var rx_unit_name = item[1] var i_col = 1 + name_to_index[tx_unit_name] # (1 +) - одна колонка для значков приёмников var i_row = 1 + name_to_index[rx_unit_name] # (1 +) - один ряд для значков передатчиков var idx = i_col + i_row * matrix.columns # последовательный индекс в GridContainer var node = nodes[idx] # Искомый элемент node.button_pressed = true ## Отображает кластеры в таблицу ## [param unit_info] - доклад от устройства ## [param ctable] - таблица ## [param indexes] - индексы в таблице func map_yems_info_to_table(unit_info: Dictionary, ctable: GDTable, indexes: Dictionary): # Обойти все кластеры устройства for cls in unit_info.mcls: # Строки для заполнения ряда var status_str: = 'Активен' if cls.status == ClasterStatus.RUNNING else 'Удалён' var row_text: = [cls.cname, status_str, '%.1f' % cls.ttl, '%.1f' % cls.daa, '%.1f' % cls.dap, '%.1f-%.1f' % [cls.cfb - cls.wfb / 2.0, cls.cfb + cls.wfb / 2.0]] # Получить индекс строки кластера в таблице var i_row = indexes[cls.cname] # Установить новый текст ctable.set_row_text(i_row, row_text) # Результат выполнения ctable.set_node_property(column_cmd_rx, i_row, 'state', cls.result_rxe) ctable.set_node_property(column_cmd_tx, i_row, 'state', cls.result_txe) ## Находит пересечения диапазонов всех устройств и кластеров func find_inters() -> Array: var result = [] var all_clusters = [] # Собираем все кластеры из всех устройств в один массив for nm in yems_info: # nm - имя устройства var unit_data = yems_info[nm] if not unit_data: continue var clasters = unit_data.mcls.filter \ (func(c): return c.status == ClasterStatus.RUNNING) for cls in clasters: # cls - один кластер var cfb = float(cls.cfb) var wfb = float(cls.wfb) / 2.0 # расчёт границ частотных диапазонов var freq_min1 = cfb - wfb var freq_max1 = cfb + wfb all_clusters.append([nm, cls.cname, freq_min1, freq_max1, cls.txe, cls.rxe]) for i in all_clusters.size(): var cl1 = all_clusters[i] var dev1 = cl1[0] var name1 = cl1[1] var min1 = cl1[2] var max1 = cl1[3] var txe1 = cl1[4] var rxe1 = cl1[5] # Сравниваем текущий кластер со всеми последующими, # чтобы избежать дублей и самопересечений for j in all_clusters.size(): var cl2 = all_clusters[j] var dev2 = cl2[0] var name2 = cl2[1] var min2 = cl2[2] var max2 = cl2[3] var txe2 = cl2[4] var rxe2 = cl2[5] # Если нет пары TR(RT) то пропускаем эту комбинацию if not ((txe1 and rxe2) or (rxe1 and txe2)): continue # Проверка на пересечение диапазонов, пересечение # есть если начало одного диапазона меньше конца другого if max1 > min2 and max2 > min1: #Вычисляем точные границы пересечения var inter_start = max(min1, min2) var inter_end = min(max1, max2) # Формируем результат в требуемом формате result.append([dev1, dev2, name1, name2, inter_start, inter_end]) return result ## Отображает кластеры на шкале ## [param unit_info] - доклад устройства ## [param cscale] - шкала кластера func map_yems_info_to_scale(unit_info: Dictionary, cscale: GridContainer): # Обойти все кластеры устройства for cls in unit_info.mcls: # Преобразовать диапазоны частот в диапазоны отображения var c: = remap(cls.cfb, freq_min, freq_max, 0.0, 1.0) var w: = remap(cls.wfb, freq_min * 0.5, freq_max * 0.5, 0.0, 0.5) # Чтобы диапазон всегда был виден, даже если он слишком узкий w = clampf(w, 0.005, 0.5) # Отсечь значения по границам var l: = clampf(c - w, 0.0, 1.0) var h: = clampf(c + w, 0.0, 1.0) # Получить шкалу приёмника var scale_rx: = cscale.get_node('panel_rx') # Получить шкалу передатчика var scale_tx: = cscale.get_node('panel_tx') # Диапазон работает (информация обновляется) if cls.status == ClasterStatus.RUNNING: # Отобразить диапазон приёмника, в соответствии с разрешением var color_rxe: = color_enabled if cls.rxe else color_disabled scale_rx.set_range(cls.cname, l, h, color_rxe) # Отобразить диапазон передатчика, в соответствии с разрешением var color_txe: = color_enabled if cls.txe else color_disabled scale_tx.set_range(cls.cname, l, h, color_txe) # Диапазон удалён (информация не обновляется) elif cls.status == ClasterStatus.DELETED: scale_rx.del_range(cls.cname) scale_tx.del_range(cls.cname)