GroupHandler.cs 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799
  1. using AutoMapper;
  2. using Edge.Core.Database;
  3. using Edge.Core.Processor;
  4. using Edge.Core.IndustryStandardInterface.Pump;
  5. using Microsoft.Extensions.DependencyInjection;
  6. using Microsoft.Extensions.Logging;
  7. using Microsoft.Extensions.Logging.Abstractions;
  8. using Wayne_VaporRecoveryDataCollectorBoard.MessageEntity;
  9. using Wayne_VaporRecoveryDataCollectorBoard.MessageEntity.Incoming;
  10. using Wayne_VaporRecoveryDataCollectorBoard.MessageEntity.Outgoing;
  11. using System;
  12. using System.Collections.Generic;
  13. using System.IO;
  14. using System.Linq;
  15. using System.Text;
  16. using System.Threading.Tasks;
  17. using System.Xml;
  18. using System.Xml.Serialization;
  19. using Timer = System.Timers.Timer;
  20. using Edge.Core.Processor.Dispatcher.Attributes;
  21. using Edge.Core.Processor.Communicator;
  22. using Edge.Core.UniversalApi;
  23. using System.Threading;
  24. namespace Wayne_VaporRecoveryDataCollectorBoard
  25. {
  26. [MetaPartsRequired(typeof(HalfDuplexActivePollingDeviceProcessor<,>))]
  27. [MetaPartsRequired(typeof(ComPortCommunicator<>))]
  28. [MetaPartsRequired(typeof(TcpClientCommunicator<>))]
  29. //[UniversalApi(Name = GenericAlarm.UniversalApiEventName, EventDataType = typeof(GenericAlarm[]), Description = "Fire GenericAlarms to AlarmBar for attracting users.")]
  30. [MetaPartsDescriptor(
  31. "lang-zh-cn:稳牌气液比数据收集板lang-zh-tw:穩牌氣液比數據收集板lang-en-us:稳牌气液比数据收集板",
  32. "lang-zh-cn:用于驱动 稳牌气液比数据收集板,日志名称 DynamicPrivate_VaporRecoveryDataCollectorBoard" +
  33. "lang-zh-tw:用於驅動 穩牌氣液比數據收集板" +
  34. "lang-en-us:Used for driven Wayne Vapor and Liquid collector board, log file name: DynamicPrivate_VaporRecoveryDataCollectorBoard",
  35. new[] { "lang-zh-cn:在线监测lang-zh-tw:在線監測lang-en-us:OnlineWatch" })]
  36. /// <summary>
  37. /// one comm channel may have multiple devices connected.
  38. /// </summary>
  39. public class GroupHandler : TestableActivePollingDeviceHandler<byte[], DataCollectorMessageBase>, IEnumerable<Board>, IDisposable
  40. {
  41. //private DateTime lastDeviceMessageReceivedTime;
  42. // by seconds, change this value need change the correlated deviceOfflineCountdownTimer's interval as well
  43. //public const int maxSecondsDeviceMuteWillTreatAsOffline = 30;
  44. //private Timer deviceOfflineCountdownTimer;
  45. private IServiceProvider services;
  46. public event EventHandler<BoardStateChangeEventArgs> OnBoardStateChange;
  47. /// <summary>
  48. /// notify only the nozzle fuelling state change.
  49. /// </summary>
  50. public event EventHandler<BoardNozzleStateChangeEventArgs> OnBoardNozzleStateChange;
  51. /// <summary>
  52. /// either a trx done flow data, or a serials of real time flow data.
  53. /// </summary>
  54. public event EventHandler<DataRecievedEventArgs> OnDataRecieved;
  55. //public string HardwareIdentity { get; set; }
  56. public IContext<byte[], DataCollectorMessageBase> Context { get { return this.context; } }
  57. static ILogger logger = NullLogger.Instance;
  58. private int singleBoardOfflineTimeThresholdByMs = 15000;
  59. private Timer singleBoardOfflineCheckTimer;
  60. public List<Board> Boards { get; private set; } = new List<Board>();
  61. /// <summary>
  62. /// the device has an internal time, if that time lagging too much from Fc, then FC will send a Write Time.
  63. /// this value controls as the threshold.
  64. /// </summary>
  65. private const int maxTimeGapFromFcAndDeviceBySeconds = 60 * 5;
  66. //private XmlConfiguration configuration;
  67. private IContext<byte[], DataCollectorMessageBase> context;
  68. //public byte DeviceAddress { get; set; }
  69. public DeviceConfigV1 DeviceConfig { get; private set; }
  70. public DataCollectorBoardTypeEnum? CurrentDataCollectorType { get; set; }
  71. //public int DeviceId { get; }
  72. public class DeviceConfigV1
  73. {
  74. public IEnumerable<BoardConfigV1> BoardConfigs { get; set; }
  75. }
  76. public class BoardConfigV1
  77. {
  78. /// <summary>
  79. /// each board has a serial number of global unique, like the mac address.
  80. /// </summary>
  81. public int SerialNumber { get; set; }
  82. /// <summary>
  83. /// fcc will based on serial number to set a physical address to board dynamically.
  84. /// </summary>
  85. public byte SetPhysicalAddress { get; set; }
  86. public IEnumerable<BoardNozzleConfigV1> NozzleConfigs { get; set; }
  87. }
  88. public class BoardInitParameterConfigV1
  89. {
  90. public byte 最大未变化次数 { get; set; }
  91. public int 开始加油阀值 { get; set; }
  92. public int 停止加油阀值 { get; set; }
  93. public int 最小加油量 { get; set; }
  94. public int 加油脉冲当量 { get; set; }
  95. public int 油气脉冲当量 { get; set; }
  96. /// <summary>
  97. /// 控制板将依据此值在加油过程中调节气泵回气量,以求尽可能靠近此比值
  98. /// </summary>
  99. public double 气液比值 { get; set; }
  100. public string ToLogString()
  101. {
  102. return "最大未变化次数: " + this.最大未变化次数
  103. + ", 开始加油阀值: " + this.开始加油阀值
  104. + ", 停止加油阀值: " + this.停止加油阀值
  105. + ", 最小加油量: " + this.最小加油量
  106. + ", 加油脉冲当量: " + this.加油脉冲当量
  107. + ", 油气脉冲当量: " + this.油气脉冲当量
  108. + ", 气液比值: " + this.气液比值;
  109. }
  110. }
  111. public class BoardNozzleConfigV1
  112. {
  113. public int OnBoardNozzleNumber { get; set; }
  114. /// <summary>
  115. /// each vr watched nozzle is a physical pump nozzle, and each nozzle must have a site level nozzle id(in China),
  116. /// here is the binding info, this is the key info for draw UI for user.
  117. /// </summary>
  118. public int SiteLevelNozzleId { get; set; }
  119. /// <summary>
  120. /// specified by config in device handler, has the priority compare to external input values.
  121. /// </summary>
  122. public BoardInitParameterConfigV1 ManualInitParameter { get; set; }
  123. /// <summary>
  124. /// specified by config from external(like an app), has the low priority compare to internal manual values.
  125. /// </summary>
  126. public BoardInitParameterConfigV1 ExternalInitParameter { get; set; }
  127. }
  128. protected void FireOnBoardStateChangeEvent(Board board)
  129. {
  130. this.OnBoardStateChange?.Invoke(this, new BoardStateChangeEventArgs(board));
  131. }
  132. protected void FireOnDataReceivedEvent(DataCollectorMessageBase data)
  133. {
  134. this.OnDataRecieved?.Invoke(this, new DataRecievedEventArgs(data));
  135. }
  136. protected void FireOnBoardNozzleStateChangeEvent(BoardNozzle nozzle)
  137. {
  138. this.OnBoardNozzleStateChange?.Invoke(this, new BoardNozzleStateChangeEventArgs(nozzle));
  139. }
  140. [ParamsJsonSchemas("DeviceGroupHandlerCtorParamsJsonSchemas")]
  141. public GroupHandler(DeviceConfigV1 deviceConfig, IServiceProvider services)
  142. {
  143. if (deviceConfig.BoardConfigs.Select(bc => bc.SetPhysicalAddress).GroupBy(g => g).Any(g => g.Count() >= 2))
  144. throw new ArgumentException("2 or more board configs have the same PhysicalAddress defined, while it should be unique per one comm channel");
  145. if (deviceConfig.BoardConfigs.Select(bc => bc.SerialNumber).GroupBy(g => g).Any(g => g.Count() >= 2))
  146. throw new ArgumentException("2 or more board configs have the same SerialNumber defined, while it should be unique per one comm channel");
  147. this.services = services;
  148. this.DeviceConfig = deviceConfig;
  149. var loggerFactory = services.GetRequiredService<ILoggerFactory>();
  150. logger = loggerFactory.CreateLogger("DynamicPrivate_VaporRecoveryDataCollectorBoard");
  151. deviceConfig.BoardConfigs.ToList().ForEach(bc =>
  152. {
  153. var board = new Board(bc.SerialNumber, bc.SetPhysicalAddress, BoardStateEnum.UnInit,
  154. bc.NozzleConfigs.Select(nc => new BoardNozzle()
  155. {
  156. NozzleNumberOnBoard = (byte)nc.OnBoardNozzleNumber,
  157. SiteLevelNozzleId = nc.SiteLevelNozzleId,
  158. NozzleState = NozzleStateEnum.Unknown
  159. }).ToList());
  160. board.LastIncomingMessageReceivedTime = DateTime.Now;
  161. this.Boards.Add(board);
  162. });
  163. //DeviceId = deviceId;
  164. //HardwareIdentity = config.SerialNumber;
  165. // sample
  166. // <XmlConfiguration>
  167. // <Nozzle dataCollectorNozzleNumber = "1" siteLevelNozzleId="8" siteLevelDispenserId="#1号机">
  168. // <最大未变化次数>5</最大未变化次数>
  169. // <开始加油阀值>16777255</开始加油阀值>
  170. // <停止加油阀值>268435495</停止加油阀值>
  171. // <最小加油量>268435495</最小加油量>
  172. // <加油脉冲当量>268435456</加油脉冲当量>
  173. // <油气脉冲当量>1677721600</油气脉冲当量>
  174. // <气液比值>2013265924</气液比值>
  175. // </Nozzle>
  176. // <Nozzle dataCollectorNozzleNumber = "2" siteLevelNozzleId="9" siteLevelDispenserId="#1号机">
  177. // <最大未变化次数>5</最大未变化次数>
  178. // <开始加油阀值>16777255</开始加油阀值>
  179. // <停止加油阀值>268435495</停止加油阀值>
  180. // <最小加油量>268435495</最小加油量>
  181. // <加油脉冲当量>268435456</加油脉冲当量>
  182. // <油气脉冲当量>1677721600</油气脉冲当量>
  183. // <气液比值>2013265924</气液比值>
  184. // </Nozzle>
  185. // </XmlConfiguration>
  186. //this.DeviceAddress = (byte)bindingDataCollectorAddress;
  187. //var xmlSerializer = new XmlSerializer(typeof(XmlConfiguration));
  188. //this.configuration =
  189. // xmlSerializer.Deserialize(new StringReader(xmlConfig)) as XmlConfiguration;
  190. }
  191. public async override void Init(IContext<byte[], DataCollectorMessageBase> context)
  192. {
  193. base.Init(context);
  194. this.context = context;
  195. // may have many boards in underlying communicator(share one channel), so here dynamic set offline time but with a safe value for avoid too sensitive.
  196. this.singleBoardOfflineTimeThresholdByMs = 2200 * this.Boards.Count >= 6000 ? 2200 * this.Boards.Count : 6000;
  197. //accuracy is 1000ms
  198. this.singleBoardOfflineCheckTimer = new Timer(1000);
  199. this.singleBoardOfflineCheckTimer.Elapsed += async (a, b) =>
  200. {
  201. var offlineBoards = this.Boards.Where(b => DateTime.Now.Subtract(b.LastIncomingMessageReceivedTime).TotalMilliseconds >= this.singleBoardOfflineTimeThresholdByMs);
  202. foreach (var offBoard in offlineBoards)
  203. {
  204. if (offBoard.State == BoardStateEnum.UnInit || offBoard.State == BoardStateEnum.Initializing) continue;
  205. logger.LogInformation($"Board with serialNumber: {offBoard.SerialNumber}, PhyAddr: {offBoard.BoardPhysicalAddress} is offline({this.singleBoardOfflineTimeThresholdByMs}ms no see data), turn state to UnInit...");
  206. offBoard.State = BoardStateEnum.UnInit;
  207. this.FireOnBoardStateChangeEvent(offBoard);
  208. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  209. await universalApiHub.FirePersistGenericAlarmIfNotExists(this.context.Processor,
  210. new GenericAlarm()
  211. {
  212. Title = $"lang-en-us:V/R board disconnectedlang-zh-cn:气液比采集板通讯断开lang-zh-tw:氣液比數據采集板通訊斷開",
  213. Category = $"lang-zh-cn:气液比采集板lang-zh-tw:氣液比采集板",
  214. Detail = $"lang-zh-cn:断开采集板的SN是:{offBoard.SerialNumber},上面配置的全站枪号是: " +
  215. $"{string.Join(", ", offBoard.Nozzles.Select(n => n.SiteLevelNozzleId.ToString()))}" +
  216. $"lang-zh-tw:斷開采集板的SN是:{offBoard.SerialNumber},上面配置的全站槍號是: " +
  217. $"{string.Join(", ", offBoard.Nozzles.Select(n => n.SiteLevelNozzleId.ToString()))}",
  218. Severity = GenericAlarmSeverity.Warning
  219. },
  220. ga => ga.Detail,
  221. ga => ga.Detail);
  222. }
  223. };
  224. this.singleBoardOfflineCheckTimer.Start();
  225. this.context.Incoming.OnLongTimeNoSeeMessage += async (_, __) =>
  226. {
  227. logger.LogInformation($"Boards group(serialNumbers are: {string.Join(" ,", this.Boards.Select(b => b.SerialNumber.ToString()))}, PhyAddr are: {string.Join(" ,", this.Boards.Select(b => b.BoardPhysicalAddress.ToString()))}) has lone time no see incoming message from underlying comm, switching all boards' state to UnInit.");
  228. this.Boards.ForEach(s =>
  229. {
  230. if (s.State != BoardStateEnum.UnInit)
  231. {
  232. s.State = BoardStateEnum.UnInit;
  233. this.FireOnBoardStateChangeEvent(s);
  234. }
  235. });
  236. var disconnectedBoardPhyAddressesStr = this.DeviceConfig.BoardConfigs?.Select(b => b.SetPhysicalAddress.ToString()).Aggregate("", (acc, n) => acc + ", " + n);
  237. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  238. await universalApiHub.FirePersistGenericAlarmIfNotExists(context.Processor,
  239. new GenericAlarm()
  240. {
  241. Title = $"lang-en-us:Multiple V/R board disconnectedlang-zh-cn:多块气液比数据采集板通讯断开lang-zh-tw:多塊氣液比數據采集板通訊斷開",
  242. Category = $"lang-zh-cn:气液比采集板lang-zh-tw:氣液比采集板",
  243. Detail = $"lang-zh-cn:断开采集板的物理地址分别是:{disconnectedBoardPhyAddressesStr}" +
  244. $"lang-zh-tw:斷開采集板的物理地址分別是:{disconnectedBoardPhyAddressesStr}",
  245. Severity = GenericAlarmSeverity.Warning
  246. }, ga => ga.Detail,
  247. ga => ga.Detail);
  248. };
  249. this.context.Incoming.LongTimeNoSeeMessageTimeout = 18000;
  250. var timeWindowWithActivePollingOutgoing = this.context.Outgoing as TimeWindowWithActivePollingOutgoing<byte[], DataCollectorMessageBase>;
  251. int prePolledBoardIndex = 0;
  252. timeWindowWithActivePollingOutgoing.PollingMsgProducer = () =>
  253. {
  254. if (prePolledBoardIndex == this.Boards.Count) prePolledBoardIndex = 0;
  255. var targetBoard = this.Boards[prePolledBoardIndex];
  256. prePolledBoardIndex++;
  257. switch (targetBoard.State)
  258. {
  259. case BoardStateEnum.Initialized:
  260. {
  261. return new READ_WORKING_TYPE_Request()
  262. {
  263. Address = targetBoard.BoardPhysicalAddress
  264. };
  265. }
  266. case BoardStateEnum.UnInit:
  267. {
  268. //logger.LogInformation($"Board with SerialNumber: {targetBoard.SerialNumber }, PhysicalAddress: {targetBoard.BoardPhysicalAddress} is turning state from UnInit to Initializing...");
  269. targetBoard.State = BoardStateEnum.Initializing;
  270. this.FireOnBoardStateChangeEvent(targetBoard);
  271. Task.Run(() =>
  272. {
  273. try
  274. {
  275. this.InitializeDevice(targetBoard.SerialNumber, targetBoard.BoardPhysicalAddress).ContinueWith(async (pt) =>
  276. {
  277. if (pt.Result)
  278. {
  279. targetBoard.State = BoardStateEnum.Initialized;
  280. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  281. await universalApiHub.ClosePersistGenericAlarms(this.context.Processor, $"断开", "conn recovered");
  282. }
  283. else
  284. {
  285. targetBoard.State = BoardStateEnum.UnInit;
  286. }
  287. this.FireOnBoardStateChangeEvent(targetBoard);
  288. });
  289. }
  290. catch (Exception exxx)
  291. {
  292. logger.LogError($"InitializeDevice Board with SerialNumber: {targetBoard.SerialNumber} will set to PhyAddr: {targetBoard.BoardPhysicalAddress} exceptioned with: {exxx}");
  293. }
  294. });
  295. return null;
  296. }
  297. case BoardStateEnum.Initializing: { return null; }
  298. };
  299. return null;
  300. };
  301. context.Communicator.OnConnected += async (_, __) =>
  302. {
  303. //await Task.Delay(5000).ContinueWith((t) => this.InitializeDevice());
  304. };
  305. }
  306. /// <summary>
  307. /// send params and re-sync time from device.
  308. /// </summary>,
  309. /// <returns></returns>
  310. private async Task<bool> InitializeDevice(int boardSerialNumber, byte setBoardPhysicalAddress)
  311. {
  312. logger.LogInformation($"InitializeDevice ==========> Board with SerialNumber: {boardSerialNumber} will set to PhyAddr: {setBoardPhysicalAddress}...");
  313. var setAddressResponse = await context.Outgoing.WriteAsync(
  314. new SET_ADDRESS_Request(boardSerialNumber, setBoardPhysicalAddress),
  315. (_, testResponse) => testResponse is SET_ADDRESS_Response r && r.Address == 0
  316. && r.DeviceSerialNumber == boardSerialNumber && r.NewDeviceAddress == setBoardPhysicalAddress, 1000) as SET_ADDRESS_Response;
  317. if (setAddressResponse == null)
  318. {
  319. logger.LogError($" Board with SerialNumber: { boardSerialNumber} timedout to set to PhyAddr: { setBoardPhysicalAddress}");
  320. return false;
  321. }
  322. var b = this.Boards?.FirstOrDefault(b => b.BoardPhysicalAddress == setAddressResponse.NewDeviceAddress);
  323. if (b == null)
  324. {
  325. logger.LogInformation($" B PhyAddr: {setAddressResponse.NewDeviceAddress} does not defined via local configs");
  326. return false;
  327. }
  328. //logger.LogInformation($" Board with SerialNumber: {boardSerialNumber}, " +
  329. // $"Will read board side Params for its total {this.DeviceConfig.BoardConfigs.FirstOrDefault(bc => bc.SerialNumber == boardSerialNumber)?.NozzleConfigs?.Count() ?? 0} nozzles");
  330. foreach (var nozzleNo in
  331. this.DeviceConfig.BoardConfigs.FirstOrDefault(bc => bc.SerialNumber == boardSerialNumber)?.NozzleConfigs?.Select(nc => (byte)nc.OnBoardNozzleNumber))
  332. {
  333. //var readParaResponse = await context.Outgoing.WriteAsync(
  334. // new READ_PARA_Request(nozzleNo) { Address = setBoardPhysicalAddress },
  335. // (request, response) => response is READ_PARA_Response r && r.Address == setBoardPhysicalAddress, 5000) as READ_PARA_Response;
  336. //if (readParaResponse == null)
  337. //{
  338. // logger.LogInformation(" B PhyAddr: " + setBoardPhysicalAddress + ", board side Parameter read for nozzle " +
  339. // nozzleNo +
  340. // " timedout, will skip this nozzle and continue...");
  341. // continue;
  342. //}
  343. //logger.LogInformation(" B PhyAddr: " + setBoardPhysicalAddress + ", board side Parameter was read for nozzle " + readParaResponse.NozzleNumber
  344. // + ", detail: " + readParaResponse.ToLogString());
  345. var nozzleInitParameter = this.DeviceConfig.BoardConfigs.FirstOrDefault(bc => bc.SerialNumber == boardSerialNumber)
  346. ?.NozzleConfigs?.FirstOrDefault(nc => nc.OnBoardNozzleNumber == nozzleNo)?.ManualInitParameter;
  347. if (nozzleInitParameter == null)
  348. {
  349. logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, Will try use ExternalInitParameter to Write Para to board for nozzle: {nozzleNo}");
  350. nozzleInitParameter = this.DeviceConfig.BoardConfigs.FirstOrDefault(bc => bc.SerialNumber == boardSerialNumber)
  351. ?.NozzleConfigs?.FirstOrDefault(nc => nc.OnBoardNozzleNumber == nozzleNo)?.ExternalInitParameter;
  352. }
  353. else
  354. logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, Will use ManualInitParameter to Write para to board for nozzle: {nozzleNo}");
  355. if (nozzleInitParameter != null)
  356. {
  357. //logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, Will set new PARA to board for nozzle: {nozzleNo}");
  358. var writeParamsResponse = await context.Outgoing.WriteAsync(
  359. new WRITE_PARA_Request(nozzleNo)
  360. {
  361. Address = setBoardPhysicalAddress,
  362. 最大未变化次数 = nozzleInitParameter.最大未变化次数,
  363. 开始加油阀值 = nozzleInitParameter.开始加油阀值,
  364. 停止加油阀值 = nozzleInitParameter.停止加油阀值,
  365. 最小加油量 = nozzleInitParameter.最小加油量,
  366. 加油脉冲当量 = nozzleInitParameter.加油脉冲当量,
  367. 油气脉冲当量 = nozzleInitParameter.油气脉冲当量,
  368. 气液比值 = nozzleInitParameter.气液比值
  369. },
  370. (_, test) => test is WRITE_PARA_Response r && r.Address == setBoardPhysicalAddress, 2000);
  371. if (writeParamsResponse != null)
  372. logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, new Parameter was set successfully to nozzle {nozzleNo}");
  373. else
  374. logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, new Parameter was failed set to nozzle {nozzleNo}");
  375. }
  376. else
  377. logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, Skip set new PARA to board for nozzle: {nozzleNo} as Manual and External InitParmater are both null");
  378. }
  379. b.LastIncomingMessageReceivedTime = DateTime.Now;
  380. logger.LogInformation($" Board with SerialNumber: { boardSerialNumber} has successfully set to PhyAddr: { setBoardPhysicalAddress}");
  381. var syncTimeResult = await this.ReadAndWriteTimeIfMaxTimeGapReached(setBoardPhysicalAddress);
  382. if (syncTimeResult)
  383. {
  384. logger.LogInformation("B PhyAddr: " + setBoardPhysicalAddress + ", Sync time succeed, and init succeed!");
  385. return true;
  386. }
  387. else
  388. {
  389. logger.LogInformation("B PhyAddr: " + setBoardPhysicalAddress + ", Sync time failed, init failed");
  390. return false;
  391. }
  392. }
  393. [UniversalApi(Description = "获取VR board油枪的初始化参数值")]
  394. public async virtual Task<READ_PARA_Response> ReadBoardNozzleInitParameters(byte boardPhysicalAddress, byte onBoardNozzleNumber)
  395. {
  396. logger.LogInformation($"B PhyAddr: {boardPhysicalAddress }, Will ReadBoardNozzleInitParameters for nozzle { onBoardNozzleNumber}");
  397. var targetBoard = this.Boards.FirstOrDefault(b => b.BoardPhysicalAddress == boardPhysicalAddress && b.Nozzles.Any(n => n.NozzleNumberOnBoard == onBoardNozzleNumber));
  398. if (targetBoard == null)
  399. {
  400. logger.LogInformation($" B PhyAddr: {boardPhysicalAddress }, parameters were read failed for nozzle {onBoardNozzleNumber} due to target board or nozzel does not exists");
  401. return null;
  402. }
  403. var response = await this.context.Outgoing.WriteAsync(
  404. new READ_PARA_Request(onBoardNozzleNumber) { Address = boardPhysicalAddress },
  405. (_, test) => test is READ_PARA_Response r && r.Address == boardPhysicalAddress, 2000);
  406. if (response is READ_PARA_Response readParameterResponse && readParameterResponse != null)
  407. {
  408. logger.LogDebug($" B PhyAddr: {boardPhysicalAddress }, parameters were read successfully for nozzle {onBoardNozzleNumber}: {readParameterResponse.ToLogString()}");
  409. return readParameterResponse;
  410. }
  411. else
  412. {
  413. logger.LogInformation($" B PhyAddr: {boardPhysicalAddress }, parameters were read failed for nozzle {onBoardNozzleNumber}");
  414. return null;
  415. }
  416. }
  417. [UniversalApi(Description = "写入VR board油枪的初始化参数值")]
  418. public async virtual Task<bool> WriteBoardNozzleInitParameters(byte boardPhysicalAddress, byte onBoardNozzleNumber, BoardInitParameterConfigV1 parametersConfig)
  419. {
  420. logger.LogInformation($"B PhyAddr: {boardPhysicalAddress }, Will WriteBoardNozzleInitParameters for nozzle { onBoardNozzleNumber}: {parametersConfig.ToLogString()}");
  421. var targetBoard = this.Boards.FirstOrDefault(b => b.BoardPhysicalAddress == boardPhysicalAddress && b.Nozzles.Any(n => n.NozzleNumberOnBoard == onBoardNozzleNumber));
  422. if (targetBoard == null)
  423. {
  424. logger.LogInformation($" B PhyAddr: {boardPhysicalAddress }, write Parameters failed for nozzle {onBoardNozzleNumber} due to target board or nozzel does not exists");
  425. return false;
  426. }
  427. var response = await context.Outgoing.WriteAsync(
  428. new WRITE_PARA_Request(onBoardNozzleNumber)
  429. {
  430. Address = boardPhysicalAddress,
  431. 最大未变化次数 = parametersConfig.最大未变化次数,
  432. 开始加油阀值 = parametersConfig.开始加油阀值,
  433. 停止加油阀值 = parametersConfig.停止加油阀值,
  434. 最小加油量 = parametersConfig.最小加油量,
  435. 加油脉冲当量 = parametersConfig.加油脉冲当量,
  436. 油气脉冲当量 = parametersConfig.油气脉冲当量,
  437. 气液比值 = parametersConfig.气液比值
  438. },
  439. (_, test) => test is WRITE_PARA_Response r && r.Address == boardPhysicalAddress, 2000);
  440. if (response is WRITE_PARA_Response writeParameterResponse && writeParameterResponse != null)
  441. {
  442. logger.LogInformation($" B PhyAddr: {boardPhysicalAddress }, write Parameters successfully for nozzle {onBoardNozzleNumber}");
  443. return true;
  444. }
  445. else
  446. {
  447. logger.LogInformation($" B PhyAddr: {boardPhysicalAddress }, write Parameters failed for nozzle {onBoardNozzleNumber}");
  448. return false;
  449. }
  450. }
  451. private async Task<bool> ReadAndWriteTimeIfMaxTimeGapReached(byte boardPhysicalAddress)
  452. {
  453. logger.LogInformation(">>>>>B PhyAddr: " + boardPhysicalAddress + ", trying sync time with FCC==========");
  454. var readTimeResponse = await context.Outgoing.WriteAsync(
  455. new READ_TIME_Request() { Address = boardPhysicalAddress },
  456. (request, response) => response is READ_TIME_Response r && r.Address == boardPhysicalAddress, 5000) as READ_TIME_Response;
  457. if (readTimeResponse == null)
  458. {
  459. logger.LogInformation("B PhyAddr: " + boardPhysicalAddress + ", Time was read failed from DataCollector, init failed.");
  460. return false;
  461. }
  462. //this.lastDeviceMessageReceivedTime = DateTime.Now;
  463. logger.LogDebug("B PhyAddr: " + boardPhysicalAddress + ", Time was read from DataCollector board which is: " + readTimeResponse.CurrentTime.ToString("yyyy-MM-dd HH:mm:ss"));
  464. if (Math.Abs(readTimeResponse.CurrentTime.Subtract(DateTime.Now).TotalSeconds) >= maxTimeGapFromFcAndDeviceBySeconds)
  465. {
  466. logger.LogDebug("B PhyAddr: " + boardPhysicalAddress + ", there's " + maxTimeGapFromFcAndDeviceBySeconds +
  467. " seconds time gap detected between DataCollector and local FC, will start time sync...");
  468. var writeTimeResponse = await context.Outgoing.WriteAsync(
  469. new WRITE_TIME_Request(DateTime.Now) { Address = boardPhysicalAddress },
  470. (request0, response0) => response0 is WRITE_TIME_Response r && r.Address == boardPhysicalAddress,
  471. 5000);
  472. if (writeTimeResponse != null)
  473. {
  474. this.Boards.First(b => b.BoardPhysicalAddress == writeTimeResponse.Address).LastIncomingMessageReceivedTime = DateTime.Now;
  475. //this.lastDeviceMessageReceivedTime = DateTime.Now;
  476. logger.LogDebug("B PhyAddr: " + boardPhysicalAddress + ", Sync time succeed!");
  477. return true;
  478. }
  479. else
  480. {
  481. logger.LogInformation("B PhyAddr: " + boardPhysicalAddress + ", Sync time failed!");
  482. return false;
  483. }
  484. }
  485. else
  486. {
  487. logger.LogDebug("B PhyAddr: " + boardPhysicalAddress + ", No need sync time.");
  488. }
  489. return true;
  490. }
  491. private int onReadAndClearHistoryDataFromBoardGuard = 0;
  492. public async override Task Process(IContext<byte[], DataCollectorMessageBase> context)
  493. {
  494. if (!this.DeviceConfig.BoardConfigs.Any(bc => bc.SetPhysicalAddress == context.Incoming.Message.Address))
  495. throw new ArgumentException($"Incoming message from board with PhyAddr: {context.Incoming.Message.Address} does not has definition in this device handler config, may the FC side config wrong here?");
  496. this.Boards.First(b => b.BoardPhysicalAddress == context.Incoming.Message.Address).LastIncomingMessageReceivedTime = DateTime.Now;
  497. switch (context.Incoming.Message)
  498. {
  499. case READ_WORKING_AND_TYPE_Response realTimeStateAndDataResponse:
  500. if (realTimeStateAndDataResponse.NozzleStates?.Any() ?? false)
  501. {
  502. foreach (var newNozzleState in realTimeStateAndDataResponse.NozzleStates)
  503. {
  504. var nozzle =
  505. this.Boards.FirstOrDefault(b => b.BoardPhysicalAddress == realTimeStateAndDataResponse.Address)
  506. ?.Nozzles.FirstOrDefault(n => n.NozzleNumberOnBoard == newNozzleState.Key);
  507. if (nozzle == null)
  508. {
  509. logger.LogError($"Incoming a device msg from Board(on comm: {context.Communicator.Identity ?? ""}, with PhysicalAddress: {realTimeStateAndDataResponse.Address}) for board nozzle(with index:{newNozzleState.Key}) say its state changed to: {newNozzleState.Value}, but no correlated nozzle in FCC, may the config missing in FCC??");
  510. }
  511. else
  512. {
  513. if (nozzle.NozzleState != newNozzleState.Value)
  514. {
  515. logger.LogDebug($"B PhyAddr: { context.Incoming.Message.Address}, Nozzle: {nozzle.NozzleNumberOnBoard} is swtching state from: {nozzle.NozzleState} to: {newNozzleState.Value}");
  516. nozzle.NozzleState = newNozzleState.Value;
  517. this.FireOnBoardNozzleStateChangeEvent(nozzle);
  518. }
  519. }
  520. }
  521. this.FireOnDataReceivedEvent(realTimeStateAndDataResponse);
  522. }
  523. this.CurrentDataCollectorType = realTimeStateAndDataResponse.DataCollectorType;
  524. var withHistoryDataNozzles = realTimeStateAndDataResponse.NozzleHistoryDataStates.Where(h => h.Value == NozzleHistoryDataStateEnum.HasData);
  525. foreach (var n in withHistoryDataNozzles)
  526. {
  527. //context.Outgoing.Write(new READ_HISTORY_NUM_Request(n.Key) { Address = realTimeStateAndDataResponse.Address });
  528. if (0 == Interlocked.CompareExchange(ref this.onReadAndClearHistoryDataFromBoardGuard, 1, 0))
  529. {
  530. var nozzleNumber = n.Key;
  531. var targetBoard = this.Boards.FirstOrDefault(b => b.BoardPhysicalAddress == context.Incoming.Message.Address);
  532. try
  533. {
  534. if (logger.IsEnabled(LogLevel.Debug))
  535. logger.LogDebug($"B PhyAddr: { targetBoard.BoardPhysicalAddress}, Nozzle: {nozzleNumber} has data, will try ReadAndClear...");
  536. var result = await ReadAndClearHistoryDataFromBoard(context, targetBoard, nozzleNumber);
  537. if (logger.IsEnabled(LogLevel.Debug))
  538. logger.LogDebug($"B PhyAddr: { targetBoard.BoardPhysicalAddress}, Nozzle: {nozzleNumber} ReadAndClearHistoryData overall result is: {result}");
  539. }
  540. finally
  541. {
  542. logger.LogDebug($"B PhyAddr: { targetBoard?.BoardPhysicalAddress ?? -1}, Nozzle: {nozzleNumber} released guard");
  543. this.onReadAndClearHistoryDataFromBoardGuard = 0;
  544. }
  545. }
  546. else
  547. {
  548. if (logger.IsEnabled(LogLevel.Debug))
  549. logger.LogDebug($"B PhyAddr: { context.Incoming.Message.Address}, Nozzle: {n.Key} skip this round of ReadAndClear as prev still running, will try later...");
  550. break;
  551. }
  552. }
  553. //this.OnDataRecieved?.Invoke(this, new DataRecievedEventArgs(realTimeStateAndDataResponse));
  554. break;
  555. //case READ_HISTORY_NUM_Response historyNumResponse:
  556. // context.Outgoing.Write(
  557. // new READ_ONE_HISTORY_DATA_Request(historyNumResponse.NozzleHistoryUnreadRecordCount.Key) { Address = historyNumResponse.Address });
  558. // break;
  559. //case READ_ONE_HISTORY_DATA_Response historyDataResponse:
  560. // logger.LogDebug("B PhyAddr: " + historyDataResponse.Address + ", Read a history data from nozzle " + historyDataResponse.NozzleNumber
  561. // + ", LiquidVolume " + historyDataResponse.LiquidVolume
  562. // + ", AirVolume " + historyDataResponse.AirVolume
  563. // + " which trx started from " + historyDataResponse.FuellingStartTime.ToString("yyyy-MM-dd HH:mm:ss")
  564. // + " to " + historyDataResponse.FuellingEndTime.ToString("yyyy-MM-dd HH:mm:ss"));
  565. // var clearResult = await ClearHistory(context, historyDataResponse);
  566. // if (clearResult) this.FireOnDataReceivedEvent(historyDataResponse);
  567. // break;
  568. }
  569. }
  570. private async Task<bool> ReadAndClearHistoryDataFromBoard(IContext<byte[], DataCollectorMessageBase> context, Board board, byte onBoardNozzleNumber)
  571. {
  572. var readHistoryNumResponse = await context.Outgoing.WriteAsync(
  573. new READ_HISTORY_NUM_Request(onBoardNozzleNumber) { Address = board.BoardPhysicalAddress },
  574. (r, testResponse) => testResponse is READ_HISTORY_NUM_Response rp && rp.Address == board.BoardPhysicalAddress && rp.NozzleHistoryUnreadRecordCount.Key == onBoardNozzleNumber,
  575. 1500) as READ_HISTORY_NUM_Response;
  576. if (readHistoryNumResponse == null)
  577. {
  578. if (logger.IsEnabled(LogLevel.Debug))
  579. logger.LogDebug($"B PhyAddr: {board.BoardPhysicalAddress}, Nozzle: {onBoardNozzleNumber} timedout for READ_HISTORY_NUM_Request");
  580. return false;
  581. }
  582. if (readHistoryNumResponse.NozzleHistoryUnreadRecordCount.Value < 1)
  583. {
  584. if (logger.IsEnabled(LogLevel.Debug))
  585. logger.LogDebug($"B PhyAddr: {board.BoardPhysicalAddress}, Nozzle: {onBoardNozzleNumber} indicates no un-read data?!");
  586. return false;
  587. }
  588. int retryingTimes = 0;
  589. retryReadData:
  590. var operationResponse = await context.Outgoing.WriteAsync(
  591. new READ_ONE_HISTORY_DATA_Request(onBoardNozzleNumber) { Address = board.BoardPhysicalAddress },
  592. (r, testResponse) => (testResponse is READ_ONE_HISTORY_DATA_Response rp && rp.Address == board.BoardPhysicalAddress && rp.NozzleNumber == onBoardNozzleNumber)
  593. || (testResponse is COLLECTOR_BUSY_Response bRp && bRp.Address == board.BoardPhysicalAddress),
  594. 1500);
  595. if (operationResponse == null)
  596. {
  597. if (logger.IsEnabled(LogLevel.Debug))
  598. logger.LogDebug($"B PhyAddr: {board.BoardPhysicalAddress}, Nozzle: {onBoardNozzleNumber} timedout for READ_ONE_HISTORY_DATA_Request");
  599. return false;
  600. }
  601. else if (operationResponse is COLLECTOR_BUSY_Response)
  602. {
  603. //0 indicates disabled for retry.
  604. int maxRetryTimes = 0;
  605. retryingTimes++;
  606. if (retryingTimes > maxRetryTimes)
  607. {
  608. logger.LogInformation($"B PhyAddr: {board.BoardPhysicalAddress}, Nozzle: {onBoardNozzleNumber} report busy and deny for READ_ONE_HISTORY_DATA_Request");
  609. return false;
  610. }
  611. if (logger.IsEnabled(LogLevel.Debug))
  612. logger.LogDebug($"B PhyAddr: {board.BoardPhysicalAddress}, Nozzle: {onBoardNozzleNumber} Retrying for READ_ONE_HISTORY_DATA_Request...");
  613. goto retryReadData;
  614. }
  615. var historyDataResponse = operationResponse as READ_ONE_HISTORY_DATA_Response;
  616. logger.LogDebug($"B PhyAddr: { historyDataResponse.Address }, Nozzle {historyDataResponse.NozzleNumber} Read a history data"
  617. + $", LiquidVolume: {historyDataResponse.LiquidVolume}, AirVolume: { historyDataResponse.AirVolume}"
  618. + $", trx started from { historyDataResponse.FuellingStartTime.ToString("yyyy-MM-dd HH:mm:ss")}"
  619. + $" to { historyDataResponse.FuellingEndTime.ToString("yyyy-MM-dd HH:mm:ss")}, will clear...");
  620. var clearHistoryResponse = await context.Outgoing.WriteAsync(
  621. new CLEAR_ONE_HISTORY_Request(historyDataResponse.NozzleNumber)
  622. { Address = historyDataResponse.Address },
  623. (request, response) => response is CLEAR_ONE_HISTORY_Response r && r.Address == historyDataResponse.Address, 1500) as CLEAR_ONE_HISTORY_Response;
  624. if (clearHistoryResponse != null)
  625. {
  626. if (logger.IsEnabled(LogLevel.Debug))
  627. logger.LogDebug($"B PhyAddr: {historyDataResponse.Address}, Nozzle: {historyDataResponse.NozzleNumber} One history has been cleared");
  628. this.FireOnDataReceivedEvent(historyDataResponse);
  629. return true;
  630. }
  631. else
  632. {
  633. logger.LogInformation($"B PhyAddr: { historyDataResponse.Address }, Nozzle: {historyDataResponse.NozzleNumber} Previous clear One history request failed");
  634. return false;
  635. }
  636. }
  637. [UniversalApi(Description = "Get state for all boards under this comm channel")]
  638. public Task<List<Board>> GetBoardStates()
  639. {
  640. return Task.FromResult(this.Boards);
  641. }
  642. [UniversalApi(Description = "Send a query right now to all boards under this comm channel, and return the datas, if read timedout for certain board, the specific data will not be returned.")]
  643. public async Task<IEnumerable<READ_WORKING_AND_TYPE_Response>> ReadRealTimeDatas()
  644. {
  645. List<READ_WORKING_AND_TYPE_Response> results = new List<READ_WORKING_AND_TYPE_Response>();
  646. var addresses = this.DeviceConfig.BoardConfigs.Select(bc => bc.SetPhysicalAddress).ToList();
  647. foreach (var adrs in addresses)
  648. {
  649. var rsp = await this.context.Outgoing.WriteAsync(
  650. new READ_WORKING_TYPE_Request()
  651. {
  652. Address = adrs
  653. },
  654. (request, testingResponse) => testingResponse is READ_WORKING_AND_TYPE_Response p && p.Address == adrs,
  655. 4000) as READ_WORKING_AND_TYPE_Response;
  656. if (rsp != null)
  657. results.Add(rsp);
  658. }
  659. return results;
  660. }
  661. public IEnumerator<Board> GetEnumerator()
  662. {
  663. return this.Boards.GetEnumerator();
  664. }
  665. System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
  666. {
  667. return this.Boards.GetEnumerator();
  668. }
  669. public virtual void Dispose()
  670. {
  671. this.singleBoardOfflineCheckTimer?.Dispose();
  672. foreach (var d in this.OnBoardStateChange?.GetInvocationList() ?? new Delegate[] { })
  673. {
  674. this.OnBoardStateChange -= d as EventHandler<BoardStateChangeEventArgs>;
  675. }
  676. foreach (var d in this.OnBoardNozzleStateChange?.GetInvocationList() ?? new Delegate[] { })
  677. {
  678. this.OnBoardNozzleStateChange -= d as EventHandler<BoardNozzleStateChangeEventArgs>;
  679. }
  680. foreach (var d in this.OnDataRecieved?.GetInvocationList() ?? new Delegate[] { })
  681. {
  682. this.OnDataRecieved -= d as EventHandler<DataRecievedEventArgs>;
  683. }
  684. }
  685. }
  686. public enum BoardStateEnum
  687. {
  688. /// <summary>
  689. /// first time eastalized the connection with FC, or disconnected from a established connection.
  690. /// </summary>
  691. UnInit,
  692. /// <summary>
  693. /// processing and not ready for other command.
  694. /// </summary>
  695. Initializing,
  696. Initialized,
  697. }
  698. public class BoardNozzle
  699. {
  700. public byte NozzleNumberOnBoard { get; set; }
  701. /// <summary>
  702. /// each vr watching nozzle is a physical pump nozzle, here is the bind info, will be used to draw UI.
  703. /// </summary>
  704. public int SiteLevelNozzleId { get; set; }
  705. public NozzleStateEnum NozzleState { get; set; }
  706. }
  707. public class Board
  708. {
  709. public int SerialNumber { get; }
  710. public byte BoardPhysicalAddress { get; }
  711. public IEnumerable<BoardNozzle> Nozzles { get; }
  712. public BoardStateEnum State { get; set; }
  713. /// <summary>
  714. /// used for track the last received message which for caculate the offline state of this device.
  715. /// </summary>
  716. public DateTime LastIncomingMessageReceivedTime { get; set; }
  717. internal Board(int serialNumber, byte boardPhysicalAddress, BoardStateEnum state, IEnumerable<BoardNozzle> nozzles)
  718. {
  719. this.SerialNumber = serialNumber;
  720. this.BoardPhysicalAddress = boardPhysicalAddress;
  721. this.State = state;
  722. this.Nozzles = nozzles;
  723. }
  724. }
  725. }