Handler.cs 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. using Edge.Core.Processor;
  2. using Edge.Core.IndustryStandardInterface.Pump;
  3. using Edge.Core.IndustryStandardInterface.ATG;
  4. using Edge.Core.UniversalApi;
  5. using Microsoft.Extensions.DependencyInjection;
  6. using Microsoft.Extensions.Logging;
  7. using Microsoft.Extensions.Logging.Abstractions;
  8. using System;
  9. using System.Collections.Generic;
  10. using System.Linq;
  11. using System.Threading;
  12. using System.Threading.Tasks;
  13. using VeederRoot_ATG_Console.MessageEntity;
  14. using VeederRoot_ATG_Console.MessageEntity.DispenserInterface.Outgoing;
  15. using VeederRoot_ATG_Console.MessageEntity.Incoming;
  16. using VeederRoot_ATG_Console.MessageEntity.Outgoing;
  17. using Wayne.FDCPOSLibrary;
  18. using static VeederRoot_ATG_Console.MessageEntity.Incoming.QueryInTankInventoryReportResponse.InventoryData;
  19. using static VeederRoot_ATG_Console.MessageEntity.Incoming.QueryInTankStatusReportResponse;
  20. using Edge.Core.Processor.Dispatcher.Attributes;
  21. using Edge.Core.Processor.Communicator;
  22. namespace VeederRoot_ATG_Console
  23. {
  24. [UniversalApi(Name = "OnStateChange", EventDataType = typeof(AtgStateChangeEventArg))]
  25. [UniversalApi(Name = GenericAlarm.UniversalApiEventName, EventDataType = typeof(GenericAlarm[]), Description = "Fire GenericAlarms to AlarmBar for attracting users.")]
  26. [MetaPartsRequired(typeof(HalfDuplexActivePollingDeviceProcessor<,>))]
  27. [MetaPartsRequired(typeof(ComPortCommunicator<>))]
  28. [MetaPartsRequired(typeof(TcpClientCommunicator<>))]
  29. [MetaPartsRequired(typeof(TcpServerCommunicator<>))]
  30. [MetaPartsDescriptor(
  31. "lang-zh-cn:维德路特液位仪lang-en-us:VeederRoot ATG",
  32. "lang-zh-cn:用于驱动维德路特协议(或兼容)的液位仪控制台lang-en-us:Used for driven ATG console that use VeederRoot ATG protocol(or compatible with)",
  33. new[] { "lang-zh-cn:液位仪lang-en-us:ATG" })]
  34. public class Handler : TestableActivePollingDeviceHandler<byte[], MessageBaseGeneric>, IAutoTankGaugeController
  35. {
  36. private ILogger logger = NullLogger.Instance;
  37. /// <summary>
  38. /// Range from 0 to 9.
  39. /// Event Message Identifier.
  40. /// Start Events and Stop Events contain event IDs to help the dispenser
  41. /// interface module identify transmissions that are repeated as a result of
  42. /// communication errors.
  43. /// Once an event report (start or end) is successfully transmitted,
  44. /// the Event Message ID must change so the next event report (start or end)
  45. /// will get a new ID in the range 0 - 9.
  46. /// An event report must keep the same ID until it is successfully transmitted.
  47. /// The status report does not require an ID.
  48. /// </summary>
  49. public static byte nextRotateEventId;
  50. private DateTime lastLogicalDeviceStateReceivedTime = DateTime.Now;
  51. // by seconds, change this value need change the correlated deviceOfflineCountdownTimer's interval as well
  52. public const int lastLogicalDeviceStateExpiredTime = 15;
  53. private System.Timers.Timer deviceOfflineCountdownTimer;
  54. private IContext<byte[], MessageBaseGeneric> context;
  55. private IEnumerable<Tank> tanks;
  56. /// <summary>
  57. /// </summary>
  58. private int loadAsync_Guard = 0;
  59. private IServiceProvider services;
  60. public string MetaConfigName => "VeederRoot_ATG_Console";
  61. public IEnumerable<Tank> Tanks => this.tanks;
  62. private int deviceId;
  63. public event EventHandler<AtgStateChangeEventArg> OnStateChange;
  64. public event EventHandler<AtgAlarmEventArg> OnAlarm;
  65. public int DeviceId => this.deviceId;
  66. public SystemUnit SystemUnit { get; private set; }
  67. public AtgState State { get; private set; } = AtgState.Offline;
  68. #region UniversalApi Service
  69. [UniversalApi()]
  70. public Task<IEnumerable<Tank>> GetTanksAsync()
  71. {
  72. return Task.FromResult(this.tanks);
  73. }
  74. #endregion
  75. public Handler(int deviceId, IServiceProvider services)
  76. {
  77. this.services = services;
  78. var loggerFactory = services.GetRequiredService<ILoggerFactory>();
  79. this.logger = loggerFactory.CreateLogger("PumpHandler");
  80. this.deviceId = deviceId;
  81. this.deviceOfflineCountdownTimer = new System.Timers.Timer(3000);
  82. this.deviceOfflineCountdownTimer.Elapsed += async (_, __) =>
  83. {
  84. if (DateTime.Now.Subtract(this.lastLogicalDeviceStateReceivedTime).TotalSeconds
  85. >= lastLogicalDeviceStateExpiredTime)
  86. {
  87. if (this.State != AtgState.Offline)
  88. {
  89. this.State = AtgState.Offline;
  90. logger.LogInformation("VeederRoot ATG Console with Id: " + this.deviceId + ", " + " State switched to OFFLINE due to long time no see data incoming");
  91. var onStateChangeEventArg = new AtgStateChangeEventArg(this.State, "VR ATG switched to Offline due to device offline timer timeout reached");
  92. this.OnStateChange?.Invoke(this, onStateChangeEventArg);
  93. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  94. await universalApiHub.FireEvent(this.context.Processor, "OnStateChange", onStateChangeEventArg);
  95. }
  96. }
  97. };
  98. this.deviceOfflineCountdownTimer.Start();
  99. }
  100. /// <summary>
  101. /// Notify the Veeder Root ATG console that a fueling has started or done.
  102. /// </summary>
  103. /// <param name="startOrStopEventReportRequest">start or stop event which for notify ATG console.</param>
  104. /// <returns></returns>
  105. public async Task<MessageEntity.DispenserInterface.Incoming.GenericResponse>
  106. NotifyFuelTrxEventToAtgConsoleAsync(MessageEntity.DispenserInterface.Outgoing.OutgoingMessageBase startOrStopEventReportRequest)
  107. {
  108. MessageEntity.DispenserInterface.Incoming.GenericResponse response = null;
  109. startOrStopEventReportRequest.EventId = nextRotateEventId++;
  110. if (nextRotateEventId > 9)
  111. nextRotateEventId = 0;
  112. await Task.Factory.StartNew(() =>
  113. {
  114. ManualResetEvent block = new ManualResetEvent(false);
  115. this.context.Outgoing.WriteAsync(
  116. startOrStopEventReportRequest,
  117. (req, resp) => resp is MessageEntity.DispenserInterface.Incoming.GenericResponse,
  118. (req, resp) =>
  119. {
  120. if (resp != null)
  121. {
  122. var data = resp as MessageEntity.DispenserInterface.Incoming.GenericResponse;
  123. response = data;
  124. }
  125. else
  126. logger.LogError("NotifyFuelTrxEventToAtgConsoleAsync failed with timed out");
  127. block.Set();
  128. }, 2000);
  129. block.WaitOne(5 * 1000);
  130. });
  131. return response;
  132. }
  133. public override void Init(IContext<byte[], MessageBaseGeneric> context)
  134. {
  135. base.Init(context);
  136. this.context = context;
  137. this.context.Processor.Communicator.OnDisconnected += async (_, __) =>
  138. {
  139. if (this.State != AtgState.Offline)
  140. {
  141. this.State = AtgState.Offline;
  142. logger.LogInformation("VeederRoot ATG Console with Id: " + this.deviceId + ", " + " State switched to OFFLINE due to Communicator.OnDisconnected event received");
  143. var onStateChangeEventArg = new AtgStateChangeEventArg(this.State, "VR ATG switched to Offline due to Communicator OnDisconnected");
  144. this.OnStateChange?.Invoke(this, onStateChangeEventArg);
  145. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  146. await universalApiHub.FireEvent(this.context.Processor, "OnStateChange", onStateChangeEventArg);
  147. }
  148. };
  149. var timeWindowWithActivePollingOutgoing =
  150. this.context.Outgoing as TimeWindowWithActivePollingOutgoing<byte[], MessageBaseGeneric>;
  151. DateTime? lastPollingRequestSendTime = null;
  152. VeederRoot_ATG_Console.MessageEntity.Outgoing.OutgoingMessageBase lastPollingMessage = null;
  153. timeWindowWithActivePollingOutgoing.PollingMsgProducer = () =>
  154. {
  155. /* for quickly send out the request in polling queue to real device,
  156. * the polling interval configurated in TimeWindowWithActivePollingOutgoing would not be set long, say 500ms,
  157. * but, this also make this PollingMsgProducer called too fast which may not proper for this VR device,
  158. * so below we're trying to slow it down to 2 seconds.
  159. */
  160. if (lastPollingRequestSendTime == null)
  161. {
  162. lastPollingRequestSendTime = DateTime.Now;
  163. var polling = new QueryTimeOfDayRequest(MessageBase.MessageFormat.Computer);
  164. return polling;
  165. }
  166. else if (DateTime.Now.Subtract(lastPollingRequestSendTime.Value).TotalSeconds >= 2)
  167. {
  168. /* every 2 seconds query tanks' alarms and status, the response handle will be in function: Process(..., ...) */
  169. lastPollingRequestSendTime = DateTime.Now;
  170. if (lastPollingMessage == null || lastPollingMessage is QueryInTankStatusReportRequest)
  171. {
  172. /* for query tank status, like if in delivery or not*/
  173. lastPollingMessage = new QueryInTankInventoryReportRequest(MessageBase.MessageFormat.Computer, 0);
  174. }
  175. else
  176. {
  177. /* for query tank alarms*/
  178. lastPollingMessage = new QueryInTankStatusReportRequest(MessageBase.MessageFormat.Computer, 0);
  179. }
  180. return lastPollingMessage;
  181. }
  182. return null;
  183. };
  184. }
  185. public override async Task Process(IContext<byte[], MessageBaseGeneric> context)
  186. {
  187. this.lastLogicalDeviceStateReceivedTime = DateTime.Now;
  188. if (this.State == AtgState.Offline)
  189. {
  190. if (1 == Interlocked.CompareExchange(ref this.loadAsync_Guard, 1, 0))
  191. return;
  192. try
  193. {
  194. logger.LogInformation($"VR ATG is switching From Offline to Idle by received a Device Msg: {(context.Incoming.Message?.ToLogString() ?? "")}, will reloading tanks...");
  195. this.tanks = await this.LoadAsync();
  196. if (this.tanks == null)
  197. {
  198. logger.LogInformation($"VR ATG switching to Idle failed as failure in Load tanks, will treat ATG still in Offline");
  199. return;
  200. }
  201. logger.LogInformation($"Loaded Tanks info(total: {this.tanks.Count()}): " + Environment.NewLine +
  202. this.tanks.Select(t =>
  203. $"Tank with TankNumber: { t.TankNumber} => ProductCode: { (t.Product?.ProductCode ?? "")}, " +
  204. $"ProductLabel: {t.Product?.ProductLabel ?? ""}, Diameter: {t.Diameter ?? -1}, " +
  205. $"TankState: {t.State}, ProbeLength: {t.Probe.ProbeLength}, ProbeState: {t.Probe.State ?? ""}" +
  206. //$"ProbeReadings: ({t.Probe.ProbeReading.ToString()})
  207. "").Aggregate((acc, n) => acc + Environment.NewLine + n));
  208. this.State = AtgState.Idle;
  209. }
  210. catch (Exception eeee)
  211. {
  212. logger.LogError($"VR ATG switching to Idle got exception: {eeee}{Environment.NewLine}Will treat ATG still in Offline");
  213. return;
  214. }
  215. finally
  216. {
  217. this.loadAsync_Guard = 0;
  218. }
  219. var onStateChangeEventArg = new AtgStateChangeEventArg(AtgState.TanksReloaded, "VR ATG switched from Offline to Online due to Process(...) get called");
  220. this.OnStateChange?.Invoke(this, onStateChangeEventArg);
  221. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  222. await universalApiHub.FireEvent(this.context.Processor, "OnStateChange", onStateChangeEventArg);
  223. }
  224. if (context.Incoming.Message is QueryInTankStatusReportResponse pollingQueryTankAlarmResponse)
  225. {
  226. var alarmsForTanks = pollingQueryTankAlarmResponse?.TanksOfAlarms.Select(ta =>
  227. {
  228. var alarms = new List<Edge.Core.IndustryStandardInterface.ATG.Alarm>();
  229. alarms.AddRange(ta?.Alarms.Select(aType => new Edge.Core.IndustryStandardInterface.ATG.Alarm()
  230. {
  231. TankNumber = (byte)ta.TankNumber,
  232. Priority = AlarmPriority.Alarm,
  233. Type = aType,
  234. CreatedTimeStamp = DateTime.Now,
  235. }));
  236. return alarms;
  237. });
  238. foreach (var alarmsForTank in alarmsForTanks)
  239. {
  240. var alarmEventArg = new AtgAlarmEventArg(
  241. alarmsForTank.First().TankNumber,
  242. alarmsForTank.Select(alarm => new Alarm()
  243. {
  244. TankNumber = alarmsForTank.First().TankNumber,
  245. Priority = AlarmPriority.Alarm,
  246. Type = alarm.Type,
  247. CreatedTimeStamp = DateTime.Now,
  248. Description = alarmsForTank.First().TankNumber + "号油罐 报告其状态为:" + alarm.Type
  249. }));
  250. this.OnAlarm?.Invoke(this, alarmEventArg);
  251. }
  252. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  253. await universalApiHub.FireEvent(this.context.Processor, GenericAlarm.UniversalApiEventName,
  254. alarmsForTanks.SelectMany(als => als, (als, al) =>
  255. new GenericAlarm()
  256. {
  257. Category = "VeederRoot ATG Alarms from tanks",
  258. Title = $"Tank {al.TankNumber} is alarming",
  259. Detail = $"{al.Description}",
  260. Severity = (al.Priority == AlarmPriority.Alarm ? GenericAlarmSeverity.Error : GenericAlarmSeverity.Warning),
  261. }));
  262. }
  263. else if (context.Incoming.Message is QueryInTankInventoryReportResponse pollingQueryTankStateResponse
  264. && pollingQueryTankStateResponse.InventoryDatas != null)
  265. {
  266. foreach (var data in pollingQueryTankStateResponse.InventoryDatas)
  267. {
  268. var targetTank = this.tanks.FirstOrDefault(t => t.TankNumber == (byte)(data.TankNumber ?? -1));
  269. if (targetTank == null) continue;
  270. var deliveryingStateReportedFromDevice =
  271. data.States?.Exists(s => s == QueryInTankInventoryReportResponse.InventoryData.TankState.DeliveryInProgress_LSB) ?? false;
  272. if (deliveryingStateReportedFromDevice
  273. && targetTank.State != Edge.Core.IndustryStandardInterface.ATG.TankState.Delivering)
  274. {
  275. targetTank.State = Edge.Core.IndustryStandardInterface.ATG.TankState.Delivering;
  276. this.logger.LogInformation("VR ATG has a tank entered AtgTankState.Delivering due to TankState.DeliveryInProgress_LSB reported for tankNumber: " + (data.TankNumber ?? -1));
  277. }
  278. else if (!deliveryingStateReportedFromDevice
  279. && targetTank.State == Edge.Core.IndustryStandardInterface.ATG.TankState.Delivering)
  280. {
  281. targetTank.State = Edge.Core.IndustryStandardInterface.ATG.TankState.Idle;
  282. this.logger.LogInformation("VR ATG has a tank quit from AtgTankState.Delivering for tankNumber: " + (data.TankNumber ?? -1));
  283. }
  284. else return;
  285. var evtArg = new AtgStateChangeEventArg(targetTank, this.State, "the target tank changed tank state to: " + targetTank.State);
  286. this.OnStateChange?.Invoke(this, evtArg);
  287. var universalApiHub = this.services.GetRequiredService<UniversalApiHub>();
  288. await universalApiHub.FireEvent(this.context.Processor, "OnStateChange", evtArg);
  289. }
  290. }
  291. }
  292. private async Task<IEnumerable<Tank>> LoadAsync()
  293. {
  294. this.logger.LogInformation("VR ATG is starting LoadAsync()...");
  295. var _ = await this.context.Outgoing.WriteAsync(
  296. new QuerySystemTypeAndLanguageFlagsRequest(MessageBase.MessageFormat.Computer),
  297. (req, testResp) => testResp is QueryOrSetSystemTypeAndLanguageFlagsResponse, 5000);
  298. if (_ is QueryOrSetSystemTypeAndLanguageFlagsResponse querySystemTypeAndLanguageFlagsResponse)
  299. {
  300. this.lastLogicalDeviceStateReceivedTime = DateTime.Now;
  301. this.logger.LogInformation($" VR ATG, querySystemTypeAndLanguageFlagsResponse: {querySystemTypeAndLanguageFlagsResponse.ToLogString()}");
  302. var timeGapBySeconds = DateTime.Now.Subtract((querySystemTypeAndLanguageFlagsResponse.CurrentDateAndTime ?? DateTime.Now)).TotalSeconds;
  303. if (timeGapBySeconds >= 60)
  304. {
  305. this.logger.LogInformation($" VR ATG has its CurrentDateAndTime differency from fusion time with gap of TotalSeconds: " +
  306. $"{timeGapBySeconds}, will Send SetTimeOfDayRequest for align 2 sides...");
  307. var setTimeResponse = await this.context.Outgoing.WriteAsync(
  308. new SetTimeOfDayRequest(MessageBase.MessageFormat.Computer, DateTime.Now),
  309. (req, testResp) => testResp is MessageBaseGeneric, 5000);
  310. if (setTimeResponse == null)
  311. this.logger.LogInformation($" VR ATG SetTimeOfDayRequest failed due to response timedout");
  312. }
  313. }
  314. else
  315. {
  316. _ = await this.context.Outgoing.WriteAsync(
  317. new QuerySystemTypeAndLanguageFlags_Extended(MessageBase.MessageFormat.Computer),
  318. (req, testResp) => testResp is QueryOrSetSystemTypeAndLanguageFlagsResponse, 3000);
  319. if (_ is QueryOrSetSystemTypeAndLanguageFlagsResponse querySystemTypeAndLanguageFlagsResponse_Extended)
  320. {
  321. this.lastLogicalDeviceStateReceivedTime = DateTime.Now;
  322. this.logger.LogInformation($" VR ATG QuerySystemTypeAndLanguageFlags_Extended responded: {querySystemTypeAndLanguageFlagsResponse_Extended.ToLogString()}");
  323. this.SystemUnit = querySystemTypeAndLanguageFlagsResponse_Extended?.SystemUnits ?? SystemUnit.Metric;
  324. }
  325. else
  326. this.logger.LogInformation(" Seems this ATG console does not " +
  327. "support 'QuerySystemTypeAndLanguageFlags' since the query returned with response: " + (_?.ToLogString() ?? "") +
  328. Environment.NewLine + "no way to know the ATG Console System's Unit");
  329. }
  330. var queryInTankInventoryReportResponse =
  331. await this.context.Outgoing.WriteAsync(
  332. new QueryInTankInventoryReportRequest(MessageBase.MessageFormat.Computer, 0),
  333. (req, testResp) => testResp is QueryInTankInventoryReportResponse, 4000) as QueryInTankInventoryReportResponse;
  334. //var queryInTankDiagnosticReportResponse = _ as QueryInTankDiagnosticReportResponse;
  335. if (queryInTankInventoryReportResponse == null)
  336. {
  337. this.logger.LogInformation(" QueryInTankInventoryReportResponse timed out, treat as ReloadTanks failed.");
  338. return null;
  339. //below request is used for get probe info
  340. var queryInTankDiagnosticReportResponse = await this.context.Outgoing.WriteAsync(
  341. new QueryInTankDiagnosticReportRequest(MessageBase.MessageFormat.Computer, 0),
  342. (req, testResp) => testResp is QueryInTankDiagnosticReportResponse, 4000) as QueryInTankDiagnosticReportResponse;
  343. }
  344. _ = await this.context.Outgoing.WriteAsync(
  345. new QueryTankDiameterRequest(MessageBase.MessageFormat.Computer, 0),
  346. (req, testResp) => testResp is QueryOrSetTankDiameterResponse, 4000);
  347. var queryOrSetTankDiameterResponse = _ as QueryOrSetTankDiameterResponse;
  348. if (queryOrSetTankDiameterResponse == null)
  349. {
  350. this.logger.LogInformation(" QueryOrSetTankDiameterResponse timed out, so diameter value will not presents.");
  351. }
  352. _ = await this.context.Outgoing.WriteAsync(
  353. new QueryTankProductLabelRequest(MessageBase.MessageFormat.Computer, 0),
  354. (req, testResp) => testResp is QueryOrSetTankProductLabelResponse, 5000);
  355. var queryOrSetTankProductLabelResponse = _ as QueryOrSetTankProductLabelResponse;
  356. if (queryOrSetTankProductLabelResponse == null)
  357. {
  358. this.logger.LogInformation(" QueryOrSetTankProductLabelResponse timed out, so product name value will not presents.");
  359. }
  360. //this.logger.LogInformation(queryOrSetTankProductLabelResponse.ToLogString());
  361. //var probeReadings = await this.GetTankProbeReadingsAsync(null);
  362. var innerTanks = new List<Tank>();
  363. foreach (var inventoryData in queryInTankInventoryReportResponse.InventoryDatas)
  364. {
  365. innerTanks.Add(
  366. new Tank()
  367. {
  368. TankNumber = (byte)(inventoryData?.TankNumber ?? -1),
  369. State = Edge.Core.IndustryStandardInterface.ATG.TankState.Idle,
  370. Diameter = queryOrSetTankDiameterResponse?.Diameters?.FirstOrDefault(pl => pl.Item1 == (inventoryData.TankNumber ?? -1))?.Item2 ?? -1,
  371. Product = new Product()
  372. {
  373. ProductCode = inventoryData.ProductCode?.ToString() ?? "",
  374. ProductLabel = queryOrSetTankProductLabelResponse?.ProductLabels?.FirstOrDefault(pl => pl.Item1 == (inventoryData.TankNumber ?? -1))?.Item2 ?? ""
  375. },
  376. Limit = new TankLimit()
  377. {
  378. },
  379. Probe = new Probe()
  380. {
  381. ProbeLength = -1,
  382. //ProbeReading = probeReadings.First(pl => pl.Item1 == probe.TankNumber).Item2,
  383. },
  384. });
  385. }
  386. return innerTanks;
  387. }
  388. /// <summary>
  389. ///
  390. /// </summary>
  391. /// <param name="tankNumber">0 or null for query all tanks</param>
  392. /// <returns></returns>
  393. [UniversalApi(Description = "", InputParametersExampleJson = "[1]")]
  394. public async Task<TankReading> GetTankReadingAsync(int tankNumber)
  395. {
  396. //some china produced ATG may not support specific tank number query, so here have to pass in hardcoded 0 for query all tanks, and then filter out
  397. //the data for target tank
  398. var inventoryReportResponse = await this.context.Outgoing.WriteAsync(
  399. new QueryInTankInventoryReportRequest(MessageBase.MessageFormat.Computer, 0),
  400. (req, testResp) => testResp is QueryInTankInventoryReportResponse r, 5000) as QueryInTankInventoryReportResponse;
  401. if (inventoryReportResponse == null) throw new TimeoutException($"GetTankReadingAsync, QueryInTankInventoryReportRequest timedout");
  402. this.lastLogicalDeviceStateReceivedTime = DateTime.Now;
  403. if (tankNumber != 0)
  404. {
  405. var targetTankData = inventoryReportResponse.InventoryDatas.FirstOrDefault(id => id.TankNumber == tankNumber);
  406. if (targetTankData == null) throw new ArgumentException($"GetTankReadingAsync, Target tank with number: {tankNumber} does not have InventoryReport queried, you may input the not existing tank number?");
  407. }
  408. //if caller input tanknumber 0, then randomly give back a single data, otherwise filter out the data by tanknumber.
  409. return inventoryReportResponse.InventoryDatas.Where(id => tankNumber == 0 ? true : id.TankNumber == tankNumber)
  410. .Select(rd =>
  411. {
  412. #region parse each fields
  413. int tankNumber = rd.TankNumber ?? -1;
  414. double? volume
  415. = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Volume)
  416. ? rd.TankReadingDatas[TankReadingDataName.Volume]
  417. : default(double?);
  418. double? tc_Volume
  419. = rd.TankReadingDatas.ContainsKey(TankReadingDataName.TC_Volume)
  420. ? rd.TankReadingDatas[TankReadingDataName.TC_Volume]
  421. : default(double?);
  422. double? ullage
  423. = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Ullage)
  424. ? rd.TankReadingDatas[TankReadingDataName.Ullage]
  425. : default(double?);
  426. double? height
  427. = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Height)
  428. ? rd.TankReadingDatas[TankReadingDataName.Height]
  429. : default(double?);
  430. double? water
  431. = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Water)
  432. ? rd.TankReadingDatas[TankReadingDataName.Water]
  433. : default(double?);
  434. double? temperature
  435. = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Temperature)
  436. ? rd.TankReadingDatas[TankReadingDataName.Temperature]
  437. : default(double?);
  438. double? waterVolume
  439. = rd.TankReadingDatas.ContainsKey(TankReadingDataName.WaterVolume)
  440. ? rd.TankReadingDatas[TankReadingDataName.WaterVolume]
  441. : default(double?);
  442. #endregion
  443. //this.logger.LogInformation("QueryInTankInventoryReportDatas: "
  444. // + "TankNumber: " + ((rd?.TankNumber.Value.ToString()) ?? "")
  445. // + ", ProductCode: " + ((rd?.ProductCode.Value.ToString()) ?? "")
  446. // + ", State: " + (rd.States.Any() ? rd.States.First().ToString() : "")
  447. // + ", Volume: " + (volume ?? -1)
  448. // + ", TcVolume: " + (tc_Volume ?? -1)
  449. // + ", Ullage: " + (ullage ?? -1)
  450. // + ", Height: " + (height ?? -1)
  451. // + ", Water: " + (water ?? -1)
  452. // + ", Temperature: " + (temperature ?? -1)
  453. // + ", WaterVolume: " + (waterVolume ?? -1));
  454. var tankReading = new TankReading()
  455. {
  456. TankNumber = tankNumber,
  457. Volume = volume,
  458. TcVolume = tc_Volume,
  459. Ullage = ullage,
  460. Height = height,
  461. Water = water,
  462. Temperature = temperature,
  463. WaterVolume = waterVolume
  464. };
  465. return tankReading;
  466. }).FirstOrDefault();
  467. }
  468. /// <summary>
  469. /// Get reading for all tanks.
  470. /// </summary>
  471. /// <returns>reading for all tanks</returns>
  472. [UniversalApi(Description = "Get all tanks' reading")]
  473. public async Task<IEnumerable<TankReading>> GetTanksReadingAsync()
  474. {
  475. //some china produced ATG may not support specific tank number query, so here have to pass in hardcoded 0 for query all tanks, and then filter out
  476. //the data for target tank
  477. var inventoryReportResponse = await this.context.Outgoing.WriteAsync(
  478. new QueryInTankInventoryReportRequest(MessageBase.MessageFormat.Computer, 0),
  479. (req, testResp) => testResp is QueryInTankInventoryReportResponse r, 5000) as QueryInTankInventoryReportResponse;
  480. if (inventoryReportResponse == null) throw new TimeoutException($"GetTanksReadingAsync timedout");
  481. this.lastLogicalDeviceStateReceivedTime = DateTime.Now;
  482. return inventoryReportResponse.InventoryDatas.Select(rd =>
  483. {
  484. #region parse each fields
  485. int tankNumber = rd.TankNumber ?? -1;
  486. double? volume
  487. = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Volume)
  488. ? rd.TankReadingDatas[TankReadingDataName.Volume]
  489. : default(double?);
  490. double? tc_Volume
  491. = rd.TankReadingDatas.ContainsKey(TankReadingDataName.TC_Volume)
  492. ? rd.TankReadingDatas[TankReadingDataName.TC_Volume]
  493. : default(double?);
  494. double? ullage
  495. = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Ullage)
  496. ? rd.TankReadingDatas[TankReadingDataName.Ullage]
  497. : default(double?);
  498. double? height
  499. = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Height)
  500. ? rd.TankReadingDatas[TankReadingDataName.Height]
  501. : default(double?);
  502. double? water
  503. = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Water)
  504. ? rd.TankReadingDatas[TankReadingDataName.Water]
  505. : default(double?);
  506. double? temperature
  507. = rd.TankReadingDatas.ContainsKey(TankReadingDataName.Temperature)
  508. ? rd.TankReadingDatas[TankReadingDataName.Temperature]
  509. : default(double?);
  510. double? waterVolume
  511. = rd.TankReadingDatas.ContainsKey(TankReadingDataName.WaterVolume)
  512. ? rd.TankReadingDatas[TankReadingDataName.WaterVolume]
  513. : default(double?);
  514. #endregion
  515. //this.logger.LogInformation("QueryInTankInventoryReportDatas: "
  516. // + "TankNumber: " + ((rd?.TankNumber.Value.ToString()) ?? "")
  517. // + ", ProductCode: " + ((rd?.ProductCode.Value.ToString()) ?? "")
  518. // + ", State: " + (rd.States.Any() ? rd.States.First().ToString() : "")
  519. // + ", Volume: " + (volume ?? -1)
  520. // + ", TcVolume: " + (tc_Volume ?? -1)
  521. // + ", Ullage: " + (ullage ?? -1)
  522. // + ", Height: " + (height ?? -1)
  523. // + ", Water: " + (water ?? -1)
  524. // + ", Temperature: " + (temperature ?? -1)
  525. // + ", WaterVolume: " + (waterVolume ?? -1));
  526. var tankReading = new TankReading()
  527. {
  528. TankNumber = tankNumber,
  529. Volume = volume,
  530. TcVolume = tc_Volume,
  531. Ullage = ullage,
  532. Height = height,
  533. Water = water,
  534. Temperature = temperature,
  535. WaterVolume = waterVolume
  536. };
  537. return tankReading;
  538. });
  539. }
  540. [UniversalApi(Description = "ONLY the deliveries read from ATG device side with timestamp > filterTimestamp will be returned.", InputParametersExampleJson = "[1,8,0,\"2020-04-01T18:25:43.511Z\"]")]
  541. public async Task<IEnumerable<Delivery>> GetTankDeliveryAsync(int tankNumber, int pageRowCount = 10, int pageIndex = 0, DateTime? filterTimestamp = null)
  542. {
  543. this.logger.LogDebug($"VR ATG is GetTankDeliveryAsync for tankNumber: {tankNumber} with filterTimestamp: {(filterTimestamp?.ToString("yyyy-MM-dd HH:mm:ss fff") ?? "")}");
  544. var response = await this.context.Outgoing.WriteAsync(
  545. new QueryInTankMostRecentDeliveryReportRequest(MessageBase.MessageFormat.Computer, tankNumber),
  546. (req, testResp) =>
  547. testResp is QueryInTankMostRecentDeliveryReportResponse r && r.TankNumberInFunctionCode == tankNumber,
  548. 20000);
  549. var mostRecentDeliveryReportResponse = response as QueryInTankMostRecentDeliveryReportResponse;
  550. if (response == null) throw new TimeoutException($" QueryInTankMostRecentDeliveryReportRequest(tankNumber: {tankNumber}) timedout");
  551. this.logger.LogDebug($" GetTankDeliveryAsync for tankNumber: {tankNumber} got mostRecentDeliveryReportResponse: {mostRecentDeliveryReportResponse.ToLogString()}");
  552. var results = mostRecentDeliveryReportResponse.DeliveriesForTanks.SelectMany(d => d.Deliveries)
  553. .Select(queried => new Delivery()
  554. {
  555. TankNumber = (byte)(queried.TankNumber ?? -1),
  556. StartingDateTime = queried.StartingDateTime.Value,
  557. StartingFuelHeight = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.StartingHeight).Value,
  558. StartingFuelVolume = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.StartingVolume).Value,
  559. StartingFuelTCVolume = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.StartingTcVolume).Value,
  560. StartingTemperture = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.StartingTemp).Value,
  561. StartingWaterHeight = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.StartingWater).Value,
  562. EndingDateTime = queried.EndingDateTime,
  563. EndingFuelHeight = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.EndingHeight).Value,
  564. EndingFuelVolume = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.EndingVolume).Value,
  565. EndingFuelTCVolume = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.EndingTcVolume).Value,
  566. EndingTemperture = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.EndingTemp).Value,
  567. EndingWaterHeight = queried.Datas.FirstOrDefault(d => d.Key == DeliveryReadingDataName.EndingWater).Value,
  568. })
  569. .Where(d => d.StartingDateTime > (filterTimestamp ?? DateTime.MinValue))
  570. .OrderByDescending(d => d.StartingDateTime).Skip(pageRowCount * pageIndex).Take(pageRowCount);
  571. this.logger.LogDebug($" TankNumber: {tankNumber} has {results.Count()} delivery records satisfy filter timestamp({(filterTimestamp?.ToString("yyyy-MM-dd HH:mm:ss fff") ?? "")}) condition.");
  572. return results;
  573. }
  574. //public async Task<IEnumerable<TankAlarms>> GetTankActiveOrUnackedAlarmsAsync(int tankNumber, int count)
  575. //{
  576. // var response = await this.context.Outgoing.WriteAsync(
  577. // new QueryInTankStatusReportRequest(MessageBase.MessageFormat.Computer, tankNumber),
  578. // (req, testResp) => testResp is MessageBaseGeneric, 3000);
  579. // if (response == null) throw new TimeoutException($"QueryInTankStatusReportRequest(tankNumber: {tankNumber}) timedout");
  580. // var queryInTankStatusReportResponse = response as QueryInTankStatusReportResponse;
  581. // return queryInTankStatusReportResponse?.TanksOfAlarms;
  582. //}
  583. [UniversalApi(Description = "", InputParametersExampleJson = "[1,8,0,\"2020-04-01T18:25:43.511Z\"]")]
  584. public async Task<IEnumerable<Inventory>> GetTankInventoryAsync(int tankNumber, int pageRowCount = 10, int pageIndex = 0, DateTime? filterTimestamp = null)
  585. {
  586. var reading = await this.GetTankReadingAsync(tankNumber);
  587. if (reading == null) return null;
  588. var inventory = new Inventory()
  589. {
  590. TankNumber = tankNumber,
  591. FuelHeight = reading.Height ?? -1,
  592. FuelVolume = reading.Volume ?? -1,
  593. FuelTCVolume = reading.TcVolume ?? -1,
  594. WaterHeight = reading.Water ?? -1,
  595. Temperture = reading.Temperature ?? int.MinValue,
  596. TimeStamp = DateTime.Now,
  597. };
  598. return new[] { inventory };
  599. }
  600. [UniversalApi(Description = "", InputParametersExampleJson = "[1,8,0,\"2020-04-01T18:25:43.511Z\"]")]
  601. public async Task<IEnumerable<Alarm>> GetTankAlarmAsync(int tankNumber, int pageRowCount = 10, int pageIndex = 0, DateTime? filterTimestamp = null)
  602. {
  603. var response = await this.context.Outgoing.WriteAsync(
  604. new QueryInTankStatusReportRequest(MessageBase.MessageFormat.Computer, tankNumber),
  605. (req, testResp) => testResp is QueryInTankStatusReportResponse r && r.TankNumberInFunctionCode == tankNumber, 5000);
  606. if (response == null) throw new TimeoutException($"QueryInTankStatusReportRequest(tankNumber: {tankNumber}) timedout");
  607. var queryInTankStatusReportResponse = response as QueryInTankStatusReportResponse;
  608. var alarmsForTanks = queryInTankStatusReportResponse?.TanksOfAlarms.Select(ta =>
  609. {
  610. var alarms = new List<Edge.Core.IndustryStandardInterface.ATG.Alarm>();
  611. alarms.AddRange(ta?.Alarms.Select(aType => new Edge.Core.IndustryStandardInterface.ATG.Alarm()
  612. {
  613. TankNumber = (byte)ta.TankNumber,
  614. Priority = AlarmPriority.Alarm,
  615. Type = aType,
  616. CreatedTimeStamp = DateTime.Now,
  617. }));
  618. return alarms;
  619. });
  620. return alarmsForTanks?.SelectMany(al => al)
  621. .Where(al => al.CreatedTimeStamp > (filterTimestamp ?? DateTime.MinValue))
  622. .OrderByDescending(d => d.CreatedTimeStamp).Skip(pageRowCount * pageIndex).Take(pageRowCount);
  623. }
  624. }
  625. }