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 CMD_FLAG_EMS_G: int = 70 # Минимальный размер пакета для валидации 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 logs_manager: LogsRich 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 _input_impulse: int = 0xffff var _input_const: int = 0xffff var _output_impulse: int = 0xffff var _output_const: int = 0xffff var ukp0_data: PackedByteArray var ukp1_data: PackedByteArray signal update_isa_ports(_isa_ports: Dictionary) signal update_socket_status(_sockets_status_bit: int) func _init(name: String, logs_node: RichTextLabel): '''Инициализация UDP broadcast клиента''' status_manager = StatusManager.new(self) _self_name = name initialize_logs_manager(logs_node) _initialize_socket() _initialize_ukp_data() _initialize_timers() _initialize_isa_ports() if not _bind_to_device_address(name): push_error('Не удалось инициализировать устройство: %s' % name) func initialize_logs_manager(logs_node: LogsRich) -> void: '''Инициализация менеджера логов''' logs_manager = logs_node logs_manager.info("Логирование инициализировано для %s" % _self_name) func _initialize_socket() -> void: '''Инициализация UDP сокета''' _socket = SocketUDP.new() if logs_manager: logs_manager.debug("UDP сокет инициализирован") func _initialize_timers() -> void: '''Инициализация таймеров''' # Таймер для широковещательной рассылки (3 раза в секунду) _timer = Timer.new() _timer.wait_time = 1.0 / 5.0 _timer.timeout.connect(_on_broadcast_timeout) if logs_manager: logs_manager.debug("Таймеры инициализированы") 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) if logs_manager: logs_manager.debug("Данные UKP инициализированы") func _initialize_isa_ports() -> void: '''Инициализация ISA портов начальными значениями''' # Инициализируем порты, которые могут запрашиваться set_isa_port(0x0106, 0) # DOU2 статус set_isa_port(0x0108, 0) # DOU3 статус set_isa_port(0x010A, 0) # ATT if logs_manager: logs_manager.debug("ISA порты инициализированы") func _bind_to_device_address(device_name: String) -> bool: '''Привязка к адресу устройства по его имени''' if not ADDRESS_LIST.has(device_name): _log_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: _log_error('Некорректный номер порта: %d' % port) return false var bind_result = _socket.bind(port, address) if bind_result != OK: _log_error('Не удалось привязаться к %s:%d - %s' % [address, port, error_string(bind_result)]) return false _log_info('Успешно привязано к %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: _log_warning('Таймер broadcast уже добавлен в другую сцену') else: _log_debug("Таймеры добавлены в сцену") func start_broadcast() -> void: '''Запуск широковещательной рассылки и опроса сокета''' _timer.start() _log_info('Широковещательная рассылка запущена') func stop_broadcast() -> void: '''Остановка широковещательной рассылки''' _timer.stop() _log_info('Широковещательная рассылка остановлена') func close_udp_unit() -> void: '''Закрытие UDP соединения''' stop_broadcast() _socket.close() _log_info('UDP соединение закрыто') #region Публичные методы для управления состоянием func _handle_set_flag_ems_g(set_flag: bool)->void: '''Установка или сброс 5-го бита (бит 4) в статусе сокетов''' var current_status = get_sockets_status() if set_flag: current_status = current_status | (1 << 4) _log_info('Установлен флаг загрузки EMS-G') set_sockets_status(current_status) func set_sockets_status(status: int) -> void: '''Установка статуса сокетов''' _sockets_status_bit = status emit_signal('update_socket_status', _sockets_status_bit, _self_name) _log_debug("Статус сокетов обновлен: 0x%02X" % status) func get_sockets_status() -> int: '''Получение статуса сокетов''' return _sockets_status_bit func set_dkm_status(status: int) -> void: '''Установка статуса DKM''' _dkm_status_bit = status _log_debug("Статус DKM установлен: 0x%02X" % status) func get_dkm_status() -> int: '''Получение статуса DKM''' return _dkm_status_bit func set_ip_status(status: int)->void: '''Установка статуса ИП''' _dry_contact_status_bit = status _log_debug("Статус ИП установлен: 0x%02X" % status) func get_ip_status()->int: '''Получение статуса ИП''' return _dry_contact_status_bit func set_ems_g_status(in_imp: int, in_const: int, out_imp: int, out_const: int) -> void: '''Установка статуса входов и выходов ЕМС-Г''' _input_impulse = in_imp _input_const = in_const _output_impulse = out_imp _output_const = out_const _log_debug("Статус EMS-G обновлен: IN_C=0x%04X, IN_I=0x%04X, OUT_C=0x%04X, OUT_I=0x%04X" % [in_const, in_imp, out_const, out_imp]) func set_isa_port(port_addr: int, value: int) -> bool: '''Установка значения ISA порта''' if port_addr < 0 or port_addr > 0xFFFF: _log_error('Некорректный адрес порта: 0x%03X' % port_addr) return false _isa_ports[port_addr] = value emit_signal('update_isa_ports', _isa_ports, _self_name) _log_debug("Порт ISA 0x%04X установлен: 0x%04X" % [port_addr, value]) 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: _log_error('Некорректный адрес порта: ' + 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: _log_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''' buffer.encode_u16(EMS_G_DATA_OFFSET, _input_const) buffer.encode_u16(EMS_G_DATA_OFFSET + 6, _input_impulse) buffer.encode_u16(EMS_G_DATA_OFFSET + 12, _output_const) buffer.encode_u16(EMS_G_DATA_OFFSET + 18, _output_impulse) 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: _log_debug('Отправлен немедленный ответ на %s:%d' % [address, port]) else: _log_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: _log_warning('Получен слишком короткий пакет: %d байт' % data.size()) return false return true func _get_command_type(data: PackedByteArray) -> int: '''Получение типа команды из данных пакета''' if data.size() < 6: 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) CMD_FLAG_EMS_G: _handle_set_flag_ems_g(true) _: _log_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) _log_info('Принята команда на чтение 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(): _log_warning('Несоответствие размеров портов и данных от %s:%d' % [address, port]) return # Обновляем или добавляем записанные порты 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 break if not found: _written_isa_ports.append([port_addr, value]) # Добавляем новый порт _log_info('Принята команда на запись в 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 = [] if _get_command_type(data) == CMD_READ_ISA: for i in range(ports_count): var port_address = data.decode_u16(8 + i * 2) ports.append(port_address) elif _get_command_type(data) == CMD_WRITE_ISA: for i in range(ports_count): var port_address = data.decode_u16(8 + i * 4) ports.append(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 * 4 < data.size(): var port = data.decode_u16(8 + i * 4) var port_data = data.decode_u16(data_offset + i * 4) ports_data.append(port_data) _log_debug('Данные порта 0x%02X: 0x%02X' % [port, 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 Методы логирования func _log_info(message: String) -> void: '''Логирование информационного сообщения''' if logs_manager: logs_manager.info("[%s] %s" % [_self_name, message]) else: print("[%s] INFO: %s" % [_self_name, message]) func _log_warning(message: String) -> void: '''Логирование предупреждения''' if logs_manager: logs_manager.warning("[%s] %s" % [_self_name, message]) else: print("[%s] WARNING: %s" % [_self_name, message]) func _log_error(message: String) -> void: '''Логирование ошибки''' if logs_manager: logs_manager.error("[%s] %s" % [_self_name, message]) else: print("[%s] ERROR: %s" % [_self_name, message]) func _log_debug(message: String) -> void: '''Логирование отладочной информации''' if logs_manager: logs_manager.debug("[%s] %s" % [_self_name, message]) else: print("[%s] DEBUG: %s" % [_self_name, message]) #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() _log_info('Список записанных портов очищен') 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) _log_info('Порт 0x%04X удален из списка записанных' % port_addr) return true return false #endregion