diff --git a/assets/vector/status/link.svg b/assets/vector/status/link.svg new file mode 100644 index 0000000..78f6338 --- /dev/null +++ b/assets/vector/status/link.svg @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c76a625c17d48ff3388b77407dee66f42f6ae518541279609bf6f9c68c2c2ac5 +size 352 diff --git a/assets/vector/status/link.svg.import b/assets/vector/status/link.svg.import new file mode 100644 index 0000000..efa8c79 --- /dev/null +++ b/assets/vector/status/link.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://1sjxuy7kgxy7" +path="res://.godot/imported/link.svg-f8724c7cad57bde98ee0cda284f3ebbb.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://assets/vector/status/link.svg" +dest_files=["res://.godot/imported/link.svg-f8724c7cad57bde98ee0cda284f3ebbb.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +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/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +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/components/link_panel.tscn b/components/link_panel.tscn new file mode 100644 index 0000000..12e3fd2 --- /dev/null +++ b/components/link_panel.tscn @@ -0,0 +1,103 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://scripts/link_panel.gd" id="1"] +[ext_resource type="PackedScene" path="res://components/link_rate.tscn" id="2"] +[ext_resource type="Texture2D" path="res://assets/vector/status/link.svg" id="3"] + +[node name="LinkPanel" type="PanelContainer"] +custom_minimum_size = Vector2(360, 240) +offset_right = 360.0 +offset_bottom = 240.0 +theme_type_variation = &"VehicleBackground" +script = ExtResource("1") +link_icon = ExtResource("3") + +[node name="Backdrop" type="Control" parent="."] +layout_mode = 2 +clip_contents = true +mouse_filter = 2 + +[node name="LinkIcon" type="TextureRect" parent="Backdrop"] +unique_name_in_owner = true +layout_mode = 1 +anchor_left = 0.65 +anchor_right = 0.65 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -90.0 +offset_right = 90.0 +offset_top = -90.0 +offset_bottom = 90.0 +rotation = -0.20943952 +pivot_offset = Vector2(90, 90) +mouse_filter = 2 +texture = ExtResource("3") +expand_mode = 1 +stretch_mode = 5 + +[node name="Margin" type="MarginContainer" parent="."] +layout_mode = 2 +theme_override_constants/margin_left = 16 +theme_override_constants/margin_right = 16 +theme_override_constants/margin_top = 10 +theme_override_constants/margin_bottom = 10 + +[node name="Body" type="VBoxContainer" parent="Margin"] +layout_mode = 2 +theme_override_constants/separation = 4 + +[node name="Header" type="HBoxContainer" parent="Margin/Body"] +layout_mode = 2 + +[node name="Title" type="Label" parent="Margin/Body/Header"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_font_sizes/font_size = 18 +text = "LINK" + +[node name="State" type="Label" parent="Margin/Body/Header"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 14 +text = "NO TELEMETRY" +horizontal_alignment = 2 + +[node name="Rates" type="HBoxContainer" parent="Margin/Body"] +layout_mode = 2 +size_flags_vertical = 3 +theme_override_constants/separation = 16 + +[node name="Downlink" parent="Margin/Body/Rates" instance=ExtResource("2")] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 + +[node name="Uplink" parent="Margin/Body/Rates" instance=ExtResource("2")] +unique_name_in_owner = true +layout_mode = 2 +size_flags_vertical = 3 +direction = "↑ UPLINK" + +[node name="Loss" type="HBoxContainer" parent="Margin/Body"] +layout_mode = 2 +theme_override_constants/separation = 8 + +[node name="Title" type="Label" parent="Margin/Body/Loss"] +layout_mode = 2 +theme_override_font_sizes/font_size = 14 +text = "PACKET LOSS" + +[node name="LossValue" type="Label" parent="Margin/Body/Loss"] +unique_name_in_owner = true +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_font_sizes/font_size = 20 +text = "—" + +[node name="LossWindow" type="Label" parent="Margin/Body/Loss"] +unique_name_in_owner = true +layout_mode = 2 +theme_type_variation = &"LinkMuted" +theme_override_font_sizes/font_size = 14 +text = "10 s window" +horizontal_alignment = 2 diff --git a/components/link_rate.tscn b/components/link_rate.tscn new file mode 100644 index 0000000..7214cf7 --- /dev/null +++ b/components/link_rate.tscn @@ -0,0 +1,57 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://scripts/link_rate.gd" id="1"] + +[node name="LinkRate" type="Control"] +custom_minimum_size = Vector2(140, 148) +layout_mode = 2 +size_flags_horizontal = 3 +script = ExtResource("1") + +[node name="Readouts" type="VBoxContainer" parent="."] +layout_mode = 1 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 0.5 +anchor_bottom = 0.5 +offset_left = -70.0 +offset_right = 70.0 +offset_top = -44.0 +offset_bottom = 44.0 +theme_override_constants/separation = 0 + +[node name="Direction" type="Label" parent="Readouts"] +unique_name_in_owner = true +layout_mode = 2 +theme_type_variation = &"LinkRateNormal" +theme_override_font_sizes/font_size = 15 +text = "↓ DOWNLINK" +horizontal_alignment = 1 + +[node name="Value" type="Label" parent="Readouts"] +unique_name_in_owner = true +layout_mode = 2 +theme_override_font_sizes/font_size = 32 +text = "—" +horizontal_alignment = 1 + +[node name="Units" type="Label" parent="Readouts"] +layout_mode = 2 +theme_override_font_sizes/font_size = 14 +text = "Gbps" +horizontal_alignment = 1 + +[node name="Capacity" type="Label" parent="."] +unique_name_in_owner = true +layout_mode = 1 +anchor_left = 0.5 +anchor_right = 0.5 +anchor_top = 1.0 +anchor_bottom = 1.0 +offset_left = -62.0 +offset_right = 62.0 +offset_top = -23.0 +offset_bottom = -3.0 +theme_override_font_sizes/font_size = 14 +text = "10 Gbps max" +horizontal_alignment = 1 diff --git a/docs/link_panel.md b/docs/link_panel.md new file mode 100644 index 0000000..286d2b9 --- /dev/null +++ b/docs/link_panel.md @@ -0,0 +1,16 @@ +# Link panel + +Run `scenes/link_demo.tscn` with F6. The controls preview connected, near-capacity, saturated, degraded, disconnected, stale and missing telemetry, plus both themes. + +```gdscript +$LinkPanel.set_telemetry(6.42, 1.08, 0.02, 0) +# down/up: Gbps; packet loss: percent; state: 0 connected, 1 degraded, 2 disconnected +``` + +Capacity defaults to 10 Gbps per direction. Arc and direction colors change at 80% and 95%; these thresholds and capacities are configurable. Numeric readings stay in the normal text color and remain unclamped above capacity. Utilization does not change the reported connection state. + +`loss_window_seconds` describes the supplied loss measurement (default 10 s); compute that measurement upstream. Disconnected and missing telemetry show dashes. After `stale_after_seconds` (default 2), values are held with a STALE label. Invalid or non-finite samples are rejected. `clear_telemetry()` restores the missing-data state. + +Layout and text are scene nodes; `link_rate.gd` only draws the utilization arcs. `link_icon` selects the subdued SVG watermark. The current local chain-link drawing is a draft. Colors come from the shared theme. + +The panel replaces the Link placeholder in both layouts and the element library. Existing JSON layouts keep their `link` type IDs. Camera and telemetry transports are not connected by this prototype. diff --git a/scenes/link_demo.tscn b/scenes/link_demo.tscn new file mode 100644 index 0000000..64f1103 --- /dev/null +++ b/scenes/link_demo.tscn @@ -0,0 +1,50 @@ +[gd_scene format=3] + +[ext_resource type="Script" path="res://scripts/link_demo.gd" id="1"] +[ext_resource type="PackedScene" path="res://components/link_panel.tscn" id="2"] + +[node name="LinkDemo" type="VBoxContainer"] +offset_left = 24.0 +offset_top = 24.0 +offset_right = 624.0 +offset_bottom = 420.0 +theme_override_constants/separation = 16 +script = ExtResource("1") + +[node name="Notice" type="Label" parent="."] +layout_mode = 2 +text = "SIMULATED / LINK INSTRUMENT" + +[node name="LinkPanel" parent="." instance=ExtResource("2")] +layout_mode = 2 +size_flags_horizontal = 4 + +[node name="Controls" type="HBoxContainer" parent="."] +layout_mode = 2 + +[node name="State" type="OptionButton" parent="Controls"] +layout_mode = 2 +selected = 0 +item_count = 7 +popup/item_0/text = "Connected" +popup/item_0/id = 0 +popup/item_1/text = "Near capacity" +popup/item_1/id = 1 +popup/item_2/text = "Saturated" +popup/item_2/id = 2 +popup/item_3/text = "Degraded" +popup/item_3/id = 3 +popup/item_4/text = "Disconnected" +popup/item_4/id = 4 +popup/item_5/text = "Stop feed / stale" +popup/item_5/id = 5 +popup/item_6/text = "No telemetry" +popup/item_6/id = 6 + +[node name="Contrast" type="CheckButton" parent="Controls"] +layout_mode = 2 +text = "High contrast" + +[node name="Thresholds" type="Label" parent="."] +layout_mode = 2 +text = "Arc thresholds: 80% warning · 95% saturation" diff --git a/scripts/link_demo.gd b/scripts/link_demo.gd new file mode 100644 index 0000000..3b270de --- /dev/null +++ b/scripts/link_demo.gd @@ -0,0 +1,22 @@ +extends VBoxContainer + +var elapsed := 0.0 +@onready var panel = $LinkPanel + +func _ready() -> void: + $Controls/Contrast.toggled.connect(func(high: bool) -> void: + theme = load("res://themes/theme_highcontrast.tres" if high else "res://themes/theme.tres") + ) + $Controls/State.item_selected.connect(func(index: int) -> void: + if index == 6: panel.clear_telemetry() + ) + +func _process(delta: float) -> void: + elapsed += delta + match $Controls/State.selected: + 0: panel.set_telemetry(6.42 + sin(elapsed) * 0.12, 1.08, 0.02) + 1: panel.set_telemetry(8.65, 7.9, 0.02) + 2: panel.set_telemetry(9.72, 10.2, 0.02) + 3: panel.set_telemetry(3.2, 0.4, 2.3, 1) + 4: panel.set_telemetry(0, 0, 0, 2) + 5, 6: pass diff --git a/scripts/link_demo.gd.uid b/scripts/link_demo.gd.uid new file mode 100644 index 0000000..6579b3e --- /dev/null +++ b/scripts/link_demo.gd.uid @@ -0,0 +1 @@ +uid://i5dbrwr8d2gr diff --git a/scripts/link_panel.gd b/scripts/link_panel.gd new file mode 100644 index 0000000..4e7dd17 --- /dev/null +++ b/scripts/link_panel.gd @@ -0,0 +1,74 @@ +@tool +extends PanelContainer + +enum Connection { CONNECTED, DEGRADED, DISCONNECTED } + +@export var downlink_capacity_gbps := 10.0 +@export var uplink_capacity_gbps := 10.0 +@export_range(0, 1) var warning_fraction := 0.8 +@export_range(0, 1) var fault_fraction := 0.95 +@export var loss_window_seconds := 10.0 +@export var stale_after_seconds := 2.0 +@export var link_icon: Texture2D: + set(value): + link_icon = value + if is_node_ready(): + %LinkIcon.texture = value + +var _received_ms := -1 +var _state := Connection.CONNECTED +var _down := 0.0 +var _up := 0.0 +var _loss := 0.0 +var _stale := false + +func _ready() -> void: + %LinkIcon.texture = link_icon + theme_changed.connect(_update_icon) + _update_icon() + _refresh() + +## Rates in Gbps; loss in percent over loss_window_seconds. +func set_telemetry(down: float, up: float, loss_percent: float, state: int = Connection.CONNECTED) -> bool: + if not is_finite(down) or not is_finite(up) or not is_finite(loss_percent) or down < 0 or up < 0 or loss_percent < 0 or loss_percent > 100 or state < 0 or state > 2: + return false + _down = down + _up = up + _loss = loss_percent + _state = state + _received_ms = Time.get_ticks_msec() + _refresh() + return true + +func clear_telemetry() -> void: + _received_ms = -1 + _refresh() + +func _process(_delta: float) -> void: + var stale := _received_ms >= 0 and Time.get_ticks_msec() - _received_ms > stale_after_seconds * 1000 + if stale != _stale: + _refresh() + +func _refresh() -> void: + if not is_node_ready(): + return + var missing := _received_ms < 0 + _stale = not missing and Time.get_ticks_msec() - _received_ms > stale_after_seconds * 1000 + var measured := not missing and _state != Connection.DISCONNECTED + %State.text = "NO TELEMETRY" if missing else ("STALE" if _stale else ["CONNECTED", "DEGRADED", "DISCONNECTED"][_state]) + %State.theme_type_variation = "LinkMuted" if missing else ("LinkRateWarning" if _stale or _state == Connection.DEGRADED else ("LinkRateFault" if _state == Connection.DISCONNECTED else "Label")) + var gauges := [%Downlink, %Uplink] + for index in range(2): + gauges[index].capacity_gbps = maxf([downlink_capacity_gbps, uplink_capacity_gbps][index], 0.001) + gauges[index].warning_fraction = warning_fraction + gauges[index].fault_fraction = maxf(warning_fraction, fault_fraction) + gauges[index].rate_gbps = [_down, _up][index] + gauges[index].available = measured + gauges[index].refresh() + %LossValue.text = "%.2f%%" % _loss if measured else "—" + %LossWindow.text = "%.0f s window" % loss_window_seconds + (" · held" if _stale else "") + +func _update_icon() -> void: + var tint := get_theme_color("status_icon", "SurfaceSide") + tint.a = 0.06 + %LinkIcon.self_modulate = tint diff --git a/scripts/link_panel.gd.uid b/scripts/link_panel.gd.uid new file mode 100644 index 0000000..959080e --- /dev/null +++ b/scripts/link_panel.gd.uid @@ -0,0 +1 @@ +uid://84jt1vgxm2ei diff --git a/scripts/link_rate.gd b/scripts/link_rate.gd new file mode 100644 index 0000000..c5d73ff --- /dev/null +++ b/scripts/link_rate.gd @@ -0,0 +1,44 @@ +@tool +extends Control + +@export var direction := "↓ DOWNLINK" +var capacity_gbps := 10.0 +var warning_fraction := 0.8 +var fault_fraction := 0.95 +var rate_gbps := 0.0 +var available := false + +func _ready() -> void: + resized.connect(queue_redraw) + theme_changed.connect(queue_redraw) + refresh() + +func utilization() -> float: + return rate_gbps / maxf(capacity_gbps, 0.001) + +func severity() -> int: + if not available: + return 0 + return 2 if utilization() >= fault_fraction else (1 if utilization() >= warning_fraction else 0) + +func refresh() -> void: + if not is_node_ready(): + return + %Direction.text = direction + %Direction.theme_type_variation = "LinkMuted" if not available else ["LinkRateNormal", "LinkRateWarning", "LinkRateFault"][severity()] + %Value.text = "%.2f" % rate_gbps if available else "—" + %Capacity.text = "%s Gbps max" % String.num(capacity_gbps, 2) + queue_redraw() + +func _draw() -> void: + var radius := maxf(1.0, minf(size.x, size.y) / 2 - 6) + var center := size / 2 + var start := deg_to_rad(140) + var sweep := deg_to_rad(260) + draw_arc(center, radius, start, start + sweep, 96, get_theme_color("instrument_grid", "SurfaceSide"), 3, true) + for fraction in [warning_fraction, fault_fraction]: + var radial := Vector2.from_angle(start + sweep * fraction) + draw_line(center + radial * (radius - 4), center + radial * (radius + 4), get_theme_color("instrument_axis", "SurfaceSide"), 1, true) + if available and rate_gbps > 0: + var role: String = ["instrument_accent", "warning", "fault"][severity()] + draw_arc(center, radius, start, start + sweep * clampf(utilization(), 0, 1), 96, get_theme_color(role, "SurfaceSide"), 4, true) diff --git a/scripts/link_rate.gd.uid b/scripts/link_rate.gd.uid new file mode 100644 index 0000000..e45e42b --- /dev/null +++ b/scripts/link_rate.gd.uid @@ -0,0 +1 @@ +uid://b1pcqygbaipjh