Files
uarep-ctl/scenes/эмс-тг/эмс-тг.gd
2026-03-26 16:16:22 +03:00

438 lines
20 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@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_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
#endregion
#region Заполняется во время работы
var yems_info: Dictionary[StringName, Dictionary] ## Доклады
var yems_cmd: Dictionary[StringName, Dictionary] ## Команды
## Порядок следования кластеров в таблице.
## {имя_устройства: { имя_кластера: индекс_в_таблице } }
var cnames_table: Dictionary[StringName, Dictionary]
## Порядок следования кластеров на шкале.
## {имя_устройства: { индекса_шкале } }
var pnames_scale: Dictionary[StringName, int]
## Текущее выбранное оператором устройство на шкале
var selected_unit: Dictionary[StringName, bool]
## Состояния кластеров в соответствии с докладами и командами
var cmd_vs_info: Dictionary[StringName, Dictionary]
#endregion
## Состояние кластера
enum ClasterStatus {
DELETED,
RUNNING
}
## Режим управления кластером
enum ClasterMode {
MANUAL, ## Ручной, управляется операторром
AUTO, ## Автоматический, управляется программой
IGNORE ## Игнорируемый, не управляется
}
## Состояние кластера. Значения должны соответствовать состояниям [b]six-state-button[/b]
enum ClasterState {
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
cmd_vs_info[nm] = {}
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(6, 'image_file', image_file_rx)
ctable.set_header_property(7, '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()
## Вызывается при выборе оператором устройства на шкале
## [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: # проверить, есть ли данные
$label_msg.text = msg_waiting_data
return # данных ещё нет, ничего не далать
$label_msg.hide()
var ctable = $'таблица/сетка'
var indexes = cnames_table[nm]
var unit_cvi = cmd_vs_info[nm]
map_yems_info_to_table(unit_info, ctable, indexes)
connect_cmd_handlers(ctable, nm, indexes)
update_cmd_vs_info(nm)
map_cvi_to_table(unit_cvi, ctable, indexes)
func map_cvi_to_table(unit_cvi: Dictionary, ctable: GDTable, indexes: Dictionary):
for cname in indexes:
var i_row = indexes[cname]
var state_rxe: = ClasterState.WAITING
var state_txe: = ClasterState.WAITING
if unit_cvi.has(cname):
state_rxe = unit_cvi[cname]['state_rxe']
state_txe = unit_cvi[cname]['state_txe']
ctable.set_node_property(column_cmd_rx, i_row, 'state', int(state_rxe))
ctable.set_node_property(column_cmd_tx, i_row, 'state', int(state_txe))
func update_cmd_vs_info(nm: StringName):
if yems_info.has(nm):
return
var unit_info = yems_info[nm]
if not unit_info.has('mcls'):
return
if not yems_cmd.has(nm):
return
var unit_cvi = {}
for cls_info in unit_info.mcls:
var claster_cvi = {}
claster_cvi['state_txe'] = ClasterState.WAITING
claster_cvi['state_rxe'] = ClasterState.WAITING
var unit_cmd = yems_cmd[nm]
if not unit_cmd.has(cls_info.cname):
continue
var cls_cmd = unit_cmd[cls_info.cname]
claster_cvi['state_rxe'] = ClasterState.DONE if cls_info.rxe == cls_cmd.rxe else ClasterState.MISSMATCH
claster_cvi['state_txe'] = ClasterState.DONE if cls_info.txe == cls_cmd.txe else ClasterState.MISSMATCH
unit_cvi[cls_info.cname] = claster_cvi
cmd_vs_info[nm] = unit_cvi
## Подключает обработчики команд к таблице кластеров
## [param cls_table] - таблица для отображения кластеров
## [param nm] - имя устройства
func connect_cmd_handlers(cls_table: GDTable, nm: StringName, indexes: Dictionary):
var rows_count = cls_table.get_rows_count()
if indexes.size() != rows_count:
push_error('indexes.size() != rows_count')
return
for cname in indexes:
# Имя устройства и имя кластера
var i_row = indexes[cname]
# Подключить обработчки разрешения приёмников
var node = cls_table.get_node2(column_cmd_rx, i_row)
node.connect_check('toggled', _on_user_cmd.bind(nm, cname, 'rxe'))
# Подключить обработчки разрешения приёмников
node = cls_table.get_node2(column_cmd_tx, i_row)
node.connect_check('toggled', _on_user_cmd.bind(nm, cname, 'txe'))
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]
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 claster_scale: = name_to_scale[nm] as GridContainer
var indexes: = cnames_table[nm] # Индексы строк в таблице
if selected_unit[nm]:
map_yems_info_to_table(unit_info, ctable, indexes)
map_yems_info_to_scale(unit_info, claster_scale)
var inters: = find_inters(yems_info)
var unit_cvi = cmd_vs_info[nm]
reset_matrix()
map_inters_to_matrix(inters, $'матрица')
cmd_to_unit(nm)
update_cmd_vs_info(nm)
map_cvi_to_table(unit_cvi, ctable, indexes)
# Объединяет списки кластеров принятых от устройства
func unit_info_merge(unit_info: Dictionary, data_rx: Dictionary):
var drx: = {}
for cls in data_rx.mcls:
drx[cls.cname] = cls
var dinfo: = {}
for cls in unit_info.mcls:
dinfo[cls.cname] = cls
dinfo.merge(drx, true)
unit_info.mcls = dinfo.values()
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 cmd = yems_info[nm]
# Переписать новыми значениями из команды заданной оператором
cmd['mtype'] = 'cmd'
var unit_cmd = yems_cmd[nm]
for cls_info in cmd.mcls:
if unit_cmd.has(cls_info.cname):
var cmd_cls = unit_cmd[cls_info.cname]
cls_info.merge(cmd_cls, true)
unit_yemstg.cmd.merge(cmd, true)
unit_yemstg.cmd_state = unit.Unit.CmdState.SEND
func set_timeouts(nm: StringName, unit_info: Dictionary):
for cls in unit_info.mcls: # cls - один кластер
var timer_name = NodePath('%s_%s' % [nm, cls.cname])
var timer = get_node_or_null(timer_name)
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)
cls['status'] = ClasterStatus.RUNNING
## Обработчик истечения времени жизни кластера
func _on_timeout_ttl(nm, cname):
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
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
## Отображает пересечения на матрице
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 nm] - имя устройства
func map_yems_info_to_table(unit_info: Dictionary, ctable: GDTable, indexes: Dictionary):
# Обойти все кластеры устройства
for cls in unit_info.mcls:
# Строки для заполнения ряда
var row_text = [cls.cname, 'Активен', '%.1f' % cls.ttl, '%.1f' % cls.daa, '%.1f' % cls.dap, '%.1f-%.1f' % [cls.cfb - cls.wfb / 2.0, cls.cfb + cls.wfb / 2.0]]
# Если такой кластер уже есть, обновить его строку в таблице
if cls.cname in indexes:
# Получить индекс строки кластера в таблице
var i_row = indexes[cls.cname]
# Установить новый текст
ctable.set_row_text(i_row, row_text)
# Если такого кластера нет, то создать его строку в таблице
else:
# Добавить новый ряд
ctable.add_row(table_cell_row)
# Получить индекс нового ряда в таблице
var i_row: = indexes.size()
# Записать индекс нового ряда (связать с именем кластера)
indexes[cls.cname] = i_row
# Установить высоту ряда (можно пропустить, если нужная высота будет выставлена по умолчанию)
ctable.set_row_min_hight(i_row, table_rows_hight)
# Установить новый текст
ctable.set_row_text(i_row, row_text)
ctable.set_node_property(column_cmd_rx, i_row, 'pressed', cls.rxe)
ctable.set_node_property(column_cmd_tx, i_row, 'pressed', cls.txe)
## Находит пересечения диапазонов
func find_inters(info: Dictionary) -> Array:
var result = []
var all_clusters = []
# Собираем все кластеры из всех устройств в один массив
for nm in info:
# nm - имя устройства
var unit_data = info[nm]
if not unit_data: continue
# cls - один кластер
for cls in unit_data.mcls.filter(func(c): return c.status == ClasterStatus.RUNNING):
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 nm] - имя устройства
func map_yems_info_to_scale(unit_info: Dictionary, claster_scale: 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, freq_max, 0.0, 0.5)
# Чтобы диапазон всегда был виден, даже если он слишком узкий
w = clampf(w, 0.005, 1.0)
# Отсечь значения по границам
var l: = clampf(c - w, 0.0, 1.0)
var h: = clampf(c + w, 0.0, 1.0)
# Получить шкалу приёмника
var scale_rx: = claster_scale.get_node('panel_rx')
# Получить шкалу передатчика
var scale_tx: = claster_scale.get_node('panel_tx')
# Проверить что диапазон работает
if cls.status == ClasterStatus.RUNNING: # Диапазон работает (информация обновляется)
# Отобразить диапазон приёмника, в соответствии с разрешением
var color_rxe: = color_enabled if cls.get('rxe', false) else color_disabled
scale_rx.set_range(cls.cname, l, h, color_rxe)
# Отобразить диапазон передатчика, в соответствии с разрешением
var color_txe: = color_enabled if cls.get('txe', false) 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)