using AutoMapper;
using Edge.Core.Database;
using Edge.Core.Processor;
using Edge.Core.IndustryStandardInterface.Pump;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Wayne_VaporRecoveryDataCollectorBoard.MessageEntity;
using Wayne_VaporRecoveryDataCollectorBoard.MessageEntity.Incoming;
using Wayne_VaporRecoveryDataCollectorBoard.MessageEntity.Outgoing;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using System.Xml.Serialization;
using Timer = System.Timers.Timer;
using Edge.Core.Processor.Dispatcher.Attributes;
using Edge.Core.Processor.Communicator;
using Edge.Core.UniversalApi;
using System.Threading;
namespace Wayne_VaporRecoveryDataCollectorBoard
{
[MetaPartsRequired(typeof(HalfDuplexActivePollingDeviceProcessor<,>))]
[MetaPartsRequired(typeof(ComPortCommunicator<>))]
[MetaPartsRequired(typeof(TcpClientCommunicator<>))]
//[UniversalApi(Name = GenericAlarm.UniversalApiEventName, EventDataType = typeof(GenericAlarm[]), Description = "Fire GenericAlarms to AlarmBar for attracting users.")]
[MetaPartsDescriptor(
"lang-zh-cn:稳牌气液比数据收集板lang-zh-tw:穩牌氣液比數據收集板lang-en-us:稳牌气液比数据收集板",
"lang-zh-cn:用于驱动 稳牌气液比数据收集板,日志名称 DynamicPrivate_VaporRecoveryDataCollectorBoard" +
"lang-zh-tw:用於驅動 穩牌氣液比數據收集板" +
"lang-en-us:Used for driven Wayne Vapor and Liquid collector board, log file name: DynamicPrivate_VaporRecoveryDataCollectorBoard",
new[] { "lang-zh-cn:在线监测lang-zh-tw:在線監測lang-en-us:OnlineWatch" })]
///
/// one comm channel may have multiple devices connected.
///
public class GroupHandler : TestableActivePollingDeviceHandler, IEnumerable, IDisposable
{
//private DateTime lastDeviceMessageReceivedTime;
// by seconds, change this value need change the correlated deviceOfflineCountdownTimer's interval as well
//public const int maxSecondsDeviceMuteWillTreatAsOffline = 30;
//private Timer deviceOfflineCountdownTimer;
private IServiceProvider services;
public event EventHandler OnBoardStateChange;
///
/// notify only the nozzle fuelling state change.
///
public event EventHandler OnBoardNozzleStateChange;
///
/// either a trx done flow data, or a serials of real time flow data.
///
public event EventHandler OnDataRecieved;
//public string HardwareIdentity { get; set; }
public IContext Context { get { return this.context; } }
static ILogger logger = NullLogger.Instance;
private int singleBoardOfflineTimeThresholdByMs = 15000;
private Timer singleBoardOfflineCheckTimer;
public List Boards { get; private set; } = new List();
///
/// the device has an internal time, if that time lagging too much from Fc, then FC will send a Write Time.
/// this value controls as the threshold.
///
private const int maxTimeGapFromFcAndDeviceBySeconds = 60 * 5;
//private XmlConfiguration configuration;
private IContext context;
//public byte DeviceAddress { get; set; }
public DeviceConfigV1 DeviceConfig { get; private set; }
public DataCollectorBoardTypeEnum? CurrentDataCollectorType { get; set; }
//public int DeviceId { get; }
public class DeviceConfigV1
{
public IEnumerable BoardConfigs { get; set; }
}
public class BoardConfigV1
{
///
/// each board has a serial number of global unique, like the mac address.
///
public int SerialNumber { get; set; }
///
/// fcc will based on serial number to set a physical address to board dynamically.
///
public byte SetPhysicalAddress { get; set; }
public IEnumerable NozzleConfigs { get; set; }
}
public class BoardInitParameterConfigV1
{
public byte 最大未变化次数 { get; set; }
public int 开始加油阀值 { get; set; }
public int 停止加油阀值 { get; set; }
public int 最小加油量 { get; set; }
public int 加油脉冲当量 { get; set; }
public int 油气脉冲当量 { get; set; }
///
/// 控制板将依据此值在加油过程中调节气泵回气量,以求尽可能靠近此比值
///
public double 气液比值 { get; set; }
public string ToLogString()
{
return "最大未变化次数: " + this.最大未变化次数
+ ", 开始加油阀值: " + this.开始加油阀值
+ ", 停止加油阀值: " + this.停止加油阀值
+ ", 最小加油量: " + this.最小加油量
+ ", 加油脉冲当量: " + this.加油脉冲当量
+ ", 油气脉冲当量: " + this.油气脉冲当量
+ ", 气液比值: " + this.气液比值;
}
}
public class BoardNozzleConfigV1
{
public int OnBoardNozzleNumber { get; set; }
///
/// each vr watched nozzle is a physical pump nozzle, and each nozzle must have a site level nozzle id(in China),
/// here is the binding info, this is the key info for draw UI for user.
///
public int SiteLevelNozzleId { get; set; }
///
/// specified by config in device handler, has the priority compare to external input values.
///
public BoardInitParameterConfigV1 ManualInitParameter { get; set; }
///
/// specified by config from external(like an app), has the low priority compare to internal manual values.
///
public BoardInitParameterConfigV1 ExternalInitParameter { get; set; }
}
protected void FireOnBoardStateChangeEvent(Board board)
{
this.OnBoardStateChange?.Invoke(this, new BoardStateChangeEventArgs(board));
}
protected void FireOnDataReceivedEvent(DataCollectorMessageBase data)
{
this.OnDataRecieved?.Invoke(this, new DataRecievedEventArgs(data));
}
protected void FireOnBoardNozzleStateChangeEvent(BoardNozzle nozzle)
{
this.OnBoardNozzleStateChange?.Invoke(this, new BoardNozzleStateChangeEventArgs(nozzle));
}
[ParamsJsonSchemas("DeviceGroupHandlerCtorParamsJsonSchemas")]
public GroupHandler(DeviceConfigV1 deviceConfig, IServiceProvider services)
{
if (deviceConfig.BoardConfigs.Select(bc => bc.SetPhysicalAddress).GroupBy(g => g).Any(g => g.Count() >= 2))
throw new ArgumentException("2 or more board configs have the same PhysicalAddress defined, while it should be unique per one comm channel");
if (deviceConfig.BoardConfigs.Select(bc => bc.SerialNumber).GroupBy(g => g).Any(g => g.Count() >= 2))
throw new ArgumentException("2 or more board configs have the same SerialNumber defined, while it should be unique per one comm channel");
this.services = services;
this.DeviceConfig = deviceConfig;
var loggerFactory = services.GetRequiredService();
logger = loggerFactory.CreateLogger("DynamicPrivate_VaporRecoveryDataCollectorBoard");
deviceConfig.BoardConfigs.ToList().ForEach(bc =>
{
var board = new Board(bc.SerialNumber, bc.SetPhysicalAddress, BoardStateEnum.UnInit,
bc.NozzleConfigs.Select(nc => new BoardNozzle()
{
NozzleNumberOnBoard = (byte)nc.OnBoardNozzleNumber,
SiteLevelNozzleId = nc.SiteLevelNozzleId,
NozzleState = NozzleStateEnum.Unknown
}).ToList());
board.LastIncomingMessageReceivedTime = DateTime.Now;
this.Boards.Add(board);
});
//DeviceId = deviceId;
//HardwareIdentity = config.SerialNumber;
// sample
//
//
// <最大未变化次数>5最大未变化次数>
// <开始加油阀值>16777255开始加油阀值>
// <停止加油阀值>268435495停止加油阀值>
// <最小加油量>268435495最小加油量>
// <加油脉冲当量>268435456加油脉冲当量>
// <油气脉冲当量>1677721600油气脉冲当量>
// <气液比值>2013265924气液比值>
//
//
// <最大未变化次数>5最大未变化次数>
// <开始加油阀值>16777255开始加油阀值>
// <停止加油阀值>268435495停止加油阀值>
// <最小加油量>268435495最小加油量>
// <加油脉冲当量>268435456加油脉冲当量>
// <油气脉冲当量>1677721600油气脉冲当量>
// <气液比值>2013265924气液比值>
//
//
//this.DeviceAddress = (byte)bindingDataCollectorAddress;
//var xmlSerializer = new XmlSerializer(typeof(XmlConfiguration));
//this.configuration =
// xmlSerializer.Deserialize(new StringReader(xmlConfig)) as XmlConfiguration;
}
public async override void Init(IContext context)
{
base.Init(context);
this.context = context;
// 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.
this.singleBoardOfflineTimeThresholdByMs = 2200 * this.Boards.Count >= 6000 ? 2200 * this.Boards.Count : 6000;
//accuracy is 1000ms
this.singleBoardOfflineCheckTimer = new Timer(1000);
this.singleBoardOfflineCheckTimer.Elapsed += async (a, b) =>
{
var offlineBoards = this.Boards.Where(b => DateTime.Now.Subtract(b.LastIncomingMessageReceivedTime).TotalMilliseconds >= this.singleBoardOfflineTimeThresholdByMs);
foreach (var offBoard in offlineBoards)
{
if (offBoard.State == BoardStateEnum.UnInit || offBoard.State == BoardStateEnum.Initializing) continue;
logger.LogInformation($"Board with serialNumber: {offBoard.SerialNumber}, PhyAddr: {offBoard.BoardPhysicalAddress} is offline({this.singleBoardOfflineTimeThresholdByMs}ms no see data), turn state to UnInit...");
offBoard.State = BoardStateEnum.UnInit;
this.FireOnBoardStateChangeEvent(offBoard);
var universalApiHub = this.services.GetRequiredService();
await universalApiHub.FirePersistGenericAlarmIfNotExists(this.context.Processor,
new GenericAlarm()
{
Title = $"lang-en-us:V/R board disconnectedlang-zh-cn:气液比采集板通讯断开lang-zh-tw:氣液比數據采集板通訊斷開",
Category = $"lang-zh-cn:气液比采集板lang-zh-tw:氣液比采集板",
Detail = $"lang-zh-cn:断开采集板的SN是:{offBoard.SerialNumber},上面配置的全站枪号是: " +
$"{string.Join(", ", offBoard.Nozzles.Select(n => n.SiteLevelNozzleId.ToString()))}" +
$"lang-zh-tw:斷開采集板的SN是:{offBoard.SerialNumber},上面配置的全站槍號是: " +
$"{string.Join(", ", offBoard.Nozzles.Select(n => n.SiteLevelNozzleId.ToString()))}",
Severity = GenericAlarmSeverity.Warning
},
ga => ga.Detail,
ga => ga.Detail);
}
};
this.singleBoardOfflineCheckTimer.Start();
this.context.Incoming.OnLongTimeNoSeeMessage += async (_, __) =>
{
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.");
this.Boards.ForEach(s =>
{
if (s.State != BoardStateEnum.UnInit)
{
s.State = BoardStateEnum.UnInit;
this.FireOnBoardStateChangeEvent(s);
}
});
var disconnectedBoardPhyAddressesStr = this.DeviceConfig.BoardConfigs?.Select(b => b.SetPhysicalAddress.ToString()).Aggregate("", (acc, n) => acc + ", " + n);
var universalApiHub = this.services.GetRequiredService();
await universalApiHub.FirePersistGenericAlarmIfNotExists(context.Processor,
new GenericAlarm()
{
Title = $"lang-en-us:Multiple V/R board disconnectedlang-zh-cn:多块气液比数据采集板通讯断开lang-zh-tw:多塊氣液比數據采集板通訊斷開",
Category = $"lang-zh-cn:气液比采集板lang-zh-tw:氣液比采集板",
Detail = $"lang-zh-cn:断开采集板的物理地址分别是:{disconnectedBoardPhyAddressesStr}" +
$"lang-zh-tw:斷開采集板的物理地址分別是:{disconnectedBoardPhyAddressesStr}",
Severity = GenericAlarmSeverity.Warning
}, ga => ga.Detail,
ga => ga.Detail);
};
this.context.Incoming.LongTimeNoSeeMessageTimeout = 18000;
var timeWindowWithActivePollingOutgoing = this.context.Outgoing as TimeWindowWithActivePollingOutgoing;
int prePolledBoardIndex = 0;
timeWindowWithActivePollingOutgoing.PollingMsgProducer = () =>
{
if (prePolledBoardIndex == this.Boards.Count) prePolledBoardIndex = 0;
var targetBoard = this.Boards[prePolledBoardIndex];
prePolledBoardIndex++;
switch (targetBoard.State)
{
case BoardStateEnum.Initialized:
{
return new READ_WORKING_TYPE_Request()
{
Address = targetBoard.BoardPhysicalAddress
};
}
case BoardStateEnum.UnInit:
{
//logger.LogInformation($"Board with SerialNumber: {targetBoard.SerialNumber }, PhysicalAddress: {targetBoard.BoardPhysicalAddress} is turning state from UnInit to Initializing...");
targetBoard.State = BoardStateEnum.Initializing;
this.FireOnBoardStateChangeEvent(targetBoard);
Task.Run(() =>
{
try
{
this.InitializeDevice(targetBoard.SerialNumber, targetBoard.BoardPhysicalAddress).ContinueWith(async (pt) =>
{
if (pt.Result)
{
targetBoard.State = BoardStateEnum.Initialized;
var universalApiHub = this.services.GetRequiredService();
await universalApiHub.ClosePersistGenericAlarms(this.context.Processor, $"断开", "conn recovered");
}
else
{
targetBoard.State = BoardStateEnum.UnInit;
}
this.FireOnBoardStateChangeEvent(targetBoard);
});
}
catch (Exception exxx)
{
logger.LogError($"InitializeDevice Board with SerialNumber: {targetBoard.SerialNumber} will set to PhyAddr: {targetBoard.BoardPhysicalAddress} exceptioned with: {exxx}");
}
});
return null;
}
case BoardStateEnum.Initializing: { return null; }
};
return null;
};
context.Communicator.OnConnected += async (_, __) =>
{
//await Task.Delay(5000).ContinueWith((t) => this.InitializeDevice());
};
}
///
/// send params and re-sync time from device.
/// ,
///
private async Task InitializeDevice(int boardSerialNumber, byte setBoardPhysicalAddress)
{
logger.LogInformation($"InitializeDevice ==========> Board with SerialNumber: {boardSerialNumber} will set to PhyAddr: {setBoardPhysicalAddress}...");
var setAddressResponse = await context.Outgoing.WriteAsync(
new SET_ADDRESS_Request(boardSerialNumber, setBoardPhysicalAddress),
(_, testResponse) => testResponse is SET_ADDRESS_Response r && r.Address == 0
&& r.DeviceSerialNumber == boardSerialNumber && r.NewDeviceAddress == setBoardPhysicalAddress, 1000) as SET_ADDRESS_Response;
if (setAddressResponse == null)
{
logger.LogError($" Board with SerialNumber: { boardSerialNumber} timedout to set to PhyAddr: { setBoardPhysicalAddress}");
return false;
}
var b = this.Boards?.FirstOrDefault(b => b.BoardPhysicalAddress == setAddressResponse.NewDeviceAddress);
if (b == null)
{
logger.LogInformation($" B PhyAddr: {setAddressResponse.NewDeviceAddress} does not defined via local configs");
return false;
}
//logger.LogInformation($" Board with SerialNumber: {boardSerialNumber}, " +
// $"Will read board side Params for its total {this.DeviceConfig.BoardConfigs.FirstOrDefault(bc => bc.SerialNumber == boardSerialNumber)?.NozzleConfigs?.Count() ?? 0} nozzles");
foreach (var nozzleNo in
this.DeviceConfig.BoardConfigs.FirstOrDefault(bc => bc.SerialNumber == boardSerialNumber)?.NozzleConfigs?.Select(nc => (byte)nc.OnBoardNozzleNumber))
{
//var readParaResponse = await context.Outgoing.WriteAsync(
// new READ_PARA_Request(nozzleNo) { Address = setBoardPhysicalAddress },
// (request, response) => response is READ_PARA_Response r && r.Address == setBoardPhysicalAddress, 5000) as READ_PARA_Response;
//if (readParaResponse == null)
//{
// logger.LogInformation(" B PhyAddr: " + setBoardPhysicalAddress + ", board side Parameter read for nozzle " +
// nozzleNo +
// " timedout, will skip this nozzle and continue...");
// continue;
//}
//logger.LogInformation(" B PhyAddr: " + setBoardPhysicalAddress + ", board side Parameter was read for nozzle " + readParaResponse.NozzleNumber
// + ", detail: " + readParaResponse.ToLogString());
var nozzleInitParameter = this.DeviceConfig.BoardConfigs.FirstOrDefault(bc => bc.SerialNumber == boardSerialNumber)
?.NozzleConfigs?.FirstOrDefault(nc => nc.OnBoardNozzleNumber == nozzleNo)?.ManualInitParameter;
if (nozzleInitParameter == null)
{
logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, Will try use ExternalInitParameter to Write Para to board for nozzle: {nozzleNo}");
nozzleInitParameter = this.DeviceConfig.BoardConfigs.FirstOrDefault(bc => bc.SerialNumber == boardSerialNumber)
?.NozzleConfigs?.FirstOrDefault(nc => nc.OnBoardNozzleNumber == nozzleNo)?.ExternalInitParameter;
}
else
logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, Will use ManualInitParameter to Write para to board for nozzle: {nozzleNo}");
if (nozzleInitParameter != null)
{
//logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, Will set new PARA to board for nozzle: {nozzleNo}");
var writeParamsResponse = await context.Outgoing.WriteAsync(
new WRITE_PARA_Request(nozzleNo)
{
Address = setBoardPhysicalAddress,
最大未变化次数 = nozzleInitParameter.最大未变化次数,
开始加油阀值 = nozzleInitParameter.开始加油阀值,
停止加油阀值 = nozzleInitParameter.停止加油阀值,
最小加油量 = nozzleInitParameter.最小加油量,
加油脉冲当量 = nozzleInitParameter.加油脉冲当量,
油气脉冲当量 = nozzleInitParameter.油气脉冲当量,
气液比值 = nozzleInitParameter.气液比值
},
(_, test) => test is WRITE_PARA_Response r && r.Address == setBoardPhysicalAddress, 2000);
if (writeParamsResponse != null)
logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, new Parameter was set successfully to nozzle {nozzleNo}");
else
logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, new Parameter was failed set to nozzle {nozzleNo}");
}
else
logger.LogInformation($" B PhyAddr: {setBoardPhysicalAddress }, Skip set new PARA to board for nozzle: {nozzleNo} as Manual and External InitParmater are both null");
}
b.LastIncomingMessageReceivedTime = DateTime.Now;
logger.LogInformation($" Board with SerialNumber: { boardSerialNumber} has successfully set to PhyAddr: { setBoardPhysicalAddress}");
var syncTimeResult = await this.ReadAndWriteTimeIfMaxTimeGapReached(setBoardPhysicalAddress);
if (syncTimeResult)
{
logger.LogInformation("B PhyAddr: " + setBoardPhysicalAddress + ", Sync time succeed, and init succeed!");
return true;
}
else
{
logger.LogInformation("B PhyAddr: " + setBoardPhysicalAddress + ", Sync time failed, init failed");
return false;
}
}
[UniversalApi(Description = "获取VR board油枪的初始化参数值")]
public async virtual Task ReadBoardNozzleInitParameters(byte boardPhysicalAddress, byte onBoardNozzleNumber)
{
logger.LogInformation($"B PhyAddr: {boardPhysicalAddress }, Will ReadBoardNozzleInitParameters for nozzle { onBoardNozzleNumber}");
var targetBoard = this.Boards.FirstOrDefault(b => b.BoardPhysicalAddress == boardPhysicalAddress && b.Nozzles.Any(n => n.NozzleNumberOnBoard == onBoardNozzleNumber));
if (targetBoard == null)
{
logger.LogInformation($" B PhyAddr: {boardPhysicalAddress }, parameters were read failed for nozzle {onBoardNozzleNumber} due to target board or nozzel does not exists");
return null;
}
var response = await this.context.Outgoing.WriteAsync(
new READ_PARA_Request(onBoardNozzleNumber) { Address = boardPhysicalAddress },
(_, test) => test is READ_PARA_Response r && r.Address == boardPhysicalAddress, 2000);
if (response is READ_PARA_Response readParameterResponse && readParameterResponse != null)
{
logger.LogDebug($" B PhyAddr: {boardPhysicalAddress }, parameters were read successfully for nozzle {onBoardNozzleNumber}: {readParameterResponse.ToLogString()}");
return readParameterResponse;
}
else
{
logger.LogInformation($" B PhyAddr: {boardPhysicalAddress }, parameters were read failed for nozzle {onBoardNozzleNumber}");
return null;
}
}
[UniversalApi(Description = "写入VR board油枪的初始化参数值")]
public async virtual Task WriteBoardNozzleInitParameters(byte boardPhysicalAddress, byte onBoardNozzleNumber, BoardInitParameterConfigV1 parametersConfig)
{
logger.LogInformation($"B PhyAddr: {boardPhysicalAddress }, Will WriteBoardNozzleInitParameters for nozzle { onBoardNozzleNumber}: {parametersConfig.ToLogString()}");
var targetBoard = this.Boards.FirstOrDefault(b => b.BoardPhysicalAddress == boardPhysicalAddress && b.Nozzles.Any(n => n.NozzleNumberOnBoard == onBoardNozzleNumber));
if (targetBoard == null)
{
logger.LogInformation($" B PhyAddr: {boardPhysicalAddress }, write Parameters failed for nozzle {onBoardNozzleNumber} due to target board or nozzel does not exists");
return false;
}
var response = await context.Outgoing.WriteAsync(
new WRITE_PARA_Request(onBoardNozzleNumber)
{
Address = boardPhysicalAddress,
最大未变化次数 = parametersConfig.最大未变化次数,
开始加油阀值 = parametersConfig.开始加油阀值,
停止加油阀值 = parametersConfig.停止加油阀值,
最小加油量 = parametersConfig.最小加油量,
加油脉冲当量 = parametersConfig.加油脉冲当量,
油气脉冲当量 = parametersConfig.油气脉冲当量,
气液比值 = parametersConfig.气液比值
},
(_, test) => test is WRITE_PARA_Response r && r.Address == boardPhysicalAddress, 2000);
if (response is WRITE_PARA_Response writeParameterResponse && writeParameterResponse != null)
{
logger.LogInformation($" B PhyAddr: {boardPhysicalAddress }, write Parameters successfully for nozzle {onBoardNozzleNumber}");
return true;
}
else
{
logger.LogInformation($" B PhyAddr: {boardPhysicalAddress }, write Parameters failed for nozzle {onBoardNozzleNumber}");
return false;
}
}
private async Task ReadAndWriteTimeIfMaxTimeGapReached(byte boardPhysicalAddress)
{
logger.LogInformation(">>>>>B PhyAddr: " + boardPhysicalAddress + ", trying sync time with FCC==========");
var readTimeResponse = await context.Outgoing.WriteAsync(
new READ_TIME_Request() { Address = boardPhysicalAddress },
(request, response) => response is READ_TIME_Response r && r.Address == boardPhysicalAddress, 5000) as READ_TIME_Response;
if (readTimeResponse == null)
{
logger.LogInformation("B PhyAddr: " + boardPhysicalAddress + ", Time was read failed from DataCollector, init failed.");
return false;
}
//this.lastDeviceMessageReceivedTime = DateTime.Now;
logger.LogDebug("B PhyAddr: " + boardPhysicalAddress + ", Time was read from DataCollector board which is: " + readTimeResponse.CurrentTime.ToString("yyyy-MM-dd HH:mm:ss"));
if (Math.Abs(readTimeResponse.CurrentTime.Subtract(DateTime.Now).TotalSeconds) >= maxTimeGapFromFcAndDeviceBySeconds)
{
logger.LogDebug("B PhyAddr: " + boardPhysicalAddress + ", there's " + maxTimeGapFromFcAndDeviceBySeconds +
" seconds time gap detected between DataCollector and local FC, will start time sync...");
var writeTimeResponse = await context.Outgoing.WriteAsync(
new WRITE_TIME_Request(DateTime.Now) { Address = boardPhysicalAddress },
(request0, response0) => response0 is WRITE_TIME_Response r && r.Address == boardPhysicalAddress,
5000);
if (writeTimeResponse != null)
{
this.Boards.First(b => b.BoardPhysicalAddress == writeTimeResponse.Address).LastIncomingMessageReceivedTime = DateTime.Now;
//this.lastDeviceMessageReceivedTime = DateTime.Now;
logger.LogDebug("B PhyAddr: " + boardPhysicalAddress + ", Sync time succeed!");
return true;
}
else
{
logger.LogInformation("B PhyAddr: " + boardPhysicalAddress + ", Sync time failed!");
return false;
}
}
else
{
logger.LogDebug("B PhyAddr: " + boardPhysicalAddress + ", No need sync time.");
}
return true;
}
private int onReadAndClearHistoryDataFromBoardGuard = 0;
public async override Task Process(IContext context)
{
if (!this.DeviceConfig.BoardConfigs.Any(bc => bc.SetPhysicalAddress == context.Incoming.Message.Address))
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?");
this.Boards.First(b => b.BoardPhysicalAddress == context.Incoming.Message.Address).LastIncomingMessageReceivedTime = DateTime.Now;
switch (context.Incoming.Message)
{
case READ_WORKING_AND_TYPE_Response realTimeStateAndDataResponse:
if (realTimeStateAndDataResponse.NozzleStates?.Any() ?? false)
{
foreach (var newNozzleState in realTimeStateAndDataResponse.NozzleStates)
{
var nozzle =
this.Boards.FirstOrDefault(b => b.BoardPhysicalAddress == realTimeStateAndDataResponse.Address)
?.Nozzles.FirstOrDefault(n => n.NozzleNumberOnBoard == newNozzleState.Key);
if (nozzle == null)
{
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??");
}
else
{
if (nozzle.NozzleState != newNozzleState.Value)
{
logger.LogDebug($"B PhyAddr: { context.Incoming.Message.Address}, Nozzle: {nozzle.NozzleNumberOnBoard} is swtching state from: {nozzle.NozzleState} to: {newNozzleState.Value}");
nozzle.NozzleState = newNozzleState.Value;
this.FireOnBoardNozzleStateChangeEvent(nozzle);
}
}
}
this.FireOnDataReceivedEvent(realTimeStateAndDataResponse);
}
this.CurrentDataCollectorType = realTimeStateAndDataResponse.DataCollectorType;
var withHistoryDataNozzles = realTimeStateAndDataResponse.NozzleHistoryDataStates.Where(h => h.Value == NozzleHistoryDataStateEnum.HasData);
foreach (var n in withHistoryDataNozzles)
{
//context.Outgoing.Write(new READ_HISTORY_NUM_Request(n.Key) { Address = realTimeStateAndDataResponse.Address });
if (0 == Interlocked.CompareExchange(ref this.onReadAndClearHistoryDataFromBoardGuard, 1, 0))
{
var nozzleNumber = n.Key;
var targetBoard = this.Boards.FirstOrDefault(b => b.BoardPhysicalAddress == context.Incoming.Message.Address);
try
{
if (logger.IsEnabled(LogLevel.Debug))
logger.LogDebug($"B PhyAddr: { targetBoard.BoardPhysicalAddress}, Nozzle: {nozzleNumber} has data, will try ReadAndClear...");
var result = await ReadAndClearHistoryDataFromBoard(context, targetBoard, nozzleNumber);
if (logger.IsEnabled(LogLevel.Debug))
logger.LogDebug($"B PhyAddr: { targetBoard.BoardPhysicalAddress}, Nozzle: {nozzleNumber} ReadAndClearHistoryData overall result is: {result}");
}
finally
{
logger.LogDebug($"B PhyAddr: { targetBoard?.BoardPhysicalAddress ?? -1}, Nozzle: {nozzleNumber} released guard");
this.onReadAndClearHistoryDataFromBoardGuard = 0;
}
}
else
{
if (logger.IsEnabled(LogLevel.Debug))
logger.LogDebug($"B PhyAddr: { context.Incoming.Message.Address}, Nozzle: {n.Key} skip this round of ReadAndClear as prev still running, will try later...");
break;
}
}
//this.OnDataRecieved?.Invoke(this, new DataRecievedEventArgs(realTimeStateAndDataResponse));
break;
//case READ_HISTORY_NUM_Response historyNumResponse:
// context.Outgoing.Write(
// new READ_ONE_HISTORY_DATA_Request(historyNumResponse.NozzleHistoryUnreadRecordCount.Key) { Address = historyNumResponse.Address });
// break;
//case READ_ONE_HISTORY_DATA_Response historyDataResponse:
// logger.LogDebug("B PhyAddr: " + historyDataResponse.Address + ", Read a history data from nozzle " + historyDataResponse.NozzleNumber
// + ", LiquidVolume " + historyDataResponse.LiquidVolume
// + ", AirVolume " + historyDataResponse.AirVolume
// + " which trx started from " + historyDataResponse.FuellingStartTime.ToString("yyyy-MM-dd HH:mm:ss")
// + " to " + historyDataResponse.FuellingEndTime.ToString("yyyy-MM-dd HH:mm:ss"));
// var clearResult = await ClearHistory(context, historyDataResponse);
// if (clearResult) this.FireOnDataReceivedEvent(historyDataResponse);
// break;
}
}
private async Task ReadAndClearHistoryDataFromBoard(IContext context, Board board, byte onBoardNozzleNumber)
{
var readHistoryNumResponse = await context.Outgoing.WriteAsync(
new READ_HISTORY_NUM_Request(onBoardNozzleNumber) { Address = board.BoardPhysicalAddress },
(r, testResponse) => testResponse is READ_HISTORY_NUM_Response rp && rp.Address == board.BoardPhysicalAddress && rp.NozzleHistoryUnreadRecordCount.Key == onBoardNozzleNumber,
1500) as READ_HISTORY_NUM_Response;
if (readHistoryNumResponse == null)
{
if (logger.IsEnabled(LogLevel.Debug))
logger.LogDebug($"B PhyAddr: {board.BoardPhysicalAddress}, Nozzle: {onBoardNozzleNumber} timedout for READ_HISTORY_NUM_Request");
return false;
}
if (readHistoryNumResponse.NozzleHistoryUnreadRecordCount.Value < 1)
{
if (logger.IsEnabled(LogLevel.Debug))
logger.LogDebug($"B PhyAddr: {board.BoardPhysicalAddress}, Nozzle: {onBoardNozzleNumber} indicates no un-read data?!");
return false;
}
int retryingTimes = 0;
retryReadData:
var operationResponse = await context.Outgoing.WriteAsync(
new READ_ONE_HISTORY_DATA_Request(onBoardNozzleNumber) { Address = board.BoardPhysicalAddress },
(r, testResponse) => (testResponse is READ_ONE_HISTORY_DATA_Response rp && rp.Address == board.BoardPhysicalAddress && rp.NozzleNumber == onBoardNozzleNumber)
|| (testResponse is COLLECTOR_BUSY_Response bRp && bRp.Address == board.BoardPhysicalAddress),
1500);
if (operationResponse == null)
{
if (logger.IsEnabled(LogLevel.Debug))
logger.LogDebug($"B PhyAddr: {board.BoardPhysicalAddress}, Nozzle: {onBoardNozzleNumber} timedout for READ_ONE_HISTORY_DATA_Request");
return false;
}
else if (operationResponse is COLLECTOR_BUSY_Response)
{
//0 indicates disabled for retry.
int maxRetryTimes = 0;
retryingTimes++;
if (retryingTimes > maxRetryTimes)
{
logger.LogInformation($"B PhyAddr: {board.BoardPhysicalAddress}, Nozzle: {onBoardNozzleNumber} report busy and deny for READ_ONE_HISTORY_DATA_Request");
return false;
}
if (logger.IsEnabled(LogLevel.Debug))
logger.LogDebug($"B PhyAddr: {board.BoardPhysicalAddress}, Nozzle: {onBoardNozzleNumber} Retrying for READ_ONE_HISTORY_DATA_Request...");
goto retryReadData;
}
var historyDataResponse = operationResponse as READ_ONE_HISTORY_DATA_Response;
logger.LogDebug($"B PhyAddr: { historyDataResponse.Address }, Nozzle {historyDataResponse.NozzleNumber} Read a history data"
+ $", LiquidVolume: {historyDataResponse.LiquidVolume}, AirVolume: { historyDataResponse.AirVolume}"
+ $", trx started from { historyDataResponse.FuellingStartTime.ToString("yyyy-MM-dd HH:mm:ss")}"
+ $" to { historyDataResponse.FuellingEndTime.ToString("yyyy-MM-dd HH:mm:ss")}, will clear...");
var clearHistoryResponse = await context.Outgoing.WriteAsync(
new CLEAR_ONE_HISTORY_Request(historyDataResponse.NozzleNumber)
{ Address = historyDataResponse.Address },
(request, response) => response is CLEAR_ONE_HISTORY_Response r && r.Address == historyDataResponse.Address, 1500) as CLEAR_ONE_HISTORY_Response;
if (clearHistoryResponse != null)
{
if (logger.IsEnabled(LogLevel.Debug))
logger.LogDebug($"B PhyAddr: {historyDataResponse.Address}, Nozzle: {historyDataResponse.NozzleNumber} One history has been cleared");
this.FireOnDataReceivedEvent(historyDataResponse);
return true;
}
else
{
logger.LogInformation($"B PhyAddr: { historyDataResponse.Address }, Nozzle: {historyDataResponse.NozzleNumber} Previous clear One history request failed");
return false;
}
}
[UniversalApi(Description = "Get state for all boards under this comm channel")]
public Task> GetBoardStates()
{
return Task.FromResult(this.Boards);
}
[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.")]
public async Task> ReadRealTimeDatas()
{
List results = new List();
var addresses = this.DeviceConfig.BoardConfigs.Select(bc => bc.SetPhysicalAddress).ToList();
foreach (var adrs in addresses)
{
var rsp = await this.context.Outgoing.WriteAsync(
new READ_WORKING_TYPE_Request()
{
Address = adrs
},
(request, testingResponse) => testingResponse is READ_WORKING_AND_TYPE_Response p && p.Address == adrs,
4000) as READ_WORKING_AND_TYPE_Response;
if (rsp != null)
results.Add(rsp);
}
return results;
}
public IEnumerator GetEnumerator()
{
return this.Boards.GetEnumerator();
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
{
return this.Boards.GetEnumerator();
}
public virtual void Dispose()
{
this.singleBoardOfflineCheckTimer?.Dispose();
foreach (var d in this.OnBoardStateChange?.GetInvocationList() ?? new Delegate[] { })
{
this.OnBoardStateChange -= d as EventHandler;
}
foreach (var d in this.OnBoardNozzleStateChange?.GetInvocationList() ?? new Delegate[] { })
{
this.OnBoardNozzleStateChange -= d as EventHandler;
}
foreach (var d in this.OnDataRecieved?.GetInvocationList() ?? new Delegate[] { })
{
this.OnDataRecieved -= d as EventHandler;
}
}
}
public enum BoardStateEnum
{
///
/// first time eastalized the connection with FC, or disconnected from a established connection.
///
UnInit,
///
/// processing and not ready for other command.
///
Initializing,
Initialized,
}
public class BoardNozzle
{
public byte NozzleNumberOnBoard { get; set; }
///
/// each vr watching nozzle is a physical pump nozzle, here is the bind info, will be used to draw UI.
///
public int SiteLevelNozzleId { get; set; }
public NozzleStateEnum NozzleState { get; set; }
}
public class Board
{
public int SerialNumber { get; }
public byte BoardPhysicalAddress { get; }
public IEnumerable Nozzles { get; }
public BoardStateEnum State { get; set; }
///
/// used for track the last received message which for caculate the offline state of this device.
///
public DateTime LastIncomingMessageReceivedTime { get; set; }
internal Board(int serialNumber, byte boardPhysicalAddress, BoardStateEnum state, IEnumerable nozzles)
{
this.SerialNumber = serialNumber;
this.BoardPhysicalAddress = boardPhysicalAddress;
this.State = state;
this.Nozzles = nozzles;
}
}
}