From 7a6f813825dee0295e4a3046c0c6fa828afc7ae1 Mon Sep 17 00:00:00 2001 From: sonlt Date: Mon, 22 Dec 2025 14:35:55 +0700 Subject: [PATCH] save --- .../Components/Monitor/RobotMonitorView.razor | 4 +- RobotApp.Client/Pages/Dashboard.razor | 41 +++-- .../Pages/Order/ImportOrderDialog.razor | 113 ++++++++++++-- .../Pages/Order/JsonOutputPanel.razor | 8 +- RobotApp.Client/Pages/Order/NodesPanel.razor | 9 +- RobotApp.Client/Services/RobotStateClient.cs | 31 +++- RobotApp.Client/Services/UiEdge.cs | 144 +++++++++++------- .../Services/Robot/RobotStatePublisher.cs | 42 +++-- RobotApp/robot.db | Bin 151552 -> 0 bytes 9 files changed, 284 insertions(+), 108 deletions(-) delete mode 100644 RobotApp/robot.db diff --git a/RobotApp.Client/Pages/Components/Monitor/RobotMonitorView.razor b/RobotApp.Client/Pages/Components/Monitor/RobotMonitorView.razor index 497d67a..ace1b8b 100644 --- a/RobotApp.Client/Pages/Components/Monitor/RobotMonitorView.razor +++ b/RobotApp.Client/Pages/Components/Monitor/RobotMonitorView.razor @@ -25,9 +25,9 @@ X: @MonitorData.RobotPosition.X.ToString("F2")m | Y: @MonitorData.RobotPosition.Y.ToString("F2")m | θ: @((MonitorData.RobotPosition.Theta * 180 / Math.PI).ToString("F1"))° } - + @* @(IsConnected ? "Connected" : "Disconnected") - + *@
- @(IsConnected ? "ONLINE" : "OFFLINE") - - } + Icon="@(IsConnected + ? Icons.Material.Filled.CheckCircle + : Icons.Material.Filled.Error)" + Size="Size.Large" + Color="@(IsConnected ? Color.Success : Color.Error)" + Variant="Variant.Filled" + Class="px-6 py-4 text-white" + Style="font-weight: bold; font-size: 1.1rem;"> + @(IsConnected ? "ONLINE" : "OFFLINE") + + } + @{ @@ -133,7 +136,6 @@ - @@ -371,21 +373,35 @@ }; private StateMsg? CurrentState; - private bool IsConnected => RobotStateClient.LatestStates.ContainsKey(RobotSerial); + private bool IsConnected; private readonly string RobotSerial = "T800-002"; private List MessageRows = new(); protected override async Task OnInitializedAsync() { RobotStateClient.OnStateReceived += OnRobotStateReceived; - if (RobotStateClient.LatestStates.Count == 0) + RobotStateClient.OnRobotConnectionChanged += OnRobotConnectionChanged; + + if (RobotStateClient.ConnectionState == RobotClientState.Disconnected) { await RobotStateClient.StartAsync(); } + await RobotStateClient.SubscribeRobotAsync(RobotSerial); + CurrentState = RobotStateClient.GetLatestState(RobotSerial); + IsConnected = RobotStateClient.IsRobotConnected; + UpdateMessageRows(); } + private void OnRobotConnectionChanged(bool connected) + { + InvokeAsync(() => + { + IsConnected = connected; + StateHasChanged(); + }); + } private void OnRobotStateReceived(string serialNumber, StateMsg state) @@ -421,6 +437,7 @@ public void Dispose() { RobotStateClient.OnStateReceived -= OnRobotStateReceived; + RobotStateClient.OnRobotConnectionChanged -= OnRobotConnectionChanged; } private record MessageRow(string Type, string Level, string Description, bool IsError); diff --git a/RobotApp.Client/Pages/Order/ImportOrderDialog.razor b/RobotApp.Client/Pages/Order/ImportOrderDialog.razor index 42258d6..007bee4 100644 --- a/RobotApp.Client/Pages/Order/ImportOrderDialog.razor +++ b/RobotApp.Client/Pages/Order/ImportOrderDialog.razor @@ -1,11 +1,21 @@ @using System.Text.Json @using MudBlazor +@using Microsoft.AspNetCore.Components.Forms + + - Import Order JSON + + + + Import Order JSON + + + @if (ShowWarning) @@ -18,6 +28,21 @@ } + +
+ +
+ + + + + - Cancel + + Cancel + + Import + + @code { - [CascadingParameter] public IMudDialogInstance MudDialog { get; set; } = default!; + [CascadingParameter] + public IMudDialogInstance MudDialog { get; set; } = default!; public string JsonText { get; set; } = ""; public string? ErrorMessage; - public bool ShowWarning { get; set; } = false; + public bool ShowWarning { get; set; } private void Cancel() => MudDialog.Cancel(); + // ================= FILE HANDLER ================= + private async Task OnFileSelected(InputFileChangeEventArgs e) + { + ErrorMessage = null; + ShowWarning = false; + + var file = e.File; + if (file == null) + return; + + if (!file.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase) && + !file.Name.EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) + { + ShowWarning = true; + ErrorMessage = "Only .json or .txt files are supported."; + return; + } + + try + { + using var stream = file.OpenReadStream(maxAllowedSize: 1_048_576); + using var reader = new StreamReader(stream); + + JsonText = await reader.ReadToEndAsync(); + StateHasChanged(); + } + catch (Exception ex) + { + ShowWarning = true; + ErrorMessage = $"Failed to read file: {ex.Message}"; + } + } + + // ================= VALIDATE & IMPORT ================= private void ValidateAndImport() { ErrorMessage = null; ShowWarning = false; + if (string.IsNullOrWhiteSpace(JsonText)) + { + ShowWarning = true; + ErrorMessage = "JSON content is empty."; + return; + } + try { using var doc = JsonDocument.Parse(JsonText); @@ -65,10 +138,11 @@ // ===== BASIC STRUCTURE CHECK ===== if (!root.TryGetProperty("nodes", out _) || !root.TryGetProperty("edges", out _)) + { throw new Exception("Missing 'nodes' or 'edges' field."); + } var order = OrderMessage.FromSchemaObject(root); - ValidateOrder(order); MudDialog.Close(DialogResult.Ok(order)); @@ -85,19 +159,40 @@ } } + // ================= DOMAIN VALIDATION ================= private void ValidateOrder(OrderMessage order) { if (order.Nodes.Count == 0) throw new Exception("Order must contain at least one node."); - var nodeIds = order.Nodes.Select(n => n.NodeId).ToHashSet(); + if (order.Nodes.Count != order.Edges.Count + 1) + throw new Exception( + $"Invalid path structure: Nodes count ({order.Nodes.Count}) " + + $"must equal Edges count + 1 ({order.Edges.Count + 1})." + ); + + var nodeIds = order.Nodes + .Select(n => n.NodeId) + .ToHashSet(StringComparer.Ordinal); foreach (var e in order.Edges) { - if (!nodeIds.Contains(e.StartNodeId) || - !nodeIds.Contains(e.EndNodeId)) + if (string.IsNullOrWhiteSpace(e.StartNodeId) || + string.IsNullOrWhiteSpace(e.EndNodeId)) + { throw new Exception( - $"Edge '{e.EdgeId}' references unknown node." + $"Edge '{e.EdgeId}' must define both StartNodeId and EndNodeId." + ); + } + + if (!nodeIds.Contains(e.StartNodeId)) + throw new Exception( + $"Edge '{e.EdgeId}' references unknown StartNodeId '{e.StartNodeId}'." + ); + + if (!nodeIds.Contains(e.EndNodeId)) + throw new Exception( + $"Edge '{e.EdgeId}' references unknown EndNodeId '{e.EndNodeId}'." ); } } diff --git a/RobotApp.Client/Pages/Order/JsonOutputPanel.razor b/RobotApp.Client/Pages/Order/JsonOutputPanel.razor index 513e27c..900a4ac 100644 --- a/RobotApp.Client/Pages/Order/JsonOutputPanel.razor +++ b/RobotApp.Client/Pages/Order/JsonOutputPanel.razor @@ -1,4 +1,4 @@ - + 📄 JSON Output (/order) @@ -27,7 +27,6 @@ @(Copied ? "Copied!" : "Copy") @@ -36,12 +35,11 @@
-
+
+ @* - + *@ - + @* - + + *@ diff --git a/RobotApp.Client/Services/RobotStateClient.cs b/RobotApp.Client/Services/RobotStateClient.cs index 5ca88e2..ece5fa0 100644 --- a/RobotApp.Client/Services/RobotStateClient.cs +++ b/RobotApp.Client/Services/RobotStateClient.cs @@ -7,7 +7,7 @@ using System.Text.Json; namespace RobotApp.Client.Services; -// ================= CONNECTION STATE ================= +// ================= SIGNALR CONNECTION STATE ================= public enum RobotClientState { Disconnected, @@ -16,7 +16,7 @@ public enum RobotClientState Reconnecting } -// ================= CLIENT ================= +// ================= ROBOT STATE CLIENT ================= public sealed class RobotStateClient : IAsyncDisposable { private readonly NavigationManager _nav; @@ -25,11 +25,17 @@ public sealed class RobotStateClient : IAsyncDisposable private readonly object _lock = new(); private bool _started; + // ================= STATE CACHE ================= public ConcurrentDictionary LatestStates { get; } = new(); + // ================= ROBOT CONNECTION ================= + private bool _isRobotConnected; + public bool IsRobotConnected => _isRobotConnected; + // ================= EVENTS ================= public event Action? OnStateReceived; public event Action? OnStateReceivedAny; + public event Action? OnRobotConnectionChanged; public event Action? OnConnectionStateChanged; public RobotClientState ConnectionState { get; private set; } = RobotClientState.Disconnected; @@ -43,7 +49,8 @@ public sealed class RobotStateClient : IAsyncDisposable // ================= STATE HELPER ================= private void SetState(RobotClientState state) { - if (ConnectionState == state) return; + if (ConnectionState == state) + return; ConnectionState = state; OnConnectionStateChanged?.Invoke(state); @@ -82,9 +89,10 @@ public sealed class RobotStateClient : IAsyncDisposable return Task.CompletedTask; }; - _connection.Closed += async error => + _connection.Closed += async _ => { _started = false; + SetState(RobotClientState.Disconnected); if (_connection != null) { @@ -96,8 +104,14 @@ public sealed class RobotStateClient : IAsyncDisposable } }; + // ================= SIGNALR HANDLERS ================= + + // VDA5050 State _connection.On("ReceiveState", HandleState); + // Robot connection (bool only) + _connection.On("ReceiveRobotConnection", HandleRobotConnection); + try { await _connection.StartAsync(); @@ -136,6 +150,13 @@ public sealed class RobotStateClient : IAsyncDisposable OnStateReceivedAny?.Invoke(state); } + // ================= HANDLE ROBOT CONNECTION ================= + private void HandleRobotConnection(bool isConnected) + { + _isRobotConnected = isConnected; + OnRobotConnectionChanged?.Invoke(isConnected); + } + // ================= SUBSCRIBE ================= public async Task SubscribeRobotAsync(string serialNumber) { @@ -164,6 +185,7 @@ public sealed class RobotStateClient : IAsyncDisposable } LatestStates.TryRemove(serialNumber, out _); + _isRobotConnected = false; } // ================= GET CACHE ================= @@ -177,6 +199,7 @@ public sealed class RobotStateClient : IAsyncDisposable public async ValueTask DisposeAsync() { _started = false; + _isRobotConnected = false; SetState(RobotClientState.Disconnected); if (_connection != null) diff --git a/RobotApp.Client/Services/UiEdge.cs b/RobotApp.Client/Services/UiEdge.cs index 061fe7a..987a580 100644 --- a/RobotApp.Client/Services/UiEdge.cs +++ b/RobotApp.Client/Services/UiEdge.cs @@ -270,31 +270,17 @@ public class OrderMessage } public object ToSchemaObject() { - int seq = 0; + // ================= SORT NODES BY UI SEQUENCE ================= + var orderedNodes = Nodes + .OrderBy(n => n.SequenceId) + .ToList(); - return new - { - headerId = HeaderId++, - - timestamp = string.IsNullOrWhiteSpace(Timestamp) - ? DateTime.UtcNow.ToString("O") - : Timestamp, - - version = Version, - manufacturer = Manufacturer, - serialNumber = SerialNumber, - orderId = OrderId, - orderUpdateId = OrderUpdateId, - - zoneSetId = string.IsNullOrWhiteSpace(ZoneSetId) - ? null - : ZoneSetId, - - // ================= NODES ================= - nodes = Nodes.Select(n => new + // ================= BUILD NODE OBJECTS ================= + var nodeObjects = orderedNodes + .Select((n, index) => new { nodeId = n.NodeId, - sequenceId = seq++, + sequenceId = index * 2, // ✅ NODE = EVEN released = n.Released, nodePosition = new @@ -311,36 +297,55 @@ public class OrderMessage : n.NodePosition.MapId }, - actions = n.Actions.Select(a => new - { - actionId = a.ActionId, - actionType = a.ActionType, - blockingType = a.BlockingType, + actions = n.Actions? + .Select(a => new + { + actionId = a.ActionId, + actionType = a.ActionType, + blockingType = a.BlockingType, - actionParameters = a.ActionParameters != null - ? a.ActionParameters - .Select(p => new { key = p.Key, value = p.Value }) + actionParameters = a.ActionParameters? + .Select(p => new + { + key = p.Key, + value = p.Value + }) .ToArray() - : Array.Empty() - }).ToArray() - }).ToArray(), + ?? Array.Empty() + }) + .ToArray() + ?? Array.Empty() + }) + .ToArray(); - // ================= EDGES ================= - edges = Edges.Select(e => + // ================= BUILD EDGE OBJECTS ================= + var edgeObjects = Edges + .Select((e, index) => { + int sequenceId = index * 2 + 1; // ✅ EDGE = ODD + + // ---------- BASE ---------- + var baseEdge = new + { + edgeId = e.EdgeId, + sequenceId, + released = true, + startNodeId = e.StartNodeId, + endNodeId = e.EndNodeId + }; + // ================================================= - // 1️⃣ IMPORTED TRAJECTORY (ƯU TIÊN CAO NHẤT) + // 1️⃣ IMPORTED TRAJECTORY // ================================================= if (e.HasTrajectory && e.Trajectory != null) { return new { - edgeId = e.EdgeId, - sequenceId = seq++, - released = true, - - startNodeId = e.StartNodeId, - endNodeId = e.EndNodeId, + baseEdge.edgeId, + baseEdge.sequenceId, + baseEdge.released, + baseEdge.startNodeId, + baseEdge.endNodeId, trajectory = new { @@ -356,27 +361,25 @@ public class OrderMessage } // ================================================= - // 2️⃣ STRAIGHT EDGE (KHÔNG CÓ CURVE) + // 2️⃣ STRAIGHT EDGE // ================================================= if (e.Radius <= 0) { return new { - edgeId = e.EdgeId, - sequenceId = seq++, - released = true, - - startNodeId = e.StartNodeId, - endNodeId = e.EndNodeId, - + baseEdge.edgeId, + baseEdge.sequenceId, + baseEdge.released, + baseEdge.startNodeId, + baseEdge.endNodeId, actions = Array.Empty() }; } // ================================================= - // 3️⃣ EDITOR GENERATED CURVE (RADIUS + QUADRANT) + // 3️⃣ GENERATED CURVE (EDITOR) // ================================================= - var startNode = Nodes.First(n => n.NodeId == e.StartNodeId); + var startNode = orderedNodes.First(n => n.NodeId == e.StartNodeId); var A = new Point( startNode.NodePosition.X, @@ -391,17 +394,40 @@ public class OrderMessage return new { - edgeId = e.EdgeId, - sequenceId = seq++, - released = true, - - startNodeId = e.StartNodeId, - endNodeId = e.EndNodeId, + baseEdge.edgeId, + baseEdge.sequenceId, + baseEdge.released, + baseEdge.startNodeId, + baseEdge.endNodeId, trajectory = result.Trajectory, actions = Array.Empty() }; - }).ToArray() + }) + .ToArray(); + + // ================= FINAL SCHEMA OBJECT ================= + return new + { + headerId = HeaderId++, + + timestamp = string.IsNullOrWhiteSpace(Timestamp) + ? DateTime.UtcNow.ToString("O") + : Timestamp, + + version = Version, + manufacturer = Manufacturer, + serialNumber = SerialNumber, + + orderId = OrderId, + orderUpdateId = OrderUpdateId, + + zoneSetId = string.IsNullOrWhiteSpace(ZoneSetId) + ? null + : ZoneSetId, + + nodes = nodeObjects, + edges = edgeObjects }; } } diff --git a/RobotApp/Services/Robot/RobotStatePublisher.cs b/RobotApp/Services/Robot/RobotStatePublisher.cs index 9e8cd34..2d8d7ab 100644 --- a/RobotApp/Services/Robot/RobotStatePublisher.cs +++ b/RobotApp/Services/Robot/RobotStatePublisher.cs @@ -26,6 +26,8 @@ public class RobotStatePublisher : BackgroundService private readonly ILoad _loadManager; private readonly INavigation _navigationManager; private readonly RobotStateMachine _stateManager; + private readonly RobotConnection _robotConnection; + private bool? _lastRobotConnectionState; private uint _headerId = 0; private readonly PeriodicTimer _timer = new(TimeSpan.FromMilliseconds(1000)); // 1 giây/lần @@ -42,7 +44,8 @@ public class RobotStatePublisher : BackgroundService IBattery batteryManager, ILoad loadManager, INavigation navigationManager, - RobotStateMachine stateManager) + RobotStateMachine stateManager, + RobotConnection robotConnection) { _hubContext = hubContext; _robotConfig = robotConfig; @@ -56,6 +59,7 @@ public class RobotStatePublisher : BackgroundService _loadManager = loadManager; _navigationManager = navigationManager; _stateManager = stateManager; + _robotConnection = robotConnection; } private StateMsg GetStateMsg() @@ -137,36 +141,48 @@ public class RobotStatePublisher : BackgroundService protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - Console.WriteLine("[RobotStatePublisher] Started - Publishing state every 1 second via SignalR"); - - while (await _timer.WaitForNextTickAsync(stoppingToken) && !stoppingToken.IsCancellationRequested) + while (await _timer.WaitForNextTickAsync(stoppingToken)) { try { - var state = GetStateMsg(); var serialNumber = _robotConfig.SerialNumber; + // ===== SEND STATE ===== + var state = GetStateMsg(); var json = JsonSerializer.Serialize(state, JsonOptionExtends.Write); - // Push đến tất cả client đang theo dõi robot này await _hubContext.Clients .Group(serialNumber) .SendAsync("ReceiveState", json, stoppingToken); - //Console.WriteLine($"[RobotStatePublisher] Published state for {serialNumber} | " + - // $"HeaderId: {state.HeaderId} | " + - // $"Pos: ({state.AgvPosition.X:F2}, {state.AgvPosition.Y:F2}) | " + - // $"Battery: {state.BatteryState.BatteryCharge:F1}%"); + // ===== SEND ROBOT CONNECTION (ONLY WHEN CHANGED) ===== + var isConnected = _robotConnection.IsConnected; + + if (_lastRobotConnectionState != isConnected) + { + _lastRobotConnectionState = isConnected; + + await _hubContext.Clients + .Group(serialNumber) // routing only + .SendAsync( + "ReceiveRobotConnection", + isConnected, // payload only bool + stoppingToken + ); + + Console.WriteLine( + $"[RobotStatePublisher] Robot connection changed → {(isConnected ? "ONLINE" : "OFFLINE")}" + ); + } } catch (Exception ex) { - Console.WriteLine($"[RobotStatePublisher] Error publishing state: {ex.Message}"); + Console.WriteLine(ex); } } - - Console.WriteLine("[RobotStatePublisher] Stopped."); } + public override void Dispose() { _timer?.Dispose(); diff --git a/RobotApp/robot.db b/RobotApp/robot.db deleted file mode 100644 index 95eba7cf2d609b737aceedf9c866493b567d9190..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 151552 zcmeI*+jHAieg|+uvPqeiEIZjO5+}A{Xm{2eS+OKX>n7Wbi%?L^F-6iO$2RD1tcu-btBJ3GvSFwfLnTwg3narOFLMD@$p}&7jfBT>`(&t!92l ze@*ZyUdr;1*0_wScgPFtsYZ9FrSxsXS#H&hUBiC3DQ&E7tQHM@Pu*{=4X=CarFY}L@+hKVZ|i*;FQ%0g56tSk#+VjWSqv5*(&M=W(jp|7h? z`$X*!-B$NhVhWXNQ>g5e%R;eQX*BCnsnQh0+I@HE@j-|}tzO!e>Q99G@{^lhx3bO# z9aI&CUDY1+>(>g(x0M$UiDtA_`%o12G=oOoA4FnhN#Cau=J;)-cAIF#bns5z!X`Pn zcF`n?O*)b-3LW~zR=Z?rkL#m#k}#;j|xtN ze4I^UwN16hQiImA$qVX71Gtbcloqr6mv=@cV-SW&KE_vHd}+?-_*=KQZ{H53q_Ris z!*Du=RxBi?VyOI$(lF9bLJBsV3XL8iorlCcY-q;w)W8Q-$E;Rivy(PPf7_&^_Pb+L ztkU3(*X5RC(VR;;NP<0KJ3XrEl;p3pkmYx7jn3atDtL3L8+RCsp-vKVwN@@losLN?Ys4{5E7c5>hF;Z&XGdDNqpLQX&TUmc zOsX;^0sq0z4}EE>QJ2Bujdki?3TPqg#8{8!dp?E zsr$~Ur+L_;(|A&J>I$7GtVaigq)sc3)Q)|ScI20dzMo!tkEr_xsk6qEWX8t1EMHn0 znHg>nytHr^@-MZllM~-wb8!V1kggjsy zbx-7OH}nkcpTOM{iuyI_jHo1c6_sYkh{kC(ob`@}*(G$2pjUMA=W>0pC;l*Mie)`F z%+5WxM!YO_zMf6WQm5p#v(#&qkfkSPXi9|zcCP>OXP)bJ$jy2Qh z^xHPQ4zrxg@<_Mr%6xXzm3iWJT5E}`**)+ayHP!oxE#N>#(lGrpvBTW+MKPK8Rf(( z1W3##p78N0aCifW_lHe-w`7GU5jyxEu-o~!=NH(HT+Cz^|Al?Q2Lcd)00bZa0SG_< z0uX=z1Rwx`=@R%6H=C)hudZ*btgWxCuM4Z|pKRRzWPSC++iSN!{`gNmT3%iKWObF@ z|8tA~oT2~W0|5v?00Izz00bZa0SG_<0uX?}R0-VRX195E@4vRb?)=zTYnA9H}WH*RzH;}2Iq{5Vhdi(CBH4E+xu2tWV=5P$##AOHafKmY;|fB*!hP+)0x``k$n z0sO!JKZU!8AwvKH5P$##AOHafKmY;|fB*y_Fs1-||IhyYKRytE00bZa0SG_<0uX=z z1Rwwb2%KI4y#GJFvBjQ200Izz00bZa0SG_<0uX=z1bhL!|Mw;E69N!`00bZa0SG_< z0uX=z1R!vF1@Qj=^u`u@1_1~_00Izz00bZa0SG_<0ub;8@c!SIz)uK300Izz00bZa z0SG_<0uX?}=@r2H|I-^=>=^_g009U<00Izz00bZa0SG|A7r^^}UjjcN009U<00Izz z00bZa0SG_<0;g91@BdG4Y_Vq$fB*y_009U<00Izz00bZa0bc;`|9uJkga8B}009U< z00Izz00bZa0SKI40lfb|y|KlfK>z{}fB*y_009U<00Izz00eviy#Mzl@Dlg5e%R;eQ zX*BCnsnQh0+I?3#J_u2$)l1t_{fTg2esa_6R@T{|gQ}vitJ;Hp{aQi!w({a3(Tuig zABw`BW+*3Amh^q9XpY}TYPX3-Ob7ntEo_pLYZpzT*rX%bqR^pVY_&_)hxTx%$I4Eq zRI;SDtv(~kNN$=`lrAmTpDAYhKrwIp^rM0kAzyNnSZ!17v9zMKZ1RHo(Eu*w3(`WC z-?=q9C6zs5A4bzLwCci3b3Vu4y2X9_b|@7?72FgHtyoA(!BF`drC+3-NbxQK~ zcsZZr3kB}gr6B2QTHDQ1C*uZ7=M&OxQ1~Xf7V0D+(Q4(Q)ajVSvPK-3v{KD5Y1~wO zcuJ#%JGyGKIoMY9!=x%x60M(|&+^s6$XRB9>76+T%k#%y%;or{CGOQPf>4Lz?0!9$ z5M;OHO(N{~kPzOA@=V=#&KJ$Y9-VuWqElDsQEojtAS888c%*jhgS5l8O!WQq(tAYR zKS({~rzA5r&Sm-1(#XtkgW#owyO4jWWu2V(_L`FulGUpad&eTC75bWs=0vC@niEa= zadU8Lj$hP7S|#KG+o*f?b-SUbRsXE(o(g1MyKDl=|z;~TuVo~r6Q%-QFnNW+i9&Ou0BI=I_KEE>XF3d__a0e zo0SADmgdpsY|YFlCsrXqVm9%Fk57Ta8%VsVYtmaBD?EvK+LVR;{r_`k?q;(8bmp(l z+?^-$LjG^^_vijT_s;CKnSY!4g8MJ-AoK4T!uEIK&o8g#_`A#8(G};uzx22j9Exrd zvOi0m*jmxEDpfY+$Ml9bt(hoPD}oqrEZm@X(5zRnaKv5B@zM%+bbWaHWn*8}trj~C zga_cRzCL0Ek%sXhxP>&9qT4YW!wMJ=l?H3Of3c+Tl$(ZP-?VbE-3A(LH`YagSL8Z{X~! zuO6(vnzF}14f;I8iEJqFK_MH9S6;Y#Im|Q-r(b%JJ3ra{S5) z_bVfGo?tO`%i}$K;0@h-gcC2OL-QW9cxe%x{QS!TCwu68_KZH+8JxDgL)NboJ}bnE z9x7U&UiQXMys<|@!($6gszKu36vXIo=n6N!&Pedw4A0FX&$CX&bF<^scbudCmHXT* zI8@wuJ-+;I+EG6^4R}csJ(7|Se*d_?Pv`^H$VH5Q==(J`Rja2dhvAtZbc-`;hDn_Y z`~gR&g6Od61mKT=jj`YjE8OSS{m>cS?K8B(KhufAR<$mdKCiIn&%umS5bE-lT$d|F zxe-3C(}y)T*i-KUoAo#4vP>V97o|o~+LY;a%yH%i?nHe%Hc`8$njmEEGM^T*!CBJ_ z**I5W@y;XHV1l&Mtd=Ttd)soQ>Bn$XbVO$i?z4;dB<_}tF~dhf;O^8PI|Q}2v;6Wh z_rwW+_p-+#^aX`ZZ{*x}KmOtmBmA+#8#=uzxk;^pu?!>z*yMPg7{W!81#QTx3HH5ir`RJOtzToy%-D{ojy$CcA;i+PWOVL;kSBOUQ_1=U?QSc2e~&)E>6>E>b8shp;uxAnPi@ysvZuZn zV;HsmuBKWCb<(4+;gX-u`VQ`cCjTi}@>{lO-KIuQ6G?9@{c1&Nj3E}s0&>pKsW?;e zlo$X0f8wVaXb1raKmY;|fB*y_009U<00I!08Uft@Pfe&WPzXQ(0uX=z1Rwwb2tWV= z5P-nM0=WO5m<|mg009U<00Izz00bZa0SG_<0#hS^`~Rs46$T0c2tWV=5P$##AOHaf zKmY;|m{WbNo-1xT7Ui?~oTe z7BMSImz3CoWc4bA{XHlq#F@V?m767KLg>5JPQ5;f7c-%&wxT zUz3hsS1kOwkmIk@EuM3>D0dZAi*Ihp+nl$Zpw0;!aVm<1UtY`ccbB=NE6x^5k6Xd0 z>V`(F7W=c*iLDi*8%=E%AA{2++gZ0)v2es)&GFI-cXWMt`(Y58uB48ks3~f9cnt1pVZ<0B4dY{Q3&Ug3K5$d(eTRYR#8S(VyXI{$ znp%dcRhoiWyFaw^qEM@swx#+L;lBJNNZgx(7#$8>;l|e+ZMfU5;ci3Q@iSZ$wyJfx z^m!$^rGikGx8%B9Daws7oyC%?)Da78KHHSbGM)B{Qllts$_x3zr|)L@2g@f%m}NvH zz|M;IkL!GnUtZ>3{X7<5r#J|&vv@fzx=!hs!DV&n-Xw-DY#9lWw}G?>d3_d63W?K= z7ZLZn*D@9nzrTctIL*={@;W16Q{x{cC+_}myrbTm!b3;ByXHN-|NkB@XwVe|AOHaf VKmY;|fB*y_009U<;Qy(>{{inhw@Ls2