commit 110791a97c65cdf3c2b7c5426058cda9d578070c Author: QAMuser Date: Fri Aug 23 08:45:15 2024 +0300 First commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4709183 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Godot 4+ specific ignores +.godot/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..dc9ea49 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..e116dc9 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/ТПО.iml b/.idea/ТПО.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/.idea/ТПО.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/EMS5782.tmp b/EMS5782.tmp new file mode 100644 index 0000000..f748971 --- /dev/null +++ b/EMS5782.tmp @@ -0,0 +1,38 @@ +[gd_scene load_steps=3 format=3 uid="uid://ccp4ypftnrbwq"] + +[ext_resource type="Script" path="res://Main.gd" id="1_pr72k"] +[ext_resource type="Script" path="res://table/table.gd" id="2_1lybo"] + +[node name="Node2D" type="Node2D"] +script = ExtResource("1_pr72k") + +[node name="ColorRect" type="ColorRect" parent="."] +offset_right = 1158.0 +offset_bottom = 655.0 +color = Color(0.313726, 0.227451, 0.290196, 1) +metadata/_edit_lock_ = true + +[node name="Port_ISA" type="Label" parent="."] +offset_left = 37.0 +offset_top = 29.0 +offset_right = 1114.0 +offset_bottom = 52.0 +metadata/_edit_lock_ = true + +[node name="scroll1" type="ScrollContainer" parent="."] +offset_left = 42.0 +offset_top = 72.0 +offset_right = 1135.0 +offset_bottom = 265.0 + +[node name="table1" type="GridContainer" parent="scroll1"] +layout_mode = 2 +script = ExtResource("2_1lybo") + +[node name="Turn_on" type="Button" parent="."] +offset_left = 941.0 +offset_top = 35.0 +offset_right = 1043.0 +offset_bottom = 58.0 +toggle_mode = true +text = "Подключить" diff --git a/Main.gd b/Main.gd new file mode 100644 index 0000000..8ed1895 --- /dev/null +++ b/Main.gd @@ -0,0 +1,216 @@ +extends Node2D + +var TableNode = preload("res://table/node.tscn") + +## Класс для отправки данных в сокет +class Socket extends PacketPeerUDP: + func send_to(addr: String, port: int, data: PackedByteArray): + self.set_dest_address(addr, port) + self.put_packet(data) + +var node_select: Node +var timer = Timer.new() +var soc_unicast: Socket = Socket.new() +var soc_brodcast: Socket = Socket.new() +var unit = yau07.YaU07.new('ЯУ07') +var state: int = 0 # Флаг для машины состояния +var flag_online: bool = false # Флаг для подключения +var flag_interval: bool = false # Флаг для включения непрерывной передачи с интервалом +var ports_wr: Dictionary # Словарь с портами для записи +var port_select: int = Constants.BASE_ADDR # Выбранный порд для чтения/записи +var port: int +var dt: float # Дельта +var address: String +var rx_data: PackedByteArray # Буфер принятый +var addr_receive: String # Адрес обращения к ПРД +var port_receive: int # Порт обращения к ПРД +var ROWS_REGS_DATA: Array = [ + [TableNode, TableNode, TableNode, TableNode, TableNode], + [TableNode, TableNode, TableNode, TableNode, TableNode], + [TableNode, TableNode, TableNode, TableNode, TableNode]] + + +func _ready(): + self.add_child(timer) + timer.connect('timeout', Callable(self, 'on_timer')) + # Таблица 1 + node_select = $scroll1/table1 + draw_tabl(node_select, ROWS_REGS_DATA, ROWS_REGS_DATA.size()) + node_select.set_columns_min_size([250, 150, 150, 200, 150]) + node_select.set_node_text(0, 0, 'НЕТ СВЯЗИ С ЯУ-07') + node_select.set_node_text(1, 0, ' IP-АДРЕС') + node_select.set_node_text(2, 0, '10.1.1.70') + node_select.set_node_text(3, 0, ' IP-ПОРТ') + node_select.set_node_text(4, 0, '50070') + node_select.set_node_text(0, 1, 'ПОРТ ЯЧЕЙКИ (hex)') + node_select.set_node_text(1, 2, 'ИНТЕРВАЛ') + + # Поле для ввода (запись IP-адреса) + var edit_ip = node_select.get_node2(2, 0) + edit_ip.editable = true + edit_ip.placeholder_text = 'Ввод значения' + var edit_port = node_select.get_node2(4, 0) + edit_port.editable = true + edit_port.placeholder_text = 'Ввод значения' + + # Поле для ввода (запись в порт) + var edit_node = node_select.get_node2(2, 1) + edit_node.editable = true + edit_node.placeholder_text = 'Ввод значения' + edit_node.connect('text_submitted', Callable(self, '_on_written_text_submitted')) + + # Кнопка для включения + $Turn_on.connect('toggled', Callable(self, '_on_button_pressed')) + + # Поле для ввода (базовый порт) + var edit_addr = node_select.get_node2(0, 2) + edit_addr.editable = true + edit_addr.placeholder_text = '%x' % port_select + edit_addr.connect('text_submitted', Callable(self, '_on_written_addres')) + + # Поле для ввода (интервал непрерывной записи) + var edit_interval = node_select.get_node2(2, 2) + edit_interval.editable = true + edit_interval.placeholder_text = 'Ввод значения' + edit_interval.connect('text_submitted', Callable(self, '_on_written_interval')) + + # Привязка для принятия широковещательного канала + var rc = soc_brodcast.bind(Constants.BROADCAST_PORT, '*') + if rc != OK: + print('Ошибка: Неудачная привязка широковещательного адреса') + + ports_wr[port_select] = 0 + unit.connect('line_changed', Callable(self, 'on_line_changed')) + unit.connect('command_fail', Callable(self, 'on_command_fail')) + unit.connect('command_done', Callable(self, 'on_command_done')) + + +## Рисует таблицу +func draw_tabl(tbl, row: Array, count_row: int): + for i_row in count_row: + tbl.add_row(row[i_row]) + + +func poll_receive(sock: Socket, _unit, addr: String, _port: int) -> bool: ## Приёмник + while sock.get_available_packet_count() > 0: + rx_data = sock.get_packet() + addr_receive = sock.get_packet_ip() + port_receive = sock.get_packet_port() + if (addr == addr_receive) and (port == port_receive): + unit.parse(rx_data) + return false + + +func _process(delta): + if flag_online: + if flag_interval == false: + poll_receive(soc_brodcast, unit, address, port) + var fl_done = unit.cmd_state == unit.CmdState.DONE + if state == 1 and fl_done: ## Чтение из ИСА + unit.send_isa(unit.CmdCode.READ_ISA, [port_select]) + node_select.set_node_text(3, 1, 'ПРОЧИТАНО ИЗ %x' % port_select) + state = 2 + elif state == 2 and fl_done: ## Запись в ИСА + unit.send_isa(unit.CmdCode.WRITE_ISA, [port_select, ports_wr[port_select]]) + + if unit.isa_ports.has(port_select): + node_select.set_node_text(4, 1, '%x' % unit.isa_ports[port_select]) + var isa: Array + for i in unit.isa_ports: + isa.append('%x: %x' % [i, unit.isa_ports[i]]) + node_select.set_node_text(1, 1, 'ЗАПИСАТЬ В %x' % port_select) + $ISA.text = 'ISA: ' + str(isa) + state = 1 + + var rc = unit.process(delta) + if rc == Error.OK: + soc_unicast.send_to(address, port, unit.tx_data.slice(0, unit.tx_len)) + dt = delta + + +func on_timer(): + if flag_online: + if flag_interval == true: + poll_receive(soc_brodcast, unit, address, port) + var fl_done = unit.cmd_state == unit.CmdState.DONE + if state == 1 and fl_done: ## Чтение из ИСА + unit.send_isa(unit.CmdCode.READ_ISA, [port_select, ports_wr[port_select]]) + state = 2 + elif state == 2 and fl_done: ## Запись в ИСА + unit.send_isa(unit.CmdCode.WRITE_ISA, [port_select, ports_wr[port_select]]) + + if unit.isa_ports.has(port_select): + node_select.set_node_text(4, 1, '%x' % unit.isa_ports[port_select]) + state = 1 + var rc = unit.process(dt) + if rc == Error.OK: + soc_unicast.send_to(address, port, unit.tx_data.slice(0, unit.tx_len)) + + +func on_line_changed(_unit): + print_debug() + + +## Ессли отсутствует подключение к ячейке ЯУ-07 +func on_command_fail(_unit): + var line_yau07_control = node_select.get_node2(0, 0) + line_yau07_control.text = 'НЕТ СВЯЗИ С ЯУ-07' + line_yau07_control.modulate = Color(1, 0, 0, 1) + flag_online = false + soc_unicast = Socket.new() + + +## Индикация чтения портов +func on_command_done(_unit): + if flag_online: + var line_yau07_control = node_select.get_node2(0, 0) + line_yau07_control.text = 'ЯУ-07 НА СВЯЗИ' + line_yau07_control.modulate = Color(0, 1, 0, 1) + +## Выставить адресс для записи +func _on_written_addres(write_addres): + port_select = write_addres.hex_to_int() + ports_wr[port_select] = 0 + node_select.set_node_text(0, 2, '%x' % port_select) + + +## Запись числа порт +func _on_written_text_submitted(write_addres_data): + var is_hex = true + for chr in write_addres_data: + if chr.to_upper() < '0' or chr.to_upper() > 'F': + is_hex = false + break + ports_wr[port_select] = write_addres_data.hex_to_int() if is_hex else 0xffff + state = 2 # Запись + + +func _on_written_interval(write_interval): + timer.start(float(write_interval)) + flag_interval = true + var light_interval = node_select.get_node2(1, 2) + light_interval.modulate = Color(0, 1, 0, 1) + + +## Подключение +func _on_button_pressed(turn_on): + if turn_on: + address = node_select.get_node2(2,0).text + port = int(node_select.get_node2(4,0).text) + var rc = soc_unicast.bind(Constants.UNICAST_PORT, Constants.UNICAST_ADDRESS) + if rc != OK: + print('Ошибка: неудачная привязка адреса: ', address) + soc_unicast = Socket.new() + flag_online = false + else: + state = 1 # Чтение + flag_online = true + else: + var line_yau07_control = node_select.get_node2(0, 0) + line_yau07_control.text = 'НЕТ СВЯЗИ С ЯУ-07' + line_yau07_control.modulate = Color(1, 0, 0, 1) + var light_interval = node_select.get_node2(1, 2) + light_interval.modulate = Color(0.5, 0.5, 0.5, 1) + soc_unicast = Socket.new() + flag_online = false + flag_interval = false diff --git a/Plata-check.tscn b/Plata-check.tscn new file mode 100644 index 0000000..ef41ef4 --- /dev/null +++ b/Plata-check.tscn @@ -0,0 +1,50 @@ +[gd_scene load_steps=5 format=3 uid="uid://ccp4ypftnrbwq"] + +[ext_resource type="Script" path="res://Main.gd" id="1_8v2wa"] +[ext_resource type="Script" path="res://table/table.gd" id="2_ri1qb"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_so1no"] +bg_color = Color(0, 0, 0, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_jbuuq"] +bg_color = Color(0.0901961, 0.52549, 0.0588235, 1) + +[node name="Node2D" type="Node2D"] +script = ExtResource("1_8v2wa") + +[node name="Background" type="ColorRect" parent="."] +offset_right = 1158.0 +offset_bottom = 655.0 +color = Color(0.313726, 0.227451, 0.290196, 1) +metadata/_edit_lock_ = true + +[node name="scroll1" type="ScrollContainer" parent="."] +offset_left = 42.0 +offset_top = 72.0 +offset_right = 1135.0 +offset_bottom = 265.0 +metadata/_edit_lock_ = true + +[node name="table1" type="GridContainer" parent="scroll1"] +layout_mode = 2 +script = ExtResource("2_ri1qb") +metadata/_edit_lock_ = true + +[node name="Turn_on" type="Button" parent="."] +offset_left = 963.0 +offset_top = 72.0 +offset_right = 1088.0 +offset_bottom = 96.0 +theme_override_font_sizes/font_size = 17 +theme_override_styles/normal = SubResource("StyleBoxFlat_so1no") +theme_override_styles/pressed = SubResource("StyleBoxFlat_jbuuq") +toggle_mode = true +text = "Подключить" +metadata/_edit_lock_ = true + +[node name="ISA" type="Label" parent="."] +offset_left = 46.0 +offset_top = 179.0 +offset_right = 999.0 +offset_bottom = 202.0 +metadata/_edit_lock_ = true diff --git a/constants.gd b/constants.gd new file mode 100644 index 0000000..e8295ff --- /dev/null +++ b/constants.gd @@ -0,0 +1,6 @@ +extends Node + +const UNICAST_PORT: int = 50080 +const UNICAST_ADDRESS: String = '10.1.1.14' +const BROADCAST_PORT: int = 50000 +const BASE_ADDR = 0x100 diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..b370ceb --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..6640247 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://diowevlc7bywf" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..796a9f8 --- /dev/null +++ b/project.godot @@ -0,0 +1,26 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Plata-check.exe" +run/main_scene="res://Plata-check.tscn" +config/features=PackedStringArray("4.2", "GL Compatibility") +config/icon="res://icon.svg" + +[autoload] + +yau07="*res://yau07.gd" +Constants="*res://constants.gd" + +[rendering] + +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" diff --git a/table/header.tscn b/table/header.tscn new file mode 100644 index 0000000..76a059d --- /dev/null +++ b/table/header.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3 uid="uid://ch14g46swx74h"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_2thpi"] +bg_color = Color(0.8, 0.403922, 0.27451, 1) + +[node name="heder" type="Label"] +offset_right = 128.0 +offset_bottom = 23.0 +theme_override_colors/font_color = Color(0, 0, 0, 1) +theme_override_font_sizes/font_size = 15 +theme_override_styles/normal = SubResource("StyleBoxFlat_2thpi") +horizontal_alignment = 1 diff --git a/table/nine-patch.png b/table/nine-patch.png new file mode 100644 index 0000000..1649a7f Binary files /dev/null and b/table/nine-patch.png differ diff --git a/table/nine-patch.png.import b/table/nine-patch.png.import new file mode 100644 index 0000000..2d8c80a --- /dev/null +++ b/table/nine-patch.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bw3yv3smvaxqn" +path="res://.godot/imported/nine-patch.png-798f7950fb3126b4e1ed0c39d9e4beba.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://table/nine-patch.png" +dest_files=["res://.godot/imported/nine-patch.png-798f7950fb3126b4e1ed0c39d9e4beba.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/table/node.tscn b/table/node.tscn new file mode 100644 index 0000000..a492ba9 --- /dev/null +++ b/table/node.tscn @@ -0,0 +1,48 @@ +[gd_scene load_steps=3 format=3 uid="uid://cef6gtuc60ggg"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_hrhif"] +bg_color = Color(0, 0, 0, 1) + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_q1ixs"] +bg_color = Color(0, 0, 0, 1) + +[node name="Label" type="LineEdit"] +texture_filter = 2 +texture_repeat = 2 +clip_contents = true +layout_direction = 2 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +offset_right = -279.0 +offset_bottom = -831.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +theme_override_colors/font_uneditable_color = Color(1, 1, 1, 1) +theme_override_font_sizes/font_size = 17 +theme_override_styles/normal = SubResource("StyleBoxFlat_hrhif") +theme_override_styles/read_only = SubResource("StyleBoxFlat_q1ixs") +alignment = 1 +editable = false +caret_blink = true +caret_blink_interval = 0.5 + +[node name="Label" type="Label" parent="."] +visible = false +layout_mode = 0 +offset_right = 40.0 +offset_bottom = 23.0 + +[node name="Label" type="Label" parent="Label"] +visible = false +layout_mode = 0 +offset_right = 40.0 +offset_bottom = 23.0 + +[node name="CheckButton" type="CheckButton" parent="Label"] +visible = false +layout_mode = 0 +offset_right = 44.0 +offset_bottom = 24.0 diff --git a/table/node_list.tscn b/table/node_list.tscn new file mode 100644 index 0000000..50285fe --- /dev/null +++ b/table/node_list.tscn @@ -0,0 +1,10 @@ +[gd_scene load_steps=2 format=3 uid="uid://mxm8nthir8h4"] + +[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ddnjm"] +bg_color = Color(0, 0, 0, 1) + +[node name="OptionButton" type="OptionButton"] +offset_right = 170.0 +offset_bottom = 20.0 +theme_override_styles/normal = SubResource("StyleBoxFlat_ddnjm") +alignment = 1 diff --git a/table/selector.tscn b/table/selector.tscn new file mode 100644 index 0000000..f522b3f --- /dev/null +++ b/table/selector.tscn @@ -0,0 +1,15 @@ +[gd_scene load_steps=2 format=3 uid="uid://pd3fcx55duj2"] + +[ext_resource type="Texture2D" uid="uid://bw3yv3smvaxqn" path="res://table/nine-patch.png" id="1_jtmfn"] + +[node name="NinePatchRect" type="NinePatchRect"] +offset_right = 17.0 +offset_bottom = 16.0 +texture = ExtResource("1_jtmfn") +draw_center = false +patch_margin_left = 8 +patch_margin_top = 8 +patch_margin_right = 8 +patch_margin_bottom = 8 +axis_stretch_horizontal = 1 +axis_stretch_vertical = 1 diff --git a/table/table.gd b/table/table.gd new file mode 100644 index 0000000..49ad6c1 --- /dev/null +++ b/table/table.gd @@ -0,0 +1,682 @@ +class_name GDTable extends GridContainer + +## Виджет "Таблица" + +@export var node_gap: = Vector2(0.0, 0.0) ## Зазор между ячейками, который невозможно вычислить или получить +@export var selector_color: = Color(1.0, 1.0, 1.0, 1.0) ## Окрашивающий цвет для селектора выбранного ряда +@export var selector_key_down: Key = KEY_DOWN ## Клавиша увеличения индекса выбранного ряда +@export var selector_key_up: Key = KEY_UP ## Клавиша уменьшения индекса выбранного ряда +@export_file var node_type_file: = 'res://table/node.tscn': get = get_node_scene, set = load_node_scene ## Сцена ячейки устанавливаемая по умолчанию +@export_file var selector_type_file: = 'res://table/selector.tscn': set = load_selector_scene ## Сцена селектора для выбранного ряда + +const HEAD_PREFIX = 'HEAD_' ## Префикс ключа заголовка таблицы +const INDEX_FORMAT = '%02d_%02d' ## Формат индекса ключа +const MAX_INDEX = 99 ## Максимальное значение индекса для колонки и строки. Связано с форматом индекса ключа +const NODE_PREFIX = 'NODE_' ## Префикс ключа ячейки таблицы +const SLCT_PREFIX = 'SLCT_' ## Префикс ключа индикатора выбранного ряда таблицы + +const HEAD_KEY_FORMAT = HEAD_PREFIX + INDEX_FORMAT ## Формат ключа заголовка +const NODE_KEY_FORMAT = NODE_PREFIX + INDEX_FORMAT ## Формат ключа ячейки +const SELECTOR_KEY_FORMAT = SLCT_PREFIX + INDEX_FORMAT ## Формат ключа индикаторов выбранного ряда +const META_USER_DATA = 'user_data' ## Имя свойства для сохранения данных пользователя +const META_CALLABLE = 'callable' ## Имя свойства для сохранения вызываемого +const META_EVENT_NAME = 'event_name' ## Имя свойства для сохранения названия события + +var header: GridContainer ## Заголовок таблицы +var in_focus: = false ## Фокус пользовательского ввода +var nodes: = {} ## Все ячейки таблицы и дополнительные элементы +var columns_min_size = [] ## Ширина каждой колонки +var ScnNode: PackedScene ## Прототип ячейки таблицы +var ScnSelector: PackedScene ## Прототип индикатора выбранного ряда +var selector_color_proc: Callable ## Ссылка на процедуру генератора цвета индикатора выбранного ряда +var selector_size_proc: Callable ## Ссылка на процедуру генератора размера индикатора выбранного ряда + +## Вызывается при нажатии кнопкой мыши на строку. +## Активируемый ползователем элемент ряда должен вызывать +## сигнал [b]focus_entered[/b], что бы сигнал [b]row_pressed[/b] работал.[br] +## [param i_row] - Индекс ряда в котором пользователь активировал элемент.[br] +## [i]Godot 4.2: [/i][b] Label[/b] [i] - не вызывает сигнал[/i] [b]focus_entered[/b]. +signal row_pressed(i_row: int) + +## Вызывается при изменении выбранного ряда.[br] +## [param i_row] - Текущий выбранный ряд. +signal selected_changed(i_row: int) + +var scroll: Control +var scroll_adj: = Vector2(0, 0) + + +func get_node_scene(): return node_type_file +func assert_meta(i_col: int, i_row: int, pname: String, node): assert(node.has_meta(pname), 'ошибка: нет мета-свойства \"%s\" у ячейки колонка:%d ряд:%d' % [pname, i_col, i_row]) +func get_node_user_data(i_col: int, i_row: int): return get_node_meta(i_col, i_row, META_USER_DATA) +func set_node_user_data(i_col: int, i_row: int, val) -> void: set_node_meta(i_col, i_row, META_USER_DATA, val) +func defaultselector_color_proc(_i_row: int): return selector_color + + +## Закрепляет заголовок таблицы.[br] +## [param scrl] - Прокрутка родитель таблицы.[br] +## [param prnt] - Родитель прокрутки.[br] +func pin_header(scrl: ScrollContainer, prnt: Control): + scrl.remove_child(header) + prnt.add_child(header) + var v_separation = get_theme_constant('v_separation') + header.position = scrl.position - Vector2(0, header.size.y + v_separation) + scroll = scrl + scroll_adj = scrl.position + + +## Перемещает указатель на одну строку вверх +func selector_up(): + var i_rows: = [] + get_selected_rows(i_rows) + for i in i_rows: + set_row_selected(i, false) + var rows_count: = get_rows_count() + if not (i_rows.size() and (rows_count > 1)): + return + var i_row = i_rows[0] + i_row -= 1 + if i_row < 0: + i_row = rows_count - 1 + set_row_selected(i_row, true) + var node = get_node2(0, i_row) + if scroll is ScrollContainer: + scroll.ensure_control_visible(node) + emit_signal('selected_changed', i_row) + + +## Перемещает указатель на одну строку вниз +func selector_down(): + var i_rows: = [] + get_selected_rows(i_rows) + for i in i_rows: + set_row_selected(i, false) + var rows_count: = get_rows_count() + if not (i_rows.size() and (rows_count > 1)): + return + var i_row = i_rows[0] + i_row += 1 + i_row %= rows_count + set_row_selected(i_row, true) + var node = get_node2(0, i_row) + if scroll is ScrollContainer: + scroll.ensure_control_visible(node) + emit_signal('selected_changed', i_row) + + +## Делает ряд [param i_row] видимым. +func ensure_row_visible(i_row: int): + assert(scroll is ScrollContainer, 'Таблица должна быть дочерней к ScrollContainer') + var key: = get_node_key(NODE_KEY_FORMAT, 0, i_row) + var node = nodes[key] + scroll.ensure_control_visible(node) + + +## Устанавливает выравнивание из массива [param alignments] для всех колонок. +func set_columns_alignments(alignments: Array): + assert(columns == alignments.size()) + var rows_count: = get_rows_count() + for i_row in rows_count: + for i_col in alignments.size(): + set_node_alligment(i_col, i_row, alignments[i_col]) + + +## +func load_selector_scene(file_name: String): + selector_type_file = file_name + ScnSelector = load(file_name) + + +## +func load_node_scene(file_name: String): + node_type_file = file_name + ScnNode = load(file_name) + + +## +func _ready(): + ScnSelector = load(selector_type_file) + ScnNode = load(node_type_file) + assert(ScnSelector is PackedScene) + assert(ScnNode is PackedScene) + + +## Выравнивает по нижнему левому углу. Используется для выравнивания по заголовку. +func align_to_left_bottompos(hdr: Control, gap: = Vector2(0, 0)): + position.x = hdr.position.x + gap.x + position.y = hdr.position.y + hdr.size.y + gap.y + + +## Устанавливает пользовательские данные для ячейки.[br] +## [param i_col] - Индекс колонки.[br] +## [param i_row] - Индекс ряда.[br] +## [param pname] - Имя пользовательских данных.[br] +## [param val] - Значение пользовательских данных.[br] +func set_node_meta(i_col: int, i_row: int, pname: String, val) -> void: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + var node = nodes[key] + node.set_meta(pname, val) + + +## Возвращает пользовательские данные для ячейки.[br] +## [param i_col] - Индекс колонки.[br] +## [param i_row] - Индекс ряда.[br] +## [param pname] - Имя пользовательских данных.[br] +func get_node_meta(i_col: int, i_row: int, pname: String): + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + var node = nodes[key] + assert_meta(i_col, i_row, pname, node) + return node.get_meta(pname) + + +## Устанавливает свойство ячейки.[br] +## [param i_col] - Индекс колонки.[br] +## [param i_row] - Индекс ряда.[br] +## [param pname] - Имя свойства.[br] +## [param val] - Значение свойства.[br] +## [i]Свойство[/i] [param pname] [i]ячейки должно существовать.[/i] +func set_node_property(i_col: int, i_row: int, pname: String, val) -> void: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + var node = nodes[key] + node.set(pname, val) + + +## Возвращает значение поля ячейки.[br] +## [param pname] - Имя поля.[br] +## [param i_col] - Индекс колонки.[br] +## [param i_row] - Индекс ряда. +func get_node_property(i_col: int, i_row: int, pname: String): + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + var node = nodes[key] + return node.get(pname) + + +## Возвращает ключ для существующей ячейки. Если ячейки с заданными индексами нет, +## вызывает исключение.[br] +## [param i_col] - Индекс колонки.[br] +## [param i_row] - Индекс ряда +func get_node_key(format: String, i_col: int, i_row: int) -> String: + var key: String = format % [i_row, i_col] + assert(nodes.has(key), 'ошибка: нет такой ячейки - колонка:%d ряд:%d' % [i_col, i_row]) + return key + + +## Возвращает массив с индексом ячейки:[br] +## [b][0][/b] - индекс колонки,[br] +## [b][1][/b] - индекс строки. +func get_key_indexes(key: String) -> Array: + var tokens: = key.split('_') + assert(tokens.size() == 3, 'ошибка: неверный ключ: \"%s\"' % key) + return [int(tokens[2]), int(tokens[1])] + + +## +func set_columns_min_size(sizes: Array) -> void: + assert((columns == 1) or (columns == sizes.size())) + columns = sizes.size() + var rows_count: = get_rows_count() + columns_min_size.clear() + for sz in sizes: + columns_min_size.append(sz) + for i_row in rows_count: + set_nodes_min_size(i_row, sizes, NODE_KEY_FORMAT) + if nodes.has(HEAD_KEY_FORMAT % [0, 0]): + set_nodes_min_size(0, sizes, HEAD_KEY_FORMAT) + + +## +func set_columns_aligment(alignments: Array) -> void: + var rows_count: = get_rows_count() + for i_row in rows_count: + for i_col in alignments.size(): + set_node_alligment(i_col, i_row, alignments[i_col]) + + +## +func _input(event) -> void: + if not is_visible_in_tree(): return + if event is InputEventKey: + if event.pressed and in_focus: + if event.keycode == selector_key_up: + selector_up() + elif event.keycode == selector_key_down: + selector_down() + elif event is InputEventMouseButton: + if not event.pressed: + return + if check_event_pos(event.position, scroll): + if not in_focus: + in_focus = true + emit_signal('focus_entered') + elif not check_event_pos(event.position, scroll): + if in_focus: + in_focus = false + emit_signal('focus_exited') + + +## +func _init() -> void: + selector_color_proc = Callable(self, 'defaultselector_color_proc') + assert(selector_color_proc.is_valid()) + selector_size_proc = Callable(self, 'default_selector_size_proc') + assert(selector_size_proc.is_valid()) + scroll = self + + +## +func check_event_pos(event_pos: Vector2, obj: Control): + return \ + (event_pos.x > obj.position.x) \ + and (event_pos.x < (obj.position.x + obj.size.x)) \ + and (event_pos.y > obj.position.y) \ + and (event_pos.y < (obj.position.y + obj.size.y)) + + +## +func setselector_color_proc(instance: Object, function: String) -> void: + selector_color_proc = Callable(instance, function) + assert(selector_color_proc.is_valid()) + + +## +func set_selector_size_proc(instance: Object, function: String) -> void: + selector_size_proc = Callable(instance, function) + assert(selector_size_proc.is_valid()) + + +## [param i_row] - Индекс ряда +func default_selector_size_proc(i_row: int, rect: Array) -> void: + get_row_rect(i_row, rect) + rect[0] += position.x - node_gap.x / 2.0 + rect[1] += position.y - node_gap.y / 2.0 + rect[2] += node_gap.x + rect[3] += node_gap.y + + +## +func clear_rows_selected(): + var rows_count: = get_rows_count() + for i_row in rows_count: + set_row_selected(i_row, false) + + +## [param i_row] - Индекс ряда. +func set_row_selected(i_row: int, selected: bool = true) -> void: + call_deferred('_set_row_selected', i_row, selected) + + +## [param i_row] - Индекс ряда. +func _set_row_selected(i_row: int, selected: bool = true) -> void: + var selector_key: = SELECTOR_KEY_FORMAT % [i_row, 0] + var node_key: = NODE_KEY_FORMAT % [i_row, 0] + if not nodes.has(node_key): + push_error('ошибка: нет такого ряда: %d' % i_row) + return + if nodes.has(selector_key) and (not selected): + var node = nodes[node_key] + var selector = nodes[selector_key] + node.remove_child(selector) + selector.queue_free() + nodes.erase(selector_key) + elif (not nodes.has(selector_key)) and selected: + var selector = ScnSelector.instantiate() + var rect: = [0, 0, 0, 0] + selector_size_proc.call(i_row, rect) + selector.size.x = rect[2] + selector.size.y = rect[3] + selector.modulate = selector_color_proc.call(i_row) + selector.z_index = z_index + 1 + nodes[selector_key] = selector + var node = nodes[node_key] + node.add_child(selector) + + +## [param i_row] - Индекс ряда. +func is_row_selected(i_row: int) -> bool: + var key: = SELECTOR_KEY_FORMAT % [i_row, 0] + return nodes.has(key) + + +## [param i_row] - Индексы рядов. +func get_selected_rows(i_rows: Array) -> void: + for key in nodes.keys(): + if SLCT_PREFIX in key: + var indexes = get_key_indexes(key) + i_rows.append(indexes[1]) + + +## Возвращает количество рядов +func get_rows_count() -> int: + var nodes_count: int = 0 + for key in nodes: + nodes_count += int(NODE_PREFIX in key) + return nodes_count / columns + + +## [param i_col] - Индекс колонки. [param i_row] - Индекс ряда. +func get_node2(i_col: int, i_row: int) -> Node: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + return nodes[key] + + +## Добавляет ряд. [param row_types] - Массив типов ячеек ряда +func add_row(row_types: Array) -> void: + assert(row_types.size() < MAX_INDEX, 'ошибка: слишком много колонок, больше %d' % (MAX_INDEX + 1)) + if columns == 1: + set_columns(row_types.size()) + else: + assert(columns == row_types.size(), 'ошибка: %s количество колонок установлено ранее - %d, но требуется %d' % [self, columns, row_types.size()]) + var i_row: int = get_rows_count() + assert(i_row < MAX_INDEX, 'ошибка: слишком много рядов, больше %d' % (MAX_INDEX + 1)) + var allow_set_size = columns_min_size.size() >= row_types.size() + for i_col in columns: + var node_type = row_types[i_col] + var node = node_type.instantiate() + assert(node.name == node.name, 'ошибка: экземпляр не имеет поля \"name\"') + var node_key: String = NODE_KEY_FORMAT % [i_row, i_col] + node.name = node_key + add_child(node) + if allow_set_size: + node.custom_minimum_size.x = columns_min_size[i_col] + node.set_deferred('size.x', columns_min_size[i_col]) + nodes[node_key] = node + var conn = Callable(self, '_on_focus_entered').bind(node) + node.connect('focus_entered', conn) + set_node_meta(i_col, i_row, 'callable_focus_entered', conn) + + +## +func _on_focus_entered(node): + var indexes = get_key_indexes(node.name) + emit_signal('row_pressed', indexes[1]) + + +## +func set_header(header_types: Array): + assert(header_types.size() < MAX_INDEX, 'ошибка: количество колонок в заголовке: %d больше допустимого: %d' % [header_types.size(), MAX_INDEX + 1]) + if columns == 1: + set_columns(header_types.size()) + else: + assert(columns == header_types.size(), 'ошибка: количество колонок в заголовке %d, но ранее установлено: %d' % [header_types.size(), columns]) + var do_adj: = false + if not header: + header = GridContainer.new() + var parent = scroll.get_parent() + parent.add_child(header) + header.position = position + header.columns = header_types.size() + do_adj = true + else: + var childrens = header.get_children() + for node in childrens: + childrens.erase(node.name) + childrens.remove_child(node) + node.queue_free() + for i_col in columns: + var node = header_types[i_col].instantiate() + var key: String = HEAD_KEY_FORMAT % [0, i_col] + node.name = key + nodes[key] = node + header.add_child(node) + if do_adj: + var rect = [0, 0, 0, 0] + get_node_rect(0, 0, rect, HEAD_KEY_FORMAT) + position.y += rect[3] + + +## +func set_header_text(headers: Array): + assert(columns == headers.size()) + set_row_text(0, headers, 0, HEAD_KEY_FORMAT) + + +## [param i_row] - индекс ряда. +func remove_row(i_row: int): + var sel_key: String = SELECTOR_KEY_FORMAT % [i_row, 0] + + if nodes.has(sel_key): + var selector = nodes[sel_key] + var prnt = selector.get_parent() + prnt.remove_child(selector) + selector.queue_free() + nodes.erase(sel_key) + + for i_col in columns: + + var node_key: String = NODE_KEY_FORMAT % [i_row, i_col] + var node = nodes[node_key] + + var conn = get_node_meta(i_col, i_row, 'callable_focus_entered') + node.disconnect('focus_entered', conn) + + if node.has_meta(META_CALLABLE): + conn = node.get_meta(META_CALLABLE) + var event_name = node.get_meta(META_EVENT_NAME) + node.disconnect(event_name, conn) + + remove_child(node) + nodes.erase(node_key) + node.queue_free() + rehash() + + +## +func next_index(indexes: Array) -> void: + indexes[0].x += 1 + if indexes[0].x == columns: + indexes[0].x = 0 + indexes[0].y += 1 + + +## +func swap_nodes(nodes_copy, node_key_format, i0, i1) -> void: + var key0 = node_key_format % [i0.y, i0.x] + var key1 = node_key_format % [i1.y, i1.x] + nodes_copy[key1] = nodes[key0] + nodes_copy[key1].name = key1 + + +## Переназначает ключи в соответствии с размерами таблицы. +func rehash(): + var i_node: = [Vector2i()] + var i_hdr: = [Vector2i()] + var i_slct: = [Vector2i()] + var nodes_copy = {} + for key in nodes: + var iv = get_key_indexes(key) + var i0 = Vector2(iv[0], iv[1]) + if HEAD_PREFIX in key: + swap_nodes(nodes_copy, HEAD_KEY_FORMAT, i0, i_hdr[0]) + next_index(i_hdr) + elif NODE_PREFIX in key: + swap_nodes(nodes_copy, NODE_KEY_FORMAT, i0, i_node[0]) + next_index(i_node) + elif SLCT_PREFIX in key: + swap_nodes(nodes_copy, SELECTOR_KEY_FORMAT, i0, i_slct[0]) + next_index(i_slct) + nodes.clear() + nodes.merge(nodes_copy) + + +## Удаляет все строки таблицы +func clear_rows() -> void: + var rows_count: int = get_rows_count() + for i_row in range(rows_count - 1, -1, -1): + remove_row(i_row) + + +## Настраевает обратные вызовы для колонок.[br] +## [param dest_node] - Получатель события[br] +## [param event_name] - Имя события[br] +## [param callback] - Имя функции получателя события[br] +## [param clmns] - Индексы колонок[br] +## [param user_data] - Аргументы функции получателя события[br] +func connect_columns(dest_node: Node, event_name: String, callback: String, clmns: Array, user_data: Array = []) -> void: + var rows_count = get_rows_count() + for i_row in rows_count: + for i_col in clmns: + assert(i_col >= 0 and i_col < columns, 'ошибка: нет такой колонки: %d' % i_col) + var node = get_node2(i_col, i_row) + if node.has_meta(META_CALLABLE): + var conn = node.get_meta(META_CALLABLE) + node.disconnect(event_name, conn) + var conn = Callable(dest_node, callback).bind([node] + user_data) + var rc = node.connect(event_name, conn) + if rc == Error.OK: + node.set_meta(META_CALLABLE, conn) + node.set_meta(META_EVENT_NAME, event_name) + else: + var es = error_string(rc) + push_error('ошибка: событие \"%s\" не может быть подключено к %s: %s' % [event_name, node, es]) + + +## +func set_rows_text(rows: Array) -> void: + for i_row in rows.size(): + for i_col in columns: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + var node = nodes[key] + node.text = rows[i_row][i_col] + + +## [param i_row] - Индекс ряда. +func set_nodes_min_size(i_row: int, sizes: Array, key_format: String = NODE_KEY_FORMAT) -> void: + columns_min_size.clear() + for sz in sizes: + columns_min_size.append(sz) + for i_col in columns: + var key: = get_node_key(key_format, i_col, i_row) + var node = nodes[key] + node.custom_minimum_size.x = sizes[i_col] + node.set_deferred('size.x', sizes[i_col]) + + +## [param i_col] - Индекс колонки. [param i_row] - Индекс ряда. +func set_node_text(i_col: int, i_row: int, val: String) -> void: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + nodes[key].text = val + + +## +func set_rows_state(rows: Array) -> void: + for i_row in rows.size(): + for i_col in columns: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + nodes[key].button_pressed = rows[i_row][i_col] + + +## Устанавливает состояние ячейки. +## [param i_col] - Индекс колонки.[br] +## [param i_row] - Индекс ряда.[br] +## [param val] - Новое состояние.[br] +## [i]Ячейка должна иметь свойство[/i] [param button_pressed] +func set_node_state(i_col: int, i_row: int, val: bool) -> void: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + nodes[key].button_pressed = val + + +## [param i_col] - Индекс колонки.[br] +## [param i_row] - Индекс ряда. +func set_node_frame(i_col: int, i_row: int, node_name: String, val: int) -> void: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + var node = nodes[key] + var sprite = node.get_node(node_name) + sprite.frame = val + + +## Разрешает редактирование ячейки. Ячейка должна иметь свойство [param editable].[br] +## [param i_col] - Индекс колонки.[br] +## [param i_row] - Индекс ряда. +func set_node_editable(i_col: int, i_row: int, val: bool) -> void: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + nodes[key].editable = val + + +## Разрешает редактирование ряда. Ячейки ряда должны иметь свойство [param editable].[br] +## [param i_row] - Индекс ряда. +func set_row_editable(i_row: int, val: bool) -> void: + for i_col in columns: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + nodes[key].editable = val + + +## [param i_col] - Индекс колонки.[br] +## [param i_row] - Индекс ряда. +func set_node_alligment(i_col: int, i_row: int, val: int) -> void: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + if 'horizontal_alignment' in nodes[key]: + nodes[key].horizontal_alignment = val + elif 'alignment' in nodes[key]: + nodes[key].alignment = val + + +## [param i_row] - Индекс ряда. +func set_row_alligments(i_row: int, aligns: Array) -> void: + for i_col in columns: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + nodes[key].align = aligns[i_col] + + +## [param i_row] - Индекс ряда. +func get_row_text(i_row: int) -> Array: + var row: = [] + for i_col in columns: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + row.append(nodes[key].text) + return row + + +## Устанавливает текст ячеек ряда.[br] +## [param i_row] - Индекс ряда.[br] +## [param row] - Массив новых значений текста (поле элемента [param text]).[br] +## [param i_begin] - Начальный индекс колонки начиная с которого установить новые значения.[br] +## [i]Ячейки ряда должны иметь свойство[/i] [param text] +func set_row_text(i_row: int, row: Array, i_begin: int = 0, key_format: String = NODE_KEY_FORMAT) -> void: + assert((i_begin >= 0) and (i_begin < columns)) + assert((columns - i_begin) >= row.size()) + for i_col in row.size(): + var key: = get_node_key(key_format, i_col + i_begin, i_row) + nodes[key].text = row[i_col] + + +## Возвращает все строки таблицы в двумерном массиве +func get_rows_text() -> Array: + var rows_count: = get_rows_count() + var rows: = [] + for i_row in rows_count: + var row = get_row_text(i_row) + rows.append(row) + return rows + + +## Возвращает массив ячеек из ряда [param i_row] +func get_row(i_row: int) -> Array: + var row: = [] + for i_col in columns: + var key: = get_node_key(NODE_KEY_FORMAT, i_col, i_row) + row.append(nodes[key]) + return row + + +## Возвращает прямоугольник [param rect] занимаемый ячейкой в ряду [param i_row] и колонке [param i_col].[br] +## [param key_format] - Формат ключа ячейки +func get_node_rect(i_col: int, i_row: int, rect: Array, key_format: String = NODE_KEY_FORMAT) -> void: + assert(rect.size() >= 4, 'ошибка: мало элементов для возврата результата') + var key: = get_node_key(key_format, i_col, i_row) + var cell = nodes[key] + rect[0] = cell.position.x + rect[1] = cell.position.y + rect[2] = cell.size.x + rect[3] = cell.size.y + + +## Возвращает прямоугольник [param rect] занимаемый рядом [param i_row].[br] +## [param key_format] - Формат ключа ячейки +func get_row_rect(i_row: int, rect: Array, key_format: String = NODE_KEY_FORMAT) -> void: + assert(rect.size() >= 4, 'ошибка: мало элементов для возврата результата') + var cell0 = nodes[get_node_key(key_format, 0, i_row)] + var cell1 = nodes[get_node_key(key_format, columns - 1, i_row)] + rect[0] = cell0.position.x + scroll_adj.x + rect[1] = cell0.position.y + scroll_adj.y + rect[2] = (cell1.position.x - cell0.position.x) + cell1.size.x + rect[3] = cell0.size.y diff --git a/tools.gd b/tools.gd new file mode 100644 index 0000000..2df164b --- /dev/null +++ b/tools.gd @@ -0,0 +1,91 @@ +extends Node + +## Простые часто используемые функции + + +## Возвращает true, если val число, false - если иначе +func type_is_num(val) -> bool: return (typeof(val) == TYPE_INT) or (typeof(val) == typeof(0.0)) + + +## Возвращае значение в радианах для угла в градусах [param val]. +func radians(val: float): return (val * PI) / 180.0 + + +## Возвращает значение из байтового массива в 16 битное слово +func ibytes_to_word(buf: PackedByteArray, i: int) -> int: return buf[i] + (buf[i + 1] * 256) + + +## Возвращает расстояние от центра координат до точки +func hypot(x: float, y: float) -> float: return sqrt(x * x + y * y) + + +## Проверяет пересечение диапазона x1, x2 с диапазоном y1, y2 """ +func is_overlap(x1, x2, y1, y2): return max(x1, y1) <= min(x2, y2) + + +## Производит преобразование из декартовой в полярную систему координат +func pos_calc(freq: float, aoa: float, pos0: Vector2, r0: float, r1: float, f0: float, f1: float): + var df = f1 - f0 + var r = ((freq - f0) / df) * r1 + r0 + var aoa_rad = Tools.radians(aoa) - PI / 2.0 + var x = cos(aoa_rad) * r + pos0.x + var y = sin(aoa_rad) * r + pos0.y + return Vector2(x, y) + + +## Отсекает значение до заданного диапазона +func clip_value(val, val_min, val_max): + if val < val_min: val = val_min + if val > val_max: val = val_max + return val + + +## Возвращает массив с именами членов класса [param obj] +func get_members_names(obj: Object) -> Array: + var pl = obj.get_property_list() + var names: = Array() + for item in pl: names.append(item.name) + return names + + +## Проверяет, что заданный массив [param val] похож на ip-адрес +func is_array_ipaddr(val: Array): + return (val.size() == 2) \ + and (val[0] is String) \ + and (val[1] is int) \ + and (0 < val[1]) \ + and (val[1] < 65536) + + +## Преобразует декартовые координаты в полярные. [param pos0] +## - координаты центра, [param pos] - координаты точки +func map_dec_to_rad(pos0: Vector2, pos: Vector2) -> Vector2: + var x1: float = pos.x - pos0.x + var y1: float = pos.y - pos0.y + var ang: float = (atan2(y1, x1) * 180.0) / PI + return Vector2(ang + 360.0 * float(ang < 0.0), hypot(x1, y1)) + + +## Переводит значение val из диапазона a0...a1 в диапазон b0...b1 +func map_range(val: float, a0: float, a1: float, b0: float, b1: float) -> float: + return (((val - a0) * (b1 - b0)) / (a1 - a0)) + b0 + + +## Возвращает true, если точка внутри заданного полигона +func is_point_in_polygon(pos: Vector2, p: PackedVector2Array) -> bool: + var sz: = p.size() + assert(sz) + var j: int = sz - 1 + var odd_nodes: int = 0 + for i in sz: + if ((p[i].y < pos.y and p[j].y >= pos.y or p[j].y < pos.y and p[i].y >= pos.y) and (p[i].x <= pos.x or p[j].x <= pos.x)): + odd_nodes ^= int((p[i].x + (pos.y - p[i].y) / (p[j].y - p[i].y) * (p[j].x - p[i].x) < pos.x)) + j = i + return odd_nodes > 0 + + +## Записывает слово [param val] в элемент байтовго массива [param buf] +## расположенный по индексу [param index]. Младшее слово +func set_wordi(buf: PackedByteArray, index: int, val: int) -> void: + buf[index] = val & 0xff + buf[index + 1] = (val >> 8) & 0xff diff --git a/yau07.gd b/yau07.gd new file mode 100644 index 0000000..4410d4b --- /dev/null +++ b/yau07.gd @@ -0,0 +1,251 @@ +extends Node + +## Протокол ячейки ЯУ-07 + +const ONLINE_TIMEOUT = 5.0 ## Время ожидания пакета от ячейки, секунды +const RESEND_TIMEOUT = 0.3 ## Задержка для повторной отправки команды, секунды +const RETRY_COUNT = 5 ## Количество повторных отправок +const BUFFER_SIZE = 2048 ## Размер буфера для приёма датаграмм + + +## Ячейка ЯУ-07 +class YaU07: + + ## Коды выполняемых команд + enum CmdCode { + READ_ISA = 0, ## Код команды "читать порт ISA" ячейки ЯУ-07 + WRITE_ISA = 1, ## Код команды "записать порт ISA" ячейки ЯУ-07 + WRITE_BLOCK = 60, ## Код команды "записать блок данных ISA" внешней шины ячейки ЯУ-07 + READ_BLOCK = 61, ## Код команды "читать блок данных ISA" внешней шины ячейки ЯУ-07 + } + + ## Состояние автомата выполнения команды + enum CmdState { + UNCK, ## Номер команды не ининциирован + SEND, ## Буфер готов для отправки + WAIT, ## Ожидание выполнения + DONE, ## Команда выполнена + FAIL ## Команда не выполнена + } + + signal line_changed(unit: YaU07) ## Вызывается когда меняется состояние связи с ячейкой + signal command_done(unit: YaU07) ## Вызывается после выполнения команды + signal command_fail(unit: YaU07) ## Вызывается если возникла ошибка при выполнении команды и команда не выполнена + signal parse_failed(unit: YaU07) + + var name: = String() + var cmd_num_rx: = 0 ## Номер команды принятый + var cmd_num_tx: = 0 ## Номер команды переданный + var cmd_state = CmdState.UNCK ## Состояние выполнения команды + var cmd_result: = 1 ## Результат выполнения команды. Устанавливается в ЯУ-07 + var cmd_data: = PackedByteArray() ## Данные команды. Устанавливается в ЯУ-07 + var cmd_retry = 0 ## Счётчик оставшихся попыток выполнить команду + var tick = 0.0 ## Текущее тик-время в мс + var cmd_tick_tx = 0.0 ## Время последней отправки команды + var cmd_tick_rx = 0.0 ## Время последнего приёма команды + var status: = PackedByteArray() ## Поле "состояние прибора" + var tx_data: = PackedByteArray() ## Буфер отправленный + var tx_len = 0 ## Длина последнего отправленного пакета + var isa_ports = {} ## Результат выполнения команды "читать порт" + var isa_block = {} ## Результат выполнения команды "читать массив из порта" + var online = false ## Состояние связи с ячейкой + + func _to_string() -> String: return String('яу07: %s %s' % [self.name, ['отключен', 'на связи'][int(online)]]) + + + func _init(nm): + self.name = nm + tx_data.resize(BUFFER_SIZE) + tx_data.fill(0) + tick = 0.0 + + + func process(delta: float): + tick += delta + if online and ((tick - cmd_tick_rx) > ONLINE_TIMEOUT): + online = false + emit_signal('line_changed', self) + + var rc = Error.ERR_BUSY + if cmd_state == CmdState.SEND: + rc = Error.OK + cmd_tick_tx = tick + cmd_state = CmdState.WAIT + cmd_retry = RETRY_COUNT + elif cmd_state == CmdState.WAIT: + if cmd_retry: + if (tick - cmd_tick_tx) > RESEND_TIMEOUT: + rc = Error.OK + cmd_tick_tx = tick + cmd_retry -= 1 + else: + cmd_state = CmdState.FAIL + emit_signal('command_fail', self) + rc = Error.FAILED + return rc + + + func send_isa(cmd_code: int, ports_data: Array) -> bool: + if (cmd_state == CmdState.WAIT) \ + or (cmd_state == CmdState.SEND) \ + or (cmd_state == CmdState.UNCK): + return false + if not ((cmd_code == CmdCode.READ_ISA) or (cmd_code == CmdCode.WRITE_ISA)): + return false + + cmd_num_tx = (cmd_num_rx + 1) % 0xffff + + var ports_count = ports_data.size() + tx_len = ports_count * 2 + 8 + tx_data.encode_u16(0, cmd_retry) + tx_data.encode_u16(2, cmd_num_tx) + + if cmd_code == CmdCode.WRITE_ISA: + ports_count /= 2 + tx_data[6] = cmd_code + tx_data[7] = ports_count + + var i: = 0 + for word in ports_data: + tx_data.encode_u16(8 + i, word) + i += 2 + cmd_state = CmdState.SEND + return true + + func send_block(cmd_code: int, block_len: int, port: Array) -> bool: + if (cmd_state == CmdState.WAIT) \ + or (cmd_state == CmdState.SEND) \ + or (cmd_state == CmdState.UNCK): + return false + if not ((cmd_code == CmdCode.READ_BLOCK) or (cmd_code == CmdCode.WRITE_BLOCK)): + return false + + cmd_num_tx = (cmd_num_rx + 1) % 0xffff + + tx_len = 11 + tx_data.encode_u16(0, cmd_retry) # Исправление для сетевого стека "nm-callback" процессоров STM + tx_data.encode_u16(2, cmd_num_tx) + + if cmd_code == CmdCode.WRITE_BLOCK: + block_len /= 2 + tx_data[6] = cmd_code + tx_data.encode_u16(7, block_len) + + tx_data.encode_u16(9, int(port[0])) + cmd_state = CmdState.SEND + return true + ## Возвращает код команды ЯУ-07 + ## rx_data - Принятые данные + ## Возвращает - Код команды не найден, 0...255 - код команды + func get_cmd_code(rx_data: PackedByteArray): + + var rx_data_len = rx_data.size() + if rx_data_len < 7: + return null + var status_len = rx_data.decode_u16(6) # Длина массива байтового "состояние прибора" + if rx_data_len <= (status_len + 6): # Нет поля "ответ на команду", разбор пакета завершить + return null + var cmd_code_pos = status_len + 8 # Положение в приёмном буфере "кода команды" + if rx_data_len <= cmd_code_pos: + return null + return rx_data[cmd_code_pos] # "код команды" + + + func parse_isa(rx_data: PackedByteArray, status_len) -> bool: + var ports_count_pos = status_len + 10 # Положение в приёмном буфере количества портов + if rx_data.size() < ports_count_pos: + return false + var ports_count = rx_data[ports_count_pos] # Количество портов + var counted_len = status_len + (ports_count * 4) + 11 # Вычисленная длина пакета + if rx_data.size() < counted_len: + return false + var addr_pos = status_len + 11 # Смещение положения адреса в приёмном буфере + var data_pos = status_len + 13 # Смещение положения данных в приёмном буфере + var i: = 0 + while i < ports_count: + var shift: = i * 4 + var isa_addr = rx_data.decode_u16(addr_pos + shift) # Адрес порта ISA яу-07 + var isa_data = rx_data.decode_u16(data_pos + shift) # Данные порта ISA яу-07 + isa_ports[isa_addr] = isa_data + i += 1 + cmd_data = rx_data.slice(addr_pos, (addr_pos + ports_count * 4) - 1) + return i > 0 + + func parse_block(rx_data: PackedByteArray, status_len) -> bool: + var ports_count_pos = status_len + 7 # Положение в приёмном буфере количества портов + if rx_data.size() < ports_count_pos: + return false + + + var block_len = rx_data.decode_u16(ports_count_pos + 3) + var block_adr = rx_data.decode_u16(ports_count_pos + 5) + var block_data = [] + for i in block_len: + var shift: int = i * 2 + block_data.append(rx_data.decode_u16(ports_count_pos + 7 + shift)) + isa_block[block_adr] = block_data + return true + + + ## Заполняет состояние прибора данными прочитанными из портов ЯУ-07 + ## для команд "читать порты" или "записать порты"[br] + ## [param rx_data] - Принятые данные[br] + ## Возвращает [b]true[/b] - Данные обработаны успешно[br] + ## Возвращает [b]false[/b] - Данные не обработаны из за ошибки[br] + func parse(rx_data: PackedByteArray) -> bool: + + cmd_tick_rx = tick + + if not online: + online = true + emit_signal('line_changed', self) + + var rx_data_len = rx_data.size() + if rx_data_len < 10: + return false + + cmd_num_rx = rx_data.decode_u16(2) + if cmd_state == CmdState.UNCK: + cmd_state = CmdState.DONE + + var status_len = rx_data.decode_u16(6) # Длина массива байтового "состояние прибора" + if rx_data_len < (status_len + 6): + return false + var i: = 0 + var len_rx_data: = rx_data.size() + status.resize(status_len) + while i < status_len: + var byte_i = i + 8 + if byte_i < len_rx_data: + status[i] = rx_data[byte_i] # Массив байтовый "состояние прибора" + i += 1 + + if rx_data_len == (status_len + 6): + return true # Нет поля "ответ на команду", разбор пакета завершить + + var cmd_code_pos = status_len + 8 # Положение в приёмном буфере "код команды" + if rx_data_len <= cmd_code_pos: + return false + + var cmd_status_pos = status_len + 9 # Положение в приёмном буфере "состояния выполнения команды" + if rx_data_len <= cmd_status_pos: + return false + cmd_result = rx_data[cmd_status_pos] + if cmd_result != 1: # Команда не выполнена + return false + + var cmd_code: CmdCode = rx_data[cmd_code_pos] as CmdCode # "код команды" + var rc = true + if (cmd_code == CmdCode.READ_ISA) or (cmd_code == CmdCode.WRITE_ISA): + rc = parse_isa(rx_data, status_len) + + elif (cmd_code == CmdCode.READ_BLOCK) or (cmd_code == CmdCode.WRITE_BLOCK): + rc = parse_block(rx_data, status_len) + + if rc: + if (cmd_state == CmdState.WAIT) and (cmd_num_rx == cmd_num_tx): + cmd_state = CmdState.DONE + emit_signal('command_done', self) + else: + emit_signal('parse_failed', self) + return true