Files
uarep-ctl/scenes/эмс-тг/эмс-тг.gd
2026-03-30 12:50:57 +03:00

450 lines
21 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 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
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_cvi(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 result_rxe: = ClasterResult.WAITING
var result_txe: = ClasterResult.WAITING
if unit_cvi.has(cname):
result_rxe = unit_cvi[cname].result_rxe
result_txe = unit_cvi[cname].result_txe
ctable.set_node_property(column_cmd_rx, i_row, 'state', int(result_rxe))
ctable.set_node_property(column_cmd_tx, i_row, 'state', int(result_txe))
func update_cvi(nm: StringName):
var unit_info = yems_info[nm]
var unit_cvi = {}
var unit_cmd = yems_cmd[nm]
for cls_info in unit_info.mcls:
var result_rxe = ClasterResult.DONE
var result_txe = ClasterResult.DONE
if not unit_cmd.has(cls_info.cname):
unit_cvi[cls_info.cname] = \
{ 'result_rxe': ClasterResult.DONE,
'result_txe': ClasterResult.DONE }
continue
var cls_cmd = unit_cmd[cls_info.cname]
if cls_cmd.has('rxe'):
result_rxe = ClasterResult.DONE if (cls_info.rxe == cls_cmd.rxe) else ClasterResult.MISSMATCH
if cls_cmd.has('txe'):
result_txe = ClasterResult.DONE if (cls_info.txe == cls_cmd.txe) else ClasterResult.MISSMATCH
unit_cvi[cls_info.cname] = \
{ 'result_rxe': result_rxe,
'result_txe': result_txe }
cmd_vs_info[nm].merge(unit_cvi, true)
## Подключает обработчики команд к таблице кластеров
## [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_cvi(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.merge(data_rx, 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 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.merge(cmd_cls, true)
cls_info.erase('status')
# Передать команду сетевому стеку
unit_yemstg.cmd.merge(unit_info_copy, 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)
ctable.set_node_property(column_cmd_rx, i_row, 'state', ClasterResult.WAITING)
ctable.set_node_property(column_cmd_tx, i_row, 'state', ClasterResult.WAITING)
## Находит пересечения диапазонов
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
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 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)