Files
imitator-case-y5/udp_broadcast.gd

586 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.

class_name UDPBroadcast
# Константы для размеров данных и смещений
const STATUS_PACKET_SIZE: int = 103
const HEADER_SIZE: int = 8
const EMS_G_DATA_OFFSET: int = 9
const EMS_G_DATA_SIZE: int = 26 # 13 * 2 байта
const UG_DATA_OFFSET: int = 35
const UKP0_DATA_OFFSET: int = 39
const UKP1_DATA_OFFSET: int = 71
const UKP_DATA_SIZE: int = 32
# Константы команд ISA
const CMD_READ_ISA: int = 0
const CMD_WRITE_ISA: int = 1
# Минимальный размер пакета для валидации
const MIN_PACKET_SIZE: int = 7
# Словарь адресов устройств
const ADDRESS_LIST: Dictionary = {
'ПРД-Н1': [50011, "10.1.1.11"],
'ПРД-В1': [50012, "10.1.1.12"],
'ПРД-К1': [50013, "10.1.1.13"],
'ПРД-Н2': [50021, "10.1.1.21"],
'ПРД-В2': [50022, "10.1.1.22"],
'ПРД-К2': [50023, "10.1.1.23"],
'ПРД-Н3': [50031, "10.1.1.31"],
'ПРД-В3': [50032, "10.1.1.32"],
'ПРД-К3': [50033, "10.1.1.33"],
'ПРД-Н4': [50041, "10.1.1.41"],
'ПРД-В4': [50042, "10.1.1.42"],
'ПРД-К4': [50043, "10.1.1.43"],
}
# Широковещательный адрес
const BROADCAST_ADDR: Array = [50000, "10.1.1.255"]
# Приватные свойства
var status_manager: StatusManager
var _socket: SocketUDP
var _self_name: String
var _isa_ports: Dictionary = {}
var _written_isa_ports: Array = []
var _timer: Timer
var _sockets_status_bit: int = 0
var _dkm_status_bit: int = 0
var _dry_contact_status_bit: int = 0xFF
var _cmd_number_count: int = 0
var _last_received_command: int = -1
var ukp0_data: PackedByteArray
var ukp1_data: PackedByteArray
signal update_isa_ports(_isa_ports: Dictionary)
func _init(name: String):
"""Инициализация UDP broadcast клиента"""
status_manager = StatusManager.new(self)
_self_name = name
_initialize_socket()
_initialize_ukp_data()
_initialize_timers()
_initialize_isa_ports()
if not _bind_to_device_address(name):
push_error("Не удалось инициализировать устройство: %s" % name)
func _initialize_socket() -> void:
"""Инициализация UDP сокета"""
_socket = SocketUDP.new()
func _initialize_timers() -> void:
"""Инициализация таймеров"""
# Таймер для широковещательной рассылки (3 раза в секунду)
_timer = Timer.new()
_timer.wait_time = 1.0 / 5.0
_timer.timeout.connect(_on_broadcast_timeout)
# Таймер для частого опроса сокета (100 раз в секунду)
#_poll_timer = Timer.new()
#_poll_timer.wait_time = 0.05
#_poll_timer.timeout.connect(_on_poll_timeout)
func _initialize_ukp_data() -> void:
"""Инициализация данных UKP нулевыми значениями"""
ukp0_data = PackedByteArray()
ukp0_data.resize(UKP_DATA_SIZE)
ukp0_data.fill(0)
ukp1_data = PackedByteArray()
ukp1_data.resize(UKP_DATA_SIZE)
ukp1_data.fill(0)
func _initialize_isa_ports() -> void:
"""Инициализация ISA портов начальными значениями"""
# Инициализируем порты, которые могут запрашиваться
set_isa_port(0x0106, 0) # DOU2 статус
set_isa_port(0x0108, 0) # DOU3 статус
set_isa_port(0x010A, 0) # ATT
func _bind_to_device_address(device_name: String) -> bool:
"""Привязка к адресу устройства по его имени"""
if not ADDRESS_LIST.has(device_name):
push_error("Неизвестное устройство: %s" % device_name)
return false
var port: int = ADDRESS_LIST[device_name][0]
var address: String = ADDRESS_LIST[device_name][1]
return bind_unit_address(port, address)
func bind_unit_address(port: int, address: String) -> bool:
"""Привязка сокета к указанному порту и адресу"""
if port <= 0 or port > 65535:
push_error("Некорректный номер порта: %d" % port)
return false
var bind_result = _socket.bind(port, address)
if bind_result != OK:
push_error("Не удалось привязаться к %s:%d - %s" % [address, port, error_string(bind_result)])
return false
print("Успешно привязано к %s:%d" % [address, port])
return true
func add_timers_to_scene(parent: Node) -> void:
"""Добавление таймеров в дерево сцены"""
# Проверяем что таймеры еще не имеют родителя
if not _timer.get_parent():
parent.add_child(_timer)
elif _timer.get_parent() != parent:
push_warning("Таймер broadcast уже добавлен в другую сцену")
#if not _poll_timer.get_parent():
#parent.add_child(_poll_timer)
#elif _poll_timer.get_parent() != parent:
#push_warning("Таймер poll уже добавлен в другую сцену")
func start_broadcast() -> void:
"""Запуск широковещательной рассылки и опроса сокета"""
_timer.start()
#_poll_timer.start()
print("Широковещательная рассылка запущена")
func stop_broadcast() -> void:
"""Остановка широковещательной рассылки"""
_timer.stop()
#_poll_timer.stop()
print("Широковещательная рассылка остановлена")
func close_udp_unit() -> void:
"""Закрытие UDP соединения"""
stop_broadcast()
_socket.close()
print("UDP соединение закрыто")
#region Публичные методы для управления состоянием
func set_sockets_status(status: int) -> void:
"""Установка статуса сокетов"""
_sockets_status_bit = status
func get_sockets_status() -> int:
"""Получение статуса сокетов"""
return _sockets_status_bit
func set_dkm_status(status: int) -> void:
"""Установка статуса DKM"""
_dkm_status_bit = status
func get_dkm_status() -> int:
"""Получение статуса DKM"""
return _dkm_status_bit
func set_ip_status(status: int)->void:
"""Установка статуса ИП"""
_dry_contact_status_bit = status
func get_ip_status()->int:
"""Получение статуса ИП"""
return _dry_contact_status_bit
func set_isa_port(port_addr: int, value: int) -> bool:
"""Установка значения ISA порта"""
if port_addr < 0 or port_addr > 0xFFFF:
push_error("Некорректный адрес порта: 0x%03X" % port_addr)
return false
_isa_ports[port_addr] = value
emit_signal('update_isa_ports', _isa_ports)
return true
func get_isa_port(port_addr: int) -> int:
"""Получение значения ISA порта"""
return _isa_ports.get(port_addr, 0xFFFF)
func read_isa_ports(port_addresses: Array) -> Dictionary:
"""Чтение значений нескольких ISA портов"""
var result = {}
for port_addr in port_addresses:
if port_addr is int and port_addr >= 0 and port_addr <= 0xFFFF:
result[port_addr] = get_isa_port(port_addr)
else:
push_error("UDPBroadcast: Некорректный адрес порта: " + str(port_addr))
return result
#endregion
#region Обработчики таймеров
func _on_broadcast_timeout() -> void:
"""Обработчик таймера широковещательной рассылки"""
send_broadcast()
func _on_poll_timeout() -> void:
"""Обработчик таймера опроса сокета"""
_poll_socket()
#endregion
#region Работа с сокетом
func send_broadcast() -> bool:
"""Отправка широковещательного пакета"""
_socket.set_broadcast_enabled(true)
var data: PackedByteArray = _form_status_device_packet()
var send_result = _socket.send_to(BROADCAST_ADDR[1], BROADCAST_ADDR[0], data)
if send_result == OK:
return true
else:
push_error("Ошибка отправки широковещательного пакета")
return false
func _poll_socket() -> void:
"""Опрос сокета для получения входящих пакетов"""
while _socket.get_available_packet_count() > 0:
var packet = _socket.get_packet()
var address = _socket.get_packet_ip()
var port = _socket.get_packet_port()
_handle_received_data(packet, address, port)
#endregion
#region Формирование пакета статуса устройства
func _form_status_device_packet() -> PackedByteArray:
var buffer = PackedByteArray()
# Базовый размер пакета + дополнительные байты для записанных портов
# + 2 байта для полей команды (код + результат)
var header_size = 8
var ports_count_size = 1
var cmd_fields_size = 0
var isa_ports_size = 0
if _written_isa_ports.size() > 0:
isa_ports_size = _written_isa_ports.size() * 4
cmd_fields_size = 4
var total_size = header_size + STATUS_PACKET_SIZE + ports_count_size + cmd_fields_size + isa_ports_size
buffer.resize(total_size)
buffer.fill(0)
_encode_packet_header(buffer)
_encode_device_data(buffer)
# Добавляем поле команды после статуса
if _written_isa_ports.size() > 0:
var cmd_offset = header_size + STATUS_PACKET_SIZE
if cmd_offset < buffer.size():
buffer.encode_u16(cmd_offset, _last_received_command) # код команды (0 для статуса)
if (cmd_offset + 1) < buffer.size():
buffer.encode_u16(cmd_offset+1, 1) # результат выполнения
var ports_count_offset = cmd_offset + 2
# Затем ISA порты
buffer.encode_u8(ports_count_offset, _written_isa_ports.size())
_encode_written_isa_ports(buffer, ports_count_offset+1)
return buffer
func _encode_packet_header(buffer: PackedByteArray) -> void:
"""Кодирование заголовка пакета"""
# 0-1: Reserved (0x0000)
buffer.encode_u16(0, 0)
# 2-3: Номер команды
buffer.encode_u16(2, _cmd_number_count)
# 4-5: Reserved (0x0000)
buffer.encode_u16(4, 0)
# 6-7: Длина статуса
buffer.encode_u16(6, STATUS_PACKET_SIZE)
func _encode_written_isa_ports(buffer: PackedByteArray, offset: int) -> void:
"""Добавление данных о записанных ISA портах в конец пакета"""
for i in range(_written_isa_ports.size()):
var port_data = _written_isa_ports[i]
# Добавляем порт и значение (по 2 байта каждый)
buffer.encode_u16(offset, port_data[0])
buffer.encode_u16(offset + 2, port_data[1])
offset += 4
func _encode_device_data(buffer: PackedByteArray) -> void:
"""Кодирование данных устройств в пакет"""
# 8-9: Статус сокетов
buffer.encode_u16(8, _sockets_status_bit)
# 9-34: Данные EMS-G
_encode_ems_g_data(buffer)
# 35-38: Данные UG
_encode_ug_data(buffer)
# 39-70: Данные UKP0
# 71-102: Данные UKP1
_encode_ukp_data(buffer)
func _encode_ems_g_data(buffer: PackedByteArray) -> void:
"""Кодирование данных EMS-G"""
var ems_g_data = [
0x9e00, 0xffff, 0xffff, 0x0000, 0xffff, 0xffff, 0x0000,
0xffff, 0xffff, 0x0000, 0xffff, 0xffff, 0xffff
]
for i in range(ems_g_data.size()):
buffer.encode_u16(EMS_G_DATA_OFFSET + i * 2, ems_g_data[i])
func _encode_ug_data(buffer: PackedByteArray) -> void:
"""Кодирование данных UG"""
# D15...D8 = ДКМ (старший байт)
buffer.encode_u8(UG_DATA_OFFSET + 1, _dkm_status_bit & 0xFF)
# D7...D0 = Сухие контакты (младший байт)
buffer.encode_u8(UG_DATA_OFFSET, _dry_contact_status_bit & 0xFF)
func _encode_ukp_data(buffer: PackedByteArray) -> void:
"""Кодирование данных UKP"""
for i in range(UKP_DATA_SIZE):
if i < ukp0_data.size():
buffer[UKP0_DATA_OFFSET + i] = ukp0_data[i]
if i < ukp1_data.size():
buffer[UKP1_DATA_OFFSET + i] = ukp1_data[i]
#endregion
#region Прямая отправка ответа
func _send_immediate_response(address: String, port: int) -> void:
"""Немедленная отправка пакета на указанный адрес и порт"""
var data: PackedByteArray = _form_status_device_packet()
_socket.set_broadcast_enabled(false) # Отключаем broadcast для точечной отправки
var send_result = _socket.send_to(address, port, data)
_socket.set_broadcast_enabled(true) # Включаем обратно для broadcast
if send_result == OK:
print("Отправлен немедленный ответ на %s:%d" % [address, port])
else:
push_error("Ошибка отправки ответа на %s:%d" % [address, port])
#endregion
#region Обработка входящих данных
func _handle_received_data(data: PackedByteArray, address: String, port: int) -> void:
"""Обработка входящих данных"""
if not _validate_received_packet(data):
return
var command_type = _get_command_type(data)
_cmd_number_count = data.decode_u16(2)
if command_type == -1: # Проверяем на -1 вместо null
return
_handle_isa_command(command_type, data, address, port)
func _validate_received_packet(data: PackedByteArray) -> bool:
"""Валидация входящего пакета"""
if data.size() < MIN_PACKET_SIZE:
push_warning("Получен слишком короткий пакет: %d байт" % data.size())
return false
return true
func _get_command_type(data: PackedByteArray) -> int:
"""Получение типа команды из данных пакета"""
if data.size() < 7:
return -1 # Используем -1 для обозначения ошибки
# Определяем длину статуса из заголовка
var status_length = data.decode_u8(7)
if data.size() <= (status_length + 8):
return -1 # Используем -1 для обозначения ошибки
return data.decode_u8(6)
func _handle_isa_command(command_code: int, data: PackedByteArray, address: String, port: int) -> void:
"""Обработка команд ISA"""
_last_received_command = command_code # Запоминаем последнюю команду
match command_code:
CMD_READ_ISA:
_handle_read_isa_command(data, address, port)
CMD_WRITE_ISA:
_handle_write_isa_command(data, address, port)
_:
push_warning("Неизвестная команда ISA: %d от %s:%d" % [command_code, address, port])
func _handle_read_isa_command(data: PackedByteArray, address: String, port: int) -> void:
"""Обработка команды чтения ISA портов"""
var ports = _parse_command_ports(data)
if ports.is_empty():
return
var port_data = _get_isa_ports_data(ports)
print('Принята команда на чтение ISA портов от %s:%d: %s' % [address, port, port_data])
_written_isa_ports = []
for i in range(ports.size()):
_written_isa_ports.append([ports[i], port_data[i]])
# Отправляем ответ с данными запрошенных портов
_send_immediate_response(address, port)
func _handle_write_isa_command(data: PackedByteArray, address: String, port: int) -> void:
"""Обработка команды записи ISA портов"""
var ports = _parse_command_ports(data)
var data_to_write = _parse_command_ports_data(data)
if ports.size() != data_to_write.size():
push_warning("Несоответствие размеров портов и данных от %s:%d" % [address, port])
return
var added_ports = 0
var updated_ports = 0
# Обновляем или добавляем записанные порты
for i in range(ports.size()):
var port_addr = ports[i]
var value = data_to_write[i]
set_isa_port(port_addr, value)
# Ищем порт в массиве, если есть - обновляем, если нет - добавляем
var found = false
for j in range(_written_isa_ports.size()):
if _written_isa_ports[j][0] == port_addr:
_written_isa_ports[j][1] = value # Обновляем значение
found = true
updated_ports += 1
break
if not found:
_written_isa_ports.append([port_addr, value]) # Добавляем новый порт
added_ports += 1
if added_ports > 0 or updated_ports > 0:
print("Обработана команда записи от %s:%d - добавлено: %d, обновлено: %d портов" % [address, port, added_ports, updated_ports])
print('Принята команда на запись в ISA порты от %s:%d: %s' % [address, port, _isa_ports])
# Немедленная отправка ответа отправителю
_send_immediate_response(address, port)
#endregion
#region Парсинг команд ISA
func _parse_command_ports(data: PackedByteArray) -> Array:
"""Парсинг адресов портов из команды"""
if data.size() < 7:
return []
var ports_count = data.decode_u8(7)
if data.size() <= (ports_count + 8):
return []
var ports = []
for i in range(ports_count):
var port_address = data.decode_u16(8 + i * 2)
ports.append(port_address)
print("Порт %d: 0x%03X" % [i, port_address])
return ports
func _parse_command_ports_data(data: PackedByteArray) -> Array:
"""Парсинг данных портов из команды записи"""
if data.size() < 8:
return []
var ports_count = data.decode_u8(7)
if data.size() <= (ports_count + 10):
return []
var ports_data = []
var data_offset = 10 # Смещение для данных портов
for i in range(ports_count):
if data_offset + i * 2 < data.size():
var port_data = data.decode_u16(data_offset + i * 2)
ports_data.append(port_data)
print("Данные порта %d: 0x%04X" % [i, port_data])
return ports_data
func _get_isa_ports_data(ports: Array) -> Array:
"""Получение данных указанных ISA портов"""
var result = []
for port_address in ports:
result.append(get_isa_port(port_address))
return result
func _set_isa_ports_data(ports: Array, data: Array) -> void:
"""Установка данных для указанных ISA портов"""
for i in range(ports.size()):
if i < data.size():
set_isa_port(ports[i], data[i])
#endregion
#region Вложенный класс SocketUDP
class SocketUDP extends PacketPeerUDP:
"""Расширенный класс UDP сокета с улучшенной обработкой ошибок"""
func send_to(address: String, port: int, data: PackedByteArray) -> int:
"""Отправка данных на указанный адрес и порт"""
set_dest_address(address, port)
var result = put_packet(data)
if result != OK:
push_error('Ошибка отправки: %s - %s:%d' % [error_string(result), address, port])
return result
#endregion
#region Методы управления записанными портами
func clear_written_ports() -> void:
"""Очистка списка записанных портов (ручное управление)"""
_written_isa_ports.clear()
print("Список записанных портов очищен")
func get_written_ports_count() -> int:
"""Получение количества записанных портов"""
return _written_isa_ports.size()
func remove_written_port(port_addr: int) -> bool:
"""Удаление конкретного порта из списка записанных"""
for i in range(_written_isa_ports.size()):
if _written_isa_ports[i][0] == port_addr:
_written_isa_ports.remove_at(i)
print("Порт 0x%04X удален из списка записанных" % port_addr)
return true
return false
#endregion