diff --git a/server/Program.cs b/server/Program.cs index 1c9b28e..1b4e2ad 100644 --- a/server/Program.cs +++ b/server/Program.cs @@ -1,8 +1,5 @@ -using System.Net; -using System.Net.Sockets; using Microsoft.AspNetCore.Http.Features; using Microsoft.Extensions.FileProviders; -using Microsoft.Extensions.Hosting; using Newtonsoft.Json; using NLog; using NLog.Web; @@ -38,7 +35,9 @@ try // Configure Newtonsoft.Json options here options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; - }); // Add CORS policy + }); + + // Add CORS policy if (builder.Environment.IsDevelopment()) { builder.Services.AddCors(options => @@ -56,7 +55,9 @@ try .AllowAnyOrigin() .AllowAnyHeader() ); - }); // Add Swagger + }); + + // Add Swagger builder.Services.AddOpenApiDocument(options => { options.PostProcess = document => @@ -78,8 +79,10 @@ try // Url = "https://example.com/license" // } }; - }; }); - // 添加 HTTP 视频流服务 - 使用简化的类型引用 + }; + }); + + // 添加 HTTP 视频流服务 builder.Services.AddSingleton(); builder.Services.AddHostedService(provider => provider.GetRequiredService()); @@ -96,12 +99,14 @@ try logger.Info($"Use Static Files : {Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")}"); app.UseDefaultFiles(); app.UseStaticFiles(); // Serves files from wwwroot by default + // Assets Files app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "assets")), RequestPath = "/assets" }); + // Log Files if (!Directory.Exists(Path.Combine(Directory.GetCurrentDirectory(), "log"))) { @@ -114,7 +119,7 @@ try }); app.MapFallbackToFile("index.html"); } - // Add logs + app.UseHttpsRedirection(); app.UseRouting(); app.UseCors(); diff --git a/server/src/TutorialController.cs b/server/src/Controllers/TutorialController.cs similarity index 100% rename from server/src/TutorialController.cs rename to server/src/Controllers/TutorialController.cs diff --git a/server/src/HttpVideoStreamService.cs b/server/src/HttpVideoStreamService.cs index c7337d1..c74c723 100644 --- a/server/src/HttpVideoStreamService.cs +++ b/server/src/HttpVideoStreamService.cs @@ -1,16 +1,8 @@ -using System; -using System.Collections.Generic; -using System.IO; using System.Net; -using System.Net.Sockets; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; +using System.Text; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Formats.Jpeg; using SixLabors.ImageSharp.PixelFormats; -using System.Text; namespace server.src { @@ -26,18 +18,18 @@ namespace server.src private readonly int _frameRate = 30; // 30 FPS private readonly int _frameWidth = 640; private readonly int _frameHeight = 480; - + // 模拟 FPGA 图像数据 private int _frameCounter = 0; private readonly List _activeClients = new List(); private readonly object _clientsLock = new object(); - + /// /// 获取当前连接的客户端数量 /// - public int ConnectedClientsCount - { - get + public int ConnectedClientsCount + { + get { lock (_clientsLock) { @@ -85,16 +77,16 @@ namespace server.src try { _logger.LogInformation("启动 HTTP 视频流服务,端口: {Port}", _serverPort); - // 创建 HTTP 监听器 + // 创建 HTTP 监听器 _httpListener = new HttpListener(); _httpListener.Prefixes.Add($"http://localhost:{_serverPort}/"); _httpListener.Start(); - + _logger.LogInformation("HTTP 视频流服务已启动,监听端口: {Port}", _serverPort); - + // 开始接受客户端连接 _ = Task.Run(() => AcceptClientsAsync(stoppingToken), stoppingToken); - + // 开始生成视频帧 await GenerateVideoFrames(stoppingToken); } @@ -118,11 +110,11 @@ namespace server.src var context = await _httpListener.GetContextAsync(); var request = context.Request; var response = context.Response; - + _logger.LogInformation("新HTTP客户端连接: {RemoteEndPoint}", request.RemoteEndPoint); - // 处理不同的请求路径 + // 处理不同的请求路径 var requestPath = request.Url?.AbsolutePath ?? "/"; - + if (requestPath == "/video-stream") { // MJPEG 流请求 @@ -170,15 +162,15 @@ namespace server.src response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); response.Headers.Add("Pragma", "no-cache"); response.Headers.Add("Expires", "0"); - + // 跟踪活跃的客户端 lock (_clientsLock) { _activeClients.Add(response); } - + _logger.LogDebug("已启动MJPEG流,客户端: {RemoteEndPoint}", response.OutputStream?.GetHashCode() ?? 0); - + // 保持连接直到取消或出错 try { @@ -191,7 +183,7 @@ namespace server.src { // 预期的取消 } - + _logger.LogDebug("MJPEG流已结束,客户端: {ClientId}", response.OutputStream?.GetHashCode() ?? 0); } catch (Exception ex) @@ -204,7 +196,7 @@ namespace server.src { _activeClients.Remove(response); } - + try { response.Close(); @@ -223,16 +215,16 @@ namespace server.src // 获取当前帧 var imageData = await GetFPGAImageData(); var jpegData = ConvertToJpeg(imageData); - + // 设置响应头 response.ContentType = "image/jpeg"; response.ContentLength64 = jpegData.Length; response.Headers.Add("Cache-Control", "no-cache, no-store, must-revalidate"); - + // 发送JPEG数据 await response.OutputStream.WriteAsync(jpegData, 0, jpegData.Length, cancellationToken); await response.OutputStream.FlushAsync(cancellationToken); - + _logger.LogDebug("已发送快照图像,大小:{Size} 字节", jpegData.Length); } catch (Exception ex) @@ -286,7 +278,7 @@ namespace server.src response.ContentEncoding = Encoding.UTF8; byte[] buffer = Encoding.UTF8.GetBytes(html); response.ContentLength64 = buffer.Length; - + await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); response.Close(); } @@ -320,7 +312,7 @@ namespace server.src response.ContentEncoding = Encoding.UTF8; byte[] buffer = Encoding.UTF8.GetBytes(html); response.ContentLength64 = buffer.Length; - + await response.OutputStream.WriteAsync(buffer, 0, buffer.Length); response.Close(); } @@ -328,22 +320,22 @@ namespace server.src private async Task GenerateVideoFrames(CancellationToken cancellationToken) { var frameInterval = TimeSpan.FromMilliseconds(1000.0 / _frameRate); - + while (!cancellationToken.IsCancellationRequested) { try { // 从 FPGA 获取图像数据(模拟) var imageData = await GetFPGAImageData(); - + // 将图像数据转换为 JPEG var jpegData = ConvertToJpeg(imageData); - + // 向所有连接的客户端发送帧 await BroadcastFrameAsync(jpegData, cancellationToken); - + _frameCounter++; - + // 等待下一帧 await Task.Delay(frameInterval, cancellationToken); } @@ -367,11 +359,11 @@ namespace server.src { // 模拟异步 FPGA 数据读取 await Task.Delay(1); - + // 简化的模拟图像数据生成 var random = new Random(_frameCounter); var imageData = new byte[_frameWidth * _frameHeight * 3]; // RGB24 格式 - + // 生成简单的彩色噪声图案 for (int i = 0; i < imageData.Length; i += 3) { @@ -381,12 +373,12 @@ namespace server.src imageData[i + 1] = (byte)((baseColor * 2 + random.Next(0, 50)) % 256); // G imageData[i + 2] = (byte)((baseColor * 3 + random.Next(0, 50)) % 256); // B } - + if (_frameCounter % 30 == 0) // 每秒更新一次日志 { _logger.LogDebug("生成第 {FrameNumber} 帧", _frameCounter); } - + return imageData; } @@ -396,7 +388,7 @@ namespace server.src private byte[] ConvertToJpeg(byte[] rgbData) { using var image = new Image(_frameWidth, _frameHeight); - + // 将 RGB 数据复制到 ImageSharp 图像 for (int y = 0; y < _frameHeight; y++) { @@ -407,7 +399,7 @@ namespace server.src image[x, y] = pixel; } } - + using var stream = new MemoryStream(); image.SaveAsJpeg(stream, new JpegEncoder { Quality = 80 }); return stream.ToArray(); @@ -427,16 +419,16 @@ namespace server.src // 准备MJPEG帧数据 var mjpegFrameHeader = $"--boundary\r\nContent-Type: image/jpeg\r\nContent-Length: {frameData.Length}\r\n\r\n"; var headerBytes = Encoding.ASCII.GetBytes(mjpegFrameHeader); - + var clientsToRemove = new List(); var clientsToProcess = new List(); - + // 获取当前连接的客户端列表 lock (_clientsLock) { clientsToProcess.AddRange(_activeClients); } - + if (clientsToProcess.Count == 0) { return; // 没有活跃客户端 @@ -449,19 +441,19 @@ namespace server.src { // 发送帧头部 await client.OutputStream.WriteAsync(headerBytes, 0, headerBytes.Length, cancellationToken); - + // 发送JPEG数据 await client.OutputStream.WriteAsync(frameData, 0, frameData.Length, cancellationToken); - + // 发送结尾换行符 await client.OutputStream.WriteAsync(Encoding.ASCII.GetBytes("\r\n"), 0, 2, cancellationToken); - + // 确保数据立即发送 await client.OutputStream.FlushAsync(cancellationToken); - + if (_frameCounter % 30 == 0) // 每秒记录一次日志 { - _logger.LogDebug("已向客户端 {ClientId} 发送第 {FrameNumber} 帧,大小:{Size} 字节", + _logger.LogDebug("已向客户端 {ClientId} 发送第 {FrameNumber} 帧,大小:{Size} 字节", client.OutputStream.GetHashCode(), _frameCounter, frameData.Length); } } @@ -471,7 +463,7 @@ namespace server.src clientsToRemove.Add(client); } } - + // 移除断开连接的客户端 if (clientsToRemove.Count > 0) { @@ -480,12 +472,12 @@ namespace server.src foreach (var client in clientsToRemove) { _activeClients.Remove(client); - try { client.Close(); } + try { client.Close(); } catch { /* 忽略关闭错误 */ } } } - - _logger.LogInformation("已移除 {Count} 个断开连接的客户端,当前连接数: {ActiveCount}", + + _logger.LogInformation("已移除 {Count} 个断开连接的客户端,当前连接数: {ActiveCount}", clientsToRemove.Count, _activeClients.Count); } } @@ -496,7 +488,7 @@ namespace server.src public List GetConnectedClientEndpoints() { List endpoints = new List(); - + lock (_clientsLock) { foreach (var client in _activeClients) @@ -504,7 +496,7 @@ namespace server.src endpoints.Add($"Client-{client.OutputStream?.GetHashCode() ?? 0}"); } } - + return endpoints; } @@ -523,20 +515,20 @@ namespace server.src ClientEndpoints = GetConnectedClientEndpoints() }; } - + /// /// 停止 HTTP 视频流服务 /// public override async Task StopAsync(CancellationToken cancellationToken) { _logger.LogInformation("正在停止 HTTP 视频流服务..."); - + if (_httpListener != null && _httpListener.IsListening) { _httpListener.Stop(); _httpListener.Close(); } - + // 关闭所有客户端连接 lock (_clientsLock) { @@ -547,9 +539,9 @@ namespace server.src } _activeClients.Clear(); } - + await base.StopAsync(cancellationToken); - + _logger.LogInformation("HTTP 视频流服务已停止"); } @@ -566,7 +558,7 @@ namespace server.src } _httpListener.Close(); } - + lock (_clientsLock) { foreach (var client in _activeClients) @@ -576,8 +568,8 @@ namespace server.src } _activeClients.Clear(); } - + base.Dispose(); } } -} \ No newline at end of file +} diff --git a/server/src/TutorialService.ts b/server/src/TutorialService.ts deleted file mode 100644 index 6dd953d..0000000 --- a/server/src/TutorialService.ts +++ /dev/null @@ -1,30 +0,0 @@ -// 此接口提供获取例程目录服务 -// GET /api/tutorials 返回所有可用的例程目录 - -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import { Request, Response } from 'express'; - -// 获取当前文件的目录 -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const publicDir = path.resolve(__dirname, '../public'); - -export function getTutorials(req: Request, res: Response) { - try { - const docDir = path.join(publicDir, 'doc'); - - // 读取doc目录下的所有文件夹 - const entries = fs.readdirSync(docDir, { withFileTypes: true }); - const dirs = entries - .filter(dirent => dirent.isDirectory()) - .map(dirent => dirent.name); - - // 返回文件夹列表 - res.json({ tutorials: dirs }); - } catch (error) { - console.error('获取例程目录失败:', error); - res.status(500).json({ error: '无法读取例程目录' }); - } -} diff --git a/server/src/UdpClientPool.cs b/server/src/UdpClientPool.cs index d04f221..b881d5a 100644 --- a/server/src/UdpClientPool.cs +++ b/server/src/UdpClientPool.cs @@ -180,6 +180,7 @@ public class UDPClientPool /// [TODO:description] /// /// [TODO:parameter] + /// [TODO:parameter] /// [TODO:parameter] /// [TODO:parameter] /// [TODO:return] @@ -220,6 +221,7 @@ public class UDPClientPool /// [TODO:description] /// /// [TODO:parameter] + /// [TODO:parameter] /// [TODO:parameter] /// [TODO:parameter] /// [TODO:parameter] @@ -258,6 +260,7 @@ public class UDPClientPool /// [TODO:description] /// /// [TODO:parameter] + /// [TODO:parameter] /// [TODO:parameter] /// [TODO:parameter] /// [TODO:parameter] @@ -307,6 +310,7 @@ public class UDPClientPool /// [TODO:description] /// /// [TODO:parameter] + /// [TODO:parameter] /// [TODO:parameter] /// [TODO:parameter] /// [TODO:parameter] @@ -347,6 +351,7 @@ public class UDPClientPool /// [TODO:description] /// /// [TODO:parameter] + /// [TODO:parameter] /// [TODO:parameter] /// [TODO:parameter] /// [TODO:parameter] diff --git a/server/src/UdpServer.cs b/server/src/UdpServer.cs index b3142ca..934cede 100644 --- a/server/src/UdpServer.cs +++ b/server/src/UdpServer.cs @@ -231,6 +231,7 @@ public class UDPServer /// 异步等待写响应 /// /// IP地址 + /// [TODO:parameter] /// UDP 端口 /// 超时时间范围 /// 接收响应包 @@ -256,6 +257,7 @@ public class UDPServer /// 异步等待数据 /// /// IP地址 + /// [TODO:parameter] /// UDP 端口 /// 超时时间范围 /// 接收数据包 @@ -420,6 +422,7 @@ public class UDPServer /// 清空指定IP地址的数据 /// /// IP地址 + /// [TODO:parameter] /// public async Task ClearUDPData(string ipAddr, int taskID) {