KCSLightController.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using TeamAAS_VP.Models.Lights;
  7. namespace TeamAAS_VP.Core.Lights
  8. {
  9. /// <summary>
  10. /// 基于 KCS 协议的光源控制器实现。
  11. /// 该类通过注入的 <see cref="ICommunicationProtocol"/> 与物理设备通信,
  12. /// 并提供通道创建、连接/断开、初始化、全部开/关及通道亮度的内部读写实现。
  13. /// </summary>
  14. public class KCSLightController : LightControllerBase
  15. {
  16. // 通信协议抽象,用于发送/接收字节数据
  17. private readonly ICommunicationProtocol _protocol;
  18. // 使用 ASCII 编码将字符串转换为字节流
  19. private readonly Encoding _encoding;
  20. //光源配置
  21. private readonly LightControllerConfig _config;
  22. /// <summary>
  23. /// 创建一个新的 <see cref="KCSLightController"/> 实例。
  24. /// </summary>
  25. /// <param name="id">控制器的唯一标识符。</param>
  26. /// <param name="protocol">用于与设备通信的协议实现(必须已实现连接/发送/接收等)。</param>
  27. /// <param name="channelCount">该控制器管理的通道数量。</param>
  28. /// <param name="config">控制器的配置对象。</param>
  29. public KCSLightController(int id, ICommunicationProtocol protocol,int channelCount, LightControllerConfig config)
  30. : base(id, channelCount,config)
  31. {
  32. _protocol = protocol;
  33. _encoding = Encoding.ASCII;
  34. _config = config;
  35. }
  36. /// <summary>
  37. /// 为指定索引创建通道实例。
  38. /// </summary>
  39. /// <param name="index">通道索引(从 0 开始)。</param>
  40. /// <param name="channelName">通道名称。</param>
  41. /// <returns>返回对应的 <see cref="KCSLightChannel"/> 实例。</returns>
  42. protected override ILightChannel CreateChannel(int index,string channelName)
  43. {
  44. return new KCSLightChannel(index, this, channelName);
  45. }
  46. /// <summary>
  47. /// 异步建立与设备的连接并在成功后执行初始化。
  48. /// </summary>
  49. /// <returns>若连接成功返回 true,否则返回 false。</returns>
  50. public override async Task<bool> ConnectAsync()
  51. {
  52. var connected = await _protocol.ConnectAsync();
  53. if (connected)
  54. {
  55. IsConnected = true;
  56. await InitializeAsync();
  57. }
  58. return connected;
  59. }
  60. /// <summary>
  61. /// 异步断开与设备的连接并更新连接状态。
  62. /// </summary>
  63. public override async Task DisconnectAsync()
  64. {
  65. await _protocol.DisconnectAsync();
  66. IsConnected = false;
  67. }
  68. /// <summary>
  69. /// 异步初始化控制器,通常用于测试通信并同步初始状态。
  70. /// 目前实现通过读取第 0 通道亮度来验证通信链路是否正常。
  71. /// </summary>
  72. /// <returns>若初始化成功返回 true;若发生异常或通信失败返回 false。</returns>
  73. public override async Task<bool> InitializeAsync()
  74. {
  75. try
  76. {
  77. // 初始化操作:读取第 0 通道亮度以测试通信是否正常
  78. await GetChannelBrightnessInternal(0); // 测试通信
  79. return true;
  80. }
  81. catch
  82. {
  83. // 初始化失败(通信异常等)
  84. return false;
  85. }
  86. }
  87. /// <summary>
  88. /// 将所有通道设置为打开状态。命令格式参考控制器手册。
  89. /// 构造格式示例:S{CH1:DDD}T{CH2:DDD}T...C#
  90. /// 其中每个通道使用 3 位亮度值(D3),后跟动作字符 'T' 表示开。
  91. /// </summary>
  92. /// <returns>若设备返回确认字符 '!' 则认为操作成功。</returns>
  93. public override async Task<bool> TurnOnAllAsync()
  94. {
  95. StringBuilder commandBuilder = new StringBuilder("S");
  96. for (int i = 0; i < ChannelCount; i++)
  97. {
  98. var channel = (KCSLightChannel)ChannelsInternal[i];
  99. // 使用三位数字格式表示亮度(例如 005、120、255)
  100. commandBuilder.Append(channel.CurrentBrightness.ToString("D3"));
  101. // 'T' 表示打开当前通道
  102. commandBuilder.Append("T");
  103. }
  104. // 以 C# 结尾表示执行命令(协议特定)
  105. commandBuilder.Append("C#");
  106. var response = await SendCommandAsync(commandBuilder.ToString());
  107. // 期望设备返回 "!" 表示成功(根据协议)
  108. return response?.Trim() == "!";
  109. }
  110. /// <summary>
  111. /// 将所有通道设置为关闭状态。
  112. /// 构造格式示例:S000F000F...C#
  113. /// 其中 '000' 表示亮度 0,'F' 表示关闭通道。
  114. /// </summary>
  115. /// <returns>若设备返回确认字符 '!' 则认为操作成功。</returns>
  116. public override async Task<bool> TurnOffAllAsync()
  117. {
  118. StringBuilder commandBuilder = new StringBuilder("S");
  119. for (int i = 0; i < ChannelCount; i++)
  120. {
  121. // 将每个通道设置为 000(亮度 0)并附带 'F' 动作表示关闭
  122. commandBuilder.Append("000");
  123. commandBuilder.Append("F");
  124. }
  125. commandBuilder.Append("C#");
  126. var response = await SendCommandAsync(commandBuilder.ToString());
  127. return response?.Trim() == "!";
  128. }
  129. /// <summary>
  130. /// 将字符串命令编码为字节并通过协议发送,接收响应后以字符串返回。
  131. /// </summary>
  132. /// <param name="command">要发送的命令字符串(协议约定的格式)。</param>
  133. /// <returns>设备响应的字符串表示(使用 ASCII 解码)。</returns>
  134. internal async Task<string> SendCommandAsync(string command)
  135. {
  136. var data = _encoding.GetBytes(command);
  137. // 使用协议的 SendAndReceiveAsync 发送数据并等待响应(超时时间以协议或调用方为准)
  138. var response = await _protocol.SendAndReceiveAsync(data, 1500);
  139. return _encoding.GetString(response);
  140. }
  141. /// <summary>
  142. /// 读取指定通道的亮度(内部方法)。
  143. /// 命令格式示例:S{ChannelLetter}#,例如读取第 0 通道为 S A #。
  144. /// 响应预期长度为 5 字符,亮度位于索引 2..4(3 个字符)。
  145. /// </summary>
  146. /// <param name="channelIndex">通道索引(从 0 开始)。</param>
  147. /// <returns>解析得到的亮度值(0-999);若解析失败或响应格式不符则返回 0。</returns>
  148. internal async Task<int> GetChannelBrightnessInternal(int channelIndex)
  149. {
  150. // 通道字母从 'A' 开始递增
  151. string command = $"S{(char)('A' + channelIndex)}#";
  152. if (_config.LightModel == LightModel.KCS_KDC3_24V300W_8T)
  153. {
  154. command = $"SR{(char)('A' + channelIndex)}#";
  155. }
  156. var response = await SendCommandAsync(command);
  157. // 响应示例(假设):?XDDD(总长度 5),亮度在索引 2 开始的 3 个字符
  158. if (response.Length == 5)
  159. {
  160. string valueStr = response.Substring(2, 3);
  161. if (int.TryParse(valueStr, out int brightness))
  162. {
  163. return brightness;
  164. }
  165. }
  166. else if (response.Length == 7)
  167. {
  168. string valueStr = response.Substring(3, 3);
  169. if (int.TryParse(valueStr, out int brightness))
  170. {
  171. return brightness;
  172. }
  173. }
  174. // 返回默认亮度 0(表示读取失败或设备返回异常)
  175. return 0;
  176. }
  177. /// <summary>
  178. /// 设置指定通道的亮度(内部方法)。
  179. /// 命令格式示例:S{ChannelLetter}0{DDD}#,其中 '0' 可能为协议占位符,具体请参考设备手册。
  180. /// 成功时设备返回与通道字母相同的字符作为确认。
  181. /// </summary>
  182. /// <param name="channelIndex">通道索引(从 0 开始)。</param>
  183. /// <param name="brightness">目标亮度(0-999)。方法内部会格式化为三位数字。</param>
  184. /// <returns>若设备返回与通道字母相同的字符则认为设置成功。</returns>
  185. internal async Task<bool> SetChannelBrightnessInternal(int channelIndex, int brightness)
  186. {
  187. string command = $"S{(char)('A' + channelIndex)}0{brightness.ToString("D3")}#";
  188. var response = await SendCommandAsync(command);
  189. if (response=="!")
  190. {
  191. return true;
  192. }
  193. // 成功时返回通道字母(例如 'A'),去除空白后比较
  194. return response?.Trim() == ((char)('A' + channelIndex)).ToString();
  195. }
  196. /// <summary>
  197. /// 打开指定的多个通道并设置亮度,未指定通道保持当前状态不变。
  198. /// 命令格式:S{CH1:DDD}{动作}...C#
  199. /// </summary>
  200. /// <param name="channelBrightnessMap">通道索引与目标亮度的键值对,亮度为0时关闭通道。</param>
  201. /// <returns>若设备返回确认字符 '!' 则认为操作成功。</returns>
  202. public async Task<bool> TurnOnChannelsAsync(IDictionary<int, int> channelBrightnessMap)
  203. {
  204. StringBuilder commandBuilder = new StringBuilder();
  205. for (int i = 0; i < ChannelCount; i++)
  206. {
  207. var channel = (KCSLightChannel)ChannelsInternal[i];
  208. if (channelBrightnessMap.TryGetValue(i, out int brightness))
  209. {
  210. //// 指定通道:使用传入的亮度
  211. //brightness = Math.Max(0, Math.Min(255, brightness));
  212. //commandBuilder.Append(brightness.ToString("D3"));
  213. //// 亮度 > 0 表示打开,亮度 = 0 表示关闭
  214. //commandBuilder.Append(brightness > 0 ? "T" : "F");
  215. commandBuilder.Append($"S{(char)('A' + i)}0{brightness.ToString("D3")}#");
  216. }
  217. else
  218. {
  219. //// 未指定通道:保持当前亮度,无动作字符
  220. commandBuilder.Append($"S{(char)('A' + i)}0{brightness.ToString("D3")}#");
  221. }
  222. }
  223. var response = await SendCommandAsync(commandBuilder.ToString());
  224. return response?.Trim().ToUpper() == "ABCDEFGH";
  225. }
  226. /// <summary>
  227. /// 关闭指定的多个通道,未指定通道保持当前状态不变。
  228. /// 命令格式:S{CH1:DDD}{动作}...C#
  229. /// </summary>
  230. /// <param name="channelIndices">要关闭的通道索引集合。</param>
  231. /// <returns>若设备返回确认字符 '!' 则认为操作成功。</returns>
  232. public async Task<bool> TurnOffChannelsAsync(IEnumerable<int> channelIndices)
  233. {
  234. var offIndices = new HashSet<int>(channelIndices);
  235. StringBuilder commandBuilder = new StringBuilder("S");
  236. for (int i = 0; i < ChannelCount; i++)
  237. {
  238. var channel = (KCSLightChannel)ChannelsInternal[i];
  239. if (offIndices.Contains(i))
  240. {
  241. // 关闭:亮度 000 + 'F'
  242. commandBuilder.Append("000");
  243. commandBuilder.Append("F");
  244. }
  245. else
  246. {
  247. // 保持原状态:当前亮度,无动作字符
  248. commandBuilder.Append(channel.CurrentBrightness.ToString("D3"));
  249. }
  250. }
  251. commandBuilder.Append("C#");
  252. var response = await SendCommandAsync(commandBuilder.ToString());
  253. return response?.Trim() == "!";
  254. }
  255. }
  256. }