From 010f732dc4c9e72cc76155564bb34d389916d213 Mon Sep 17 00:00:00 2001 From: Paul-Winpenny <92634321+Paul-Winpenny@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:52:12 +0000 Subject: [PATCH] Began the communication over TCP with the robobin and the app. Focusing on just one app instance for now but I will need to do more than 1 --- App/RobobinApp/App.xaml.cs | 78 +++++++------ .../{Interfaces => Networking}/BLEClasses.cs | 2 +- .../BLEInterfaces.cs | 2 +- App/RobobinApp/Networking/IWifiService.cs | 14 +++ App/RobobinApp/Networking/WifiManager.cs | 108 ++++++++++++++++++ .../ViewModels/ConnectionPageViewModel.cs | 2 +- .../ViewModels/MainPageViewModel.cs | 11 +- App/RobobinApp/Views/CentreGraph.xaml.cs | 65 ++++++++--- App/RobobinApp/Views/MainPage.xaml.cs | 21 +++- Connectivity/SampleServerPi.py | 65 +++++++++++ 10 files changed, 305 insertions(+), 63 deletions(-) rename App/RobobinApp/{Interfaces => Networking}/BLEClasses.cs (93%) rename App/RobobinApp/{Interfaces => Networking}/BLEInterfaces.cs (91%) create mode 100644 App/RobobinApp/Networking/IWifiService.cs create mode 100644 App/RobobinApp/Networking/WifiManager.cs create mode 100644 Connectivity/SampleServerPi.py diff --git a/App/RobobinApp/App.xaml.cs b/App/RobobinApp/App.xaml.cs index 848550bd..4ae70a3d 100644 --- a/App/RobobinApp/App.xaml.cs +++ b/App/RobobinApp/App.xaml.cs @@ -1,52 +1,58 @@ using Microsoft.Extensions.Logging; using Plugin.BLE; using Plugin.BLE.Abstractions.Contracts; +using RobobinApp.Networking; // Make sure to include the namespace for WifiManager -namespace RobobinApp; - -public partial class App : Application +namespace RobobinApp { - public static IBluetoothLE BluetoothLE { get; private set; } - public static IAdapter BluetoothAdapter { get; private set; } - - public App() + public partial class App : Application { - InitializeComponent(); - ConfigureLogging(); + public static IBluetoothLE BluetoothLE { get; private set; } + public static IAdapter BluetoothAdapter { get; private set; } + private WifiManager _wifiManager; - // Initialize the Bluetooth adapter - InitializeBluetoothAdapter(); + public App() + { + InitializeComponent(); + ConfigureLogging(); - MainPage = new AppShell(); - } - protected override Window CreateWindow(IActivationState activationState) - { - var window = base.CreateWindow(activationState); + + InitializeBluetoothAdapter(); + _wifiManager = new WifiManager(); + Task.Run(() => _wifiManager.StartListening()); - window.MinimumWidth = 800; - window.MinimumHeight = 650; + MainPage = new AppShell(); + } - return window; - } - private void ConfigureLogging() - { - var loggerFactory = LoggerFactory.Create(builder => + protected override Window CreateWindow(IActivationState activationState) { - builder - .AddConsole() - .AddDebug(); - }); + var window = base.CreateWindow(activationState); - Logger = loggerFactory.CreateLogger<App>(); - } + window.MinimumWidth = 800; + window.MinimumHeight = 650; - private void InitializeBluetoothAdapter() - { - - BluetoothLE = CrossBluetoothLE.Current; - BluetoothAdapter = BluetoothLE.Adapter; - } + return window; + } + + private void ConfigureLogging() + { + var loggerFactory = LoggerFactory.Create(builder => + { + builder + .AddConsole() + .AddDebug(); + }); - public ILogger<App> Logger { get; private set; } + Logger = loggerFactory.CreateLogger<App>(); + } + + private void InitializeBluetoothAdapter() + { + BluetoothLE = CrossBluetoothLE.Current; + BluetoothAdapter = BluetoothLE.Adapter; + } + + public ILogger<App> Logger { get; private set; } + } } diff --git a/App/RobobinApp/Interfaces/BLEClasses.cs b/App/RobobinApp/Networking/BLEClasses.cs similarity index 93% rename from App/RobobinApp/Interfaces/BLEClasses.cs rename to App/RobobinApp/Networking/BLEClasses.cs index d0823784..3718c695 100644 --- a/App/RobobinApp/Interfaces/BLEClasses.cs +++ b/App/RobobinApp/Networking/BLEClasses.cs @@ -6,7 +6,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Robobin.Interfaces +namespace RobobinApp.Networking { public class BluetoothLEService : IBluetoothLEService { diff --git a/App/RobobinApp/Interfaces/BLEInterfaces.cs b/App/RobobinApp/Networking/BLEInterfaces.cs similarity index 91% rename from App/RobobinApp/Interfaces/BLEInterfaces.cs rename to App/RobobinApp/Networking/BLEInterfaces.cs index 181124d9..d61f586f 100644 --- a/App/RobobinApp/Interfaces/BLEInterfaces.cs +++ b/App/RobobinApp/Networking/BLEInterfaces.cs @@ -5,7 +5,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -namespace Robobin.Interfaces +namespace RobobinApp.Networking { public interface IBluetoothLEService { diff --git a/App/RobobinApp/Networking/IWifiService.cs b/App/RobobinApp/Networking/IWifiService.cs new file mode 100644 index 00000000..99b621ee --- /dev/null +++ b/App/RobobinApp/Networking/IWifiService.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace RobobinApp.Networking +{ + public interface IWifiService + { + Task StartListening(); + void SendConnectMessage(string ipAddress); + } +} diff --git a/App/RobobinApp/Networking/WifiManager.cs b/App/RobobinApp/Networking/WifiManager.cs new file mode 100644 index 00000000..b60745e2 --- /dev/null +++ b/App/RobobinApp/Networking/WifiManager.cs @@ -0,0 +1,108 @@ +using System; +using System.Net; +using System.Net.Sockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using System.Diagnostics; + +namespace RobobinApp.Networking +{ + public class WifiManager : IWifiService + { + private UdpClient _udpClient; + private const int BroadcastPort = 5005; + private bool _isConnected = false; // Flag to indicate connection status + private CancellationTokenSource _cancellationTokenSource; // For stopping the UDP listener + + public WifiManager() + { + _udpClient = new UdpClient(BroadcastPort); + _cancellationTokenSource = new CancellationTokenSource(); + } + + public async Task StartListening() + { + while (!_isConnected) // Continue listening until connected + { + Debug.WriteLine("Waiting for broadcast..."); + var result = await _udpClient.ReceiveAsync(); + string message = Encoding.ASCII.GetString(result.Buffer); + + if (message == "ROBOBIN_PRESENT") + { + Debug.WriteLine("Detected Robobin presence from: " + result.RemoteEndPoint); + SendConnectMessage(result.RemoteEndPoint.Address.ToString()); + } + } + + // Stop listening if connected + Debug.WriteLine("Stopping UDP listener."); + _udpClient.Close(); + } + + public void SendConnectMessage(string ipAddress) + { + if (_isConnected) + { + Debug.WriteLine("Already connected. No need to send another connect message."); + return; + } + + var endPoint = new IPEndPoint(IPAddress.Parse(ipAddress), 5006); + byte[] message = Encoding.ASCII.GetBytes("CONNECT"); + _udpClient.Send(message, message.Length, endPoint); + Debug.WriteLine($"Sent connect message to: {ipAddress}"); + + Task.Run(() => ConnectToTcpServer(endPoint)); + } + public async Task SendPingMessage(TcpClient tcpClient) + { + if (!_isConnected) + { + Debug.WriteLine("Not connected. Cannot send ping message."); + return; + } + + try + { + NetworkStream stream = tcpClient.GetStream(); + byte[] pingMessage = Encoding.ASCII.GetBytes("PING"); + await stream.WriteAsync(pingMessage, 0, pingMessage.Length); + Debug.WriteLine("Sent PING message to Robobin."); + } + catch (Exception ex) + { + Debug.WriteLine($"Failed to send PING message: {ex.Message}"); + } + } + + private async Task ConnectToTcpServer(IPEndPoint endPoint) + { + using (TcpClient tcpClient = new TcpClient()) + { + try + { + await tcpClient.ConnectAsync(endPoint.Address, endPoint.Port); + Debug.WriteLine("Connected to Robobin via TCP."); + + _isConnected = true; + + // Keep the connection open to send PING messages periodically + while (_isConnected) + { + await SendPingMessage(tcpClient); + await Task.Delay(5000); // Send a ping every 5 seconds + } + } + catch (Exception ex) + { + Debug.WriteLine($"TCP connection failed: {ex.Message}"); + } + } + + _cancellationTokenSource.Cancel(); // Cancel the token to stop the UDP listener + } + + } +} diff --git a/App/RobobinApp/ViewModels/ConnectionPageViewModel.cs b/App/RobobinApp/ViewModels/ConnectionPageViewModel.cs index 57804e7a..74427aa2 100644 --- a/App/RobobinApp/ViewModels/ConnectionPageViewModel.cs +++ b/App/RobobinApp/ViewModels/ConnectionPageViewModel.cs @@ -19,7 +19,7 @@ using System.Reflection.PortableExecutable; using Plugin.BLE.Windows; using System.Text; using System.Windows.Documents; -using Robobin.Interfaces; +using RobobinApp.Networking; namespace RobobinApp.ViewModels { diff --git a/App/RobobinApp/ViewModels/MainPageViewModel.cs b/App/RobobinApp/ViewModels/MainPageViewModel.cs index 26c62ad1..da7f2073 100644 --- a/App/RobobinApp/ViewModels/MainPageViewModel.cs +++ b/App/RobobinApp/ViewModels/MainPageViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows.Input; +using Microsoft.Extensions.Logging; using Microsoft.Maui.Controls; using Robobin.Models; using RobobinApp.Views; @@ -16,6 +17,10 @@ namespace RobobinApp.ViewModels private bool _isBusy; private string _statusMessage; + + + + public ICommand ConnectToRobobinCommand { get; } private Graph _selectedGraph; @@ -36,7 +41,6 @@ namespace RobobinApp.ViewModels { ConnectToRobobinCommand = new Command(async () => await OnConnectToRobobin()); SelectGraphCommand = new Command<int>(OnSelectGraph); - var graphNodes1 = new List<Node> { new Node("A", 50, 50), @@ -77,10 +81,7 @@ namespace RobobinApp.ViewModels var graph1 = new Graph("Lab 1", graphNodes1, graphMatrix1); - - - - + diff --git a/App/RobobinApp/Views/CentreGraph.xaml.cs b/App/RobobinApp/Views/CentreGraph.xaml.cs index 7836b1d3..61c3cd36 100644 --- a/App/RobobinApp/Views/CentreGraph.xaml.cs +++ b/App/RobobinApp/Views/CentreGraph.xaml.cs @@ -2,6 +2,7 @@ using Microsoft.Maui.Graphics; using Robobin.Models; using RobobinApp.Models; using System; +using System.Collections.Generic; using System.Linq; namespace RobobinApp.Views @@ -15,16 +16,19 @@ namespace RobobinApp.Views private static readonly Color BackgroundColor = Color.FromHex("#2C2F33"); private static readonly Color ConnectionColor = Colors.Grey; private static readonly Color HighlightedNodeColor = Colors.Yellow; // Color for the highlighted node + private static readonly Color PathColor = Colors.Blue; // Color for the path // Properties to keep track of scale and offset public float CurrentScale { get; private set; } = 1.0f; // Initialize to default scale public (float X, float Y) CurrentOffset { get; private set; } = (0, 0); // Initialize to default offset private Node _highlightedNode; // Field to keep track of the highlighted node + private List<Node> _path; // Field to store the path public void SetGraph(Graph graph) { _graph = graph ?? throw new ArgumentNullException(nameof(graph)); _highlightedNode = null; // Reset highlighted node when setting a new graph + _path = null; // Reset the path when setting a new graph } public void Draw(ICanvas canvas, RectF dirtyRect) @@ -40,6 +44,8 @@ namespace RobobinApp.Views float nodeRadius = BaseNodeRadius * CurrentScale; DrawConnections(canvas, CurrentScale, offsetX, offsetY); + DrawPath(canvas, CurrentScale, offsetX, offsetY); // Draw the path + DrawNodes(canvas, CurrentScale, offsetX, offsetY, nodeRadius, _highlightedNode); } @@ -64,7 +70,7 @@ namespace RobobinApp.Views float graphHeight = maxY - minY; float scaleX = dirtyRect.Width / graphWidth; float scaleY = dirtyRect.Height / graphHeight; - return Math.Min(scaleX, scaleY) * 0.8f; // Scale with some margin + return Math.Min(scaleX, scaleY) * 0.8f; } private (float offsetX, float offsetY) CalculateOffsets(RectF dirtyRect, float minX, float minY, float maxX, float maxY, float scale) @@ -135,26 +141,27 @@ namespace RobobinApp.Views public void HighlightNode(Node node) { - _highlightedNode = node; // Store the node to highlight + _highlightedNode = node; } + public List<Node> AStarPathfinding(Node startNode, Node targetNode) { var openSet = new List<Node> { startNode }; var cameFrom = new Dictionary<Node, Node>(); - var gScore = new Dictionary<Node, float>(); // Cost from start to node - var fScore = new Dictionary<Node, float>(); // Estimated cost from start to target through node + var gScore = new Dictionary<Node, float>(); + var fScore = new Dictionary<Node, float>(); foreach (var node in _graph.Nodes) { - gScore[node] = float.MaxValue; // Initial cost is infinite - fScore[node] = float.MaxValue; // Initial cost is infinite + gScore[node] = float.MaxValue; + fScore[node] = float.MaxValue; } gScore[startNode] = 0; - fScore[startNode] = Heuristic(startNode, targetNode); // Initial heuristic + fScore[startNode] = Heuristic(startNode, targetNode); while (openSet.Count > 0) { - // Find node with the lowest fScore + Node currentNode = openSet.OrderBy(node => fScore[node]).First(); if (currentNode == targetNode) @@ -164,13 +171,13 @@ namespace RobobinApp.Views openSet.Remove(currentNode); - // Check neighboring nodes + foreach (var edge in currentNode.Edges) { - Node neighbor = edge.TargetNode; // Get the neighboring node - float tentativeGScore = gScore[currentNode] + edge.Cost; // Calculate tentative gScore + Node neighbor = edge.TargetNode; + float tentativeGScore = gScore[currentNode] + edge.Cost; - if (tentativeGScore < gScore[neighbor]) // A better path has been found + if (tentativeGScore < gScore[neighbor]) { cameFrom[neighbor] = currentNode; gScore[neighbor] = tentativeGScore; @@ -184,12 +191,12 @@ namespace RobobinApp.Views } } - return new List<Node>(); // Return empty path if there is no path + return new List<Node>(); } private float Heuristic(Node a, Node b) { - // Euclidean distance + return MathF.Sqrt(MathF.Pow(a.X - b.X, 2) + MathF.Pow(a.Y - b.Y, 2)); } @@ -203,9 +210,37 @@ namespace RobobinApp.Views totalPath.Add(currentNode); } - totalPath.Reverse(); // Reverse the path to start from the initial node + totalPath.Reverse(); return totalPath; } + public Node GetHomeNode() + { + return _graph.Nodes.FirstOrDefault(n => n.Name == "Home"); + } + public List<Node> Path + { + get => _path; + set => _path = value; + } + + private void DrawPath(ICanvas canvas, float scale, float offsetX, float offsetY) + { + if (_path != null && _path.Count > 0) + { + canvas.StrokeColor = PathColor; // Color for the path + canvas.StrokeSize = 3; + + for (int i = 0; i < _path.Count - 1; i++) + { + float startX = _path[i].X * scale + offsetX; + float startY = _path[i].Y * scale + offsetY; + float endX = _path[i + 1].X * scale + offsetX; + float endY = _path[i + 1].Y * scale + offsetY; + + canvas.DrawLine(startX, startY, endX, endY); + } + } + } } } diff --git a/App/RobobinApp/Views/MainPage.xaml.cs b/App/RobobinApp/Views/MainPage.xaml.cs index 5b06ec41..a8c12e68 100644 --- a/App/RobobinApp/Views/MainPage.xaml.cs +++ b/App/RobobinApp/Views/MainPage.xaml.cs @@ -3,6 +3,7 @@ using RobobinApp.ViewModels; using RobobinApp.Models; using System.Diagnostics; using RobobinApp.Views; +using Robobin.Models; namespace RobobinApp.Views { @@ -12,7 +13,7 @@ namespace RobobinApp.Views { InitializeComponent(); - + var drawable = (GraphicsDrawable)Resources["drawable"]; if (BindingContext is MainPageViewModel viewModel) { @@ -58,12 +59,22 @@ namespace RobobinApp.Views var tapPointY = (float)tapPoint.Value.Y; var nearestNode = drawable.FindNearestNode(tapPointX, tapPointY, scale, offset.X, offset.Y); - + // Highlight the nearest node drawable.HighlightNode(nearestNode); - if (nearestNode != null) { + // Find the home node + Node homeNode = drawable.GetHomeNode(); + + if (homeNode != null) + { + + List<Node> path = drawable.AStarPathfinding(nearestNode, homeNode); + drawable.Path = path; + GraphicsView.Invalidate(); + } + DisplayAlert("Node Clicked", $"You clicked on node: {nearestNode.Name}", "OK"); } else @@ -71,8 +82,10 @@ namespace RobobinApp.Views DisplayAlert("No Node", "No node was clicked.", "OK"); } + // Invalidate to trigger a redraw and highlight the node GraphicsView.Invalidate(); } + } - + } diff --git a/Connectivity/SampleServerPi.py b/Connectivity/SampleServerPi.py new file mode 100644 index 00000000..30465635 --- /dev/null +++ b/Connectivity/SampleServerPi.py @@ -0,0 +1,65 @@ +import socket +import threading +import time + +UDP_IP = "255.255.255.255" +UDP_PORT = 5005 +LISTEN_PORT = 5006 + +def broadcast_presence(): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) + + while True: + message = b"ROBOBIN_PRESENT" + sock.sendto(message, (UDP_IP, UDP_PORT)) + time.sleep(5) + +def handle_client_connection(client_socket): + while True: + request = client_socket.recv(1024) + if not request: + break + message = request.decode() + print("Received from client: {}".format(message)) + + if message == "PING": + print("Received PING from client.") + response = b"PONG" + client_socket.sendall(response) + + client_socket.close() + print("Client disconnected.") + + +def listen_for_connections(): + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.bind(('', LISTEN_PORT)) + + while True: + data, addr = sock.recvfrom(1024) + if data.decode() == "CONNECT": + print("Received connection request from {}".format(addr)) + + # Create a TCP socket to accept connections + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as tcp_socket: + tcp_socket.bind(('', 5006)) # Listen on the fixed TCP port + tcp_socket.listen(1) + print("Listening for TCP connection...") + + client_socket, client_addr = tcp_socket.accept() + print("Client connected from {}".format(client_addr)) + + # Spawn a new thread for handling the client connection + threading.Thread(target=handle_client_connection, args=(client_socket,)).start() + + +# Start the broadcasting and listening in separate threads +broadcast_thread = threading.Thread(target=broadcast_presence) +listen_thread = threading.Thread(target=listen_for_connections) + +broadcast_thread.start() +listen_thread.start() + +broadcast_thread.join() +listen_thread.join() -- GitLab