|
|
@ -42,7 +42,7 @@ using TouchSocket.Core;
|
|
|
|
namespace SlnMesnac.Business
|
|
|
|
namespace SlnMesnac.Business
|
|
|
|
{
|
|
|
|
{
|
|
|
|
/// <summary>
|
|
|
|
/// <summary>
|
|
|
|
/// 设备生产管理业务类
|
|
|
|
/// 设备生产管理业务类 -3F
|
|
|
|
/// </summary>
|
|
|
|
/// </summary>
|
|
|
|
public class ProdMgmtBusiness: BaseBusiness
|
|
|
|
public class ProdMgmtBusiness: BaseBusiness
|
|
|
|
{
|
|
|
|
{
|
|
|
@ -50,13 +50,17 @@ namespace SlnMesnac.Business
|
|
|
|
|
|
|
|
|
|
|
|
public readonly IBasePalletInfoService _basePalletInfoService;
|
|
|
|
public readonly IBasePalletInfoService _basePalletInfoService;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
public readonly IBaseRealTaskService _baseRealTaskService;
|
|
|
|
|
|
|
|
|
|
|
|
private List<RealPalletTask> _palletTasks;
|
|
|
|
private List<RealPalletTask> _palletTasks;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private DebugConfig debugConfig = DebugConfig.Instance;
|
|
|
|
|
|
|
|
|
|
|
|
public ProdMgmtBusiness(ILogger<ProdMgmtBusiness> logger, AppConfig appConfig, List<PlcAbsractFactory> plcFactories, List<RfidAbsractFactory> rfidFactories, IMesProductPlanService mesProductPlanService, IBasePalletInfoService basePalletInfoService, List<RealPalletTask> palletTasks,IServiceProvider serviceProvider) : base(logger, appConfig, plcFactories, rfidFactories, serviceProvider)
|
|
|
|
public ProdMgmtBusiness(ILogger<ProdMgmtBusiness> logger, AppConfig appConfig, List<PlcAbsractFactory> plcFactories, List<RfidAbsractFactory> rfidFactories, IMesProductPlanService mesProductPlanService, IBasePalletInfoService basePalletInfoService, IBaseRealTaskService baseRealTaskService, List<RealPalletTask> palletTasks,IServiceProvider serviceProvider) : base(logger, appConfig, plcFactories, rfidFactories, serviceProvider)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
_mesProductPlanService = mesProductPlanService;
|
|
|
|
_mesProductPlanService = mesProductPlanService;
|
|
|
|
_basePalletInfoService = basePalletInfoService;
|
|
|
|
_basePalletInfoService = basePalletInfoService;
|
|
|
|
|
|
|
|
_baseRealTaskService = baseRealTaskService;
|
|
|
|
_palletTasks = palletTasks;
|
|
|
|
_palletTasks = palletTasks;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -107,7 +111,7 @@ namespace SlnMesnac.Business
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// <summary>
|
|
|
|
/// 计划执行逻辑处理
|
|
|
|
/// 计划执行,叫料逻辑处理
|
|
|
|
/// </summary>
|
|
|
|
/// </summary>
|
|
|
|
private void ProdPlanExecHandle()
|
|
|
|
private void ProdPlanExecHandle()
|
|
|
|
{
|
|
|
|
{
|
|
|
@ -120,26 +124,39 @@ namespace SlnMesnac.Business
|
|
|
|
|
|
|
|
|
|
|
|
if (plc == null)
|
|
|
|
if (plc == null)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
throw new ArgumentException($"PLC连接信息为空");
|
|
|
|
//throw new ArgumentException($"PLC连接信息为空");
|
|
|
|
|
|
|
|
RefreshMessage($"PLC连接信息为空");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
MesProductPlan prodPlan = _mesProductPlanService.GetStartedProdPlan(out MesProductPlanDto productPlanDto);
|
|
|
|
MesProductPlan prodPlan = _mesProductPlanService.GetStartedProdPlan(out MesProductPlanDto productPlanDto);
|
|
|
|
if (prodPlan == null || productPlanDto == null)
|
|
|
|
if (prodPlan == null || productPlanDto == null)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
throw new ArgumentException($"未获取到需要执行的生产计划");
|
|
|
|
//throw new ArgumentException($"未获取到需要执行的生产计划");
|
|
|
|
|
|
|
|
RefreshMessage($"未获取到需要执行的生产计划");
|
|
|
|
|
|
|
|
Thread.Sleep(5000);
|
|
|
|
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (plc.readInt16ByAddress(GetPlcAddressByConfigKey("设备叫料")) != 1)
|
|
|
|
if (!plc.readBoolByAddress(GetPlcAddressByConfigKey("设备叫料")))
|
|
|
|
{
|
|
|
|
{
|
|
|
|
_logger.LogInformation("等待设备叫料信号触发......");
|
|
|
|
RefreshMessage("等待设备叫料信号触发......");
|
|
|
|
|
|
|
|
Thread.Sleep(5000);
|
|
|
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
BaseRealTask task = _baseRealTaskService.GetExeTask();
|
|
|
|
|
|
|
|
if (task!=null) {
|
|
|
|
|
|
|
|
RefreshMessage("已经叫料,请等待AGV送料......");
|
|
|
|
|
|
|
|
|
|
|
|
Thread.Sleep(5000);
|
|
|
|
Thread.Sleep(5000);
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RefreshMessage("设备要料信号触发成功");
|
|
|
|
RefreshMessage("设备要料信号触发成功");
|
|
|
|
|
|
|
|
|
|
|
|
RefreshProdPlanExecEvent?.Invoke(prodPlan);
|
|
|
|
RefreshProdPlanExecEvent?.Invoke(prodPlan);
|
|
|
|
|
|
|
|
|
|
|
|
string palletCode = GetPalletInfoByTask();
|
|
|
|
// string palletCode = GetPalletInfoByTask();
|
|
|
|
|
|
|
|
|
|
|
|
RefreshMessage($"执行计划:{prodPlan.PlanCode};计划数量:{Math.Round(prodPlan.PlanAmount,2)};完成数量:{Math.Round(prodPlan.CompleteAmount,2)};发起叫料申请等待AGV送料......");
|
|
|
|
RefreshMessage($"执行计划:{prodPlan.PlanCode};计划数量:{Math.Round(prodPlan.PlanAmount,2)};完成数量:{Math.Round(prodPlan.CompleteAmount,2)};发起叫料申请等待AGV送料......");
|
|
|
|
|
|
|
|
|
|
|
@ -147,8 +164,18 @@ namespace SlnMesnac.Business
|
|
|
|
if (ApplyDeliveryHandle(productPlanDto.MaterialId.ToString()))
|
|
|
|
if (ApplyDeliveryHandle(productPlanDto.MaterialId.ToString()))
|
|
|
|
{
|
|
|
|
{
|
|
|
|
RefreshMessage("根据计划自动申请叫料成功");
|
|
|
|
RefreshMessage("根据计划自动申请叫料成功");
|
|
|
|
|
|
|
|
#region 本地创建一个叫料任务
|
|
|
|
plc.writeInt16ByAddress(GetPlcAddressByConfigKey("设备叫料"), 0);
|
|
|
|
BaseRealTask realTask = new BaseRealTask();
|
|
|
|
|
|
|
|
realTask.PlanCode = prodPlan.PlanCode;
|
|
|
|
|
|
|
|
realTask.PlanAmount = prodPlan.PlanAmount;
|
|
|
|
|
|
|
|
realTask.PlanComplete = prodPlan.CompleteAmount;
|
|
|
|
|
|
|
|
realTask.MaterialId = prodPlan.MaterialId;
|
|
|
|
|
|
|
|
realTask.Status = 1;
|
|
|
|
|
|
|
|
realTask.CreateTime = DateTime.Now;
|
|
|
|
|
|
|
|
realTask.UpdateTime = DateTime.Now;
|
|
|
|
|
|
|
|
_baseRealTaskService.InsertTask(realTask);
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
plc.writeBoolByAddress(GetPlcAddressByConfigKey("设备叫料"), false);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
else
|
|
|
|
{
|
|
|
|
{
|
|
|
@ -158,7 +185,7 @@ namespace SlnMesnac.Business
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (Exception e)
|
|
|
|
catch (Exception e)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
_logger.LogInformation($"计划执行逻辑处理异常:{e.Message}");
|
|
|
|
_logger.LogError($"计划执行逻辑处理异常:{e.Message}");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
Thread.Sleep(5000);
|
|
|
|
Thread.Sleep(5000);
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -173,73 +200,111 @@ namespace SlnMesnac.Business
|
|
|
|
bool result = false;
|
|
|
|
bool result = false;
|
|
|
|
while (true)
|
|
|
|
while (true)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
//获取当前计划
|
|
|
|
//mes计划
|
|
|
|
var prodPlan = _mesProductPlanService.GetStartedProdPlan(out MesProductPlanDto productPlanDto);
|
|
|
|
MesProductPlan prodPlan = null;
|
|
|
|
|
|
|
|
//本地叫料任务
|
|
|
|
|
|
|
|
BaseRealTask localPlan = null;
|
|
|
|
try
|
|
|
|
try
|
|
|
|
{
|
|
|
|
{
|
|
|
|
var plc = base.GetPlcByKey("plc");
|
|
|
|
var plc = base.GetPlcByKey("plc");
|
|
|
|
|
|
|
|
|
|
|
|
if(plc == null)
|
|
|
|
if (plc == null)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
_logger.LogInformation("读取物料到位信号,PLC连接信息为空......");
|
|
|
|
_logger.LogInformation("读取物料到位信号,PLC连接信息为空......");
|
|
|
|
Thread.Sleep(3000);
|
|
|
|
Thread.Sleep(3000);
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (plc.readInt16ByAddress(GetPlcAddressByConfigKey("物料到位")) != 1)
|
|
|
|
if (!plc.readBoolByAddress(GetPlcAddressByConfigKey("物料到位")))
|
|
|
|
{
|
|
|
|
{
|
|
|
|
_logger.LogInformation("等待物料到位信号触发......");
|
|
|
|
_logger.LogInformation("等待物料到位信号触发......");
|
|
|
|
Thread.Sleep(3000);
|
|
|
|
Thread.Sleep(3000);
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (productPlanDto == null || prodPlan == null)
|
|
|
|
// TODO 时间校验
|
|
|
|
|
|
|
|
bool timeCheck = JudgeProductTime();
|
|
|
|
|
|
|
|
if (!timeCheck)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
RefreshMessage($"收到物料到位信号,未获取到正在执行的生产计划,请开始执行计划");
|
|
|
|
RefreshMessage($"收到物料到位信号,但是距离上次投料不满20min,禁止投料");
|
|
|
|
plc.writeInt16ByAddress(GetPlcAddressByConfigKey("物料到位"), 0);
|
|
|
|
Thread.Sleep(1000 * 15);
|
|
|
|
Thread.Sleep(3000);
|
|
|
|
|
|
|
|
continue;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
plc.writeInt16ByAddress(GetPlcAddressByConfigKey("物料到位"), 0);
|
|
|
|
//TODO 调用调度接口,判断AGV小车是否已经离开到位,否则不允许投料
|
|
|
|
|
|
|
|
|
|
|
|
RefreshMessage("物料到位信号触发成功,读取托盘RFID信息进行投料校验");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ReadEpcStrByRfidKey("test", out string epcStr);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
RefreshMessage($"投料校验RFID标签读取成功,标签信息:{epcStr}");
|
|
|
|
//TODO根据本地叫料计划查询对应的mes计划,完成
|
|
|
|
|
|
|
|
prodPlan = _mesProductPlanService.GetStartedProdPlan(out MesProductPlanDto productPlanDto);
|
|
|
|
|
|
|
|
|
|
|
|
//获取物料托盘上的物料信息
|
|
|
|
if (productPlanDto == null || prodPlan == null)
|
|
|
|
BasePalletInfo palletInfo = _basePalletInfoService.GetPalletInfoByPalletCode(epcStr);
|
|
|
|
|
|
|
|
if (palletInfo == null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
result = MatPutInValidTriggerEvent(2, prodPlan, string.Empty, "投料校验失败,未获取到托盘信息,是否继续投料");
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
{
|
|
|
|
{
|
|
|
|
if (palletInfo.MaterialId != prodPlan.MaterialId)
|
|
|
|
RefreshMessage($"收到物料到位信号,未获取到正在执行的生产计划,请开始执行计划");
|
|
|
|
{
|
|
|
|
|
|
|
|
result = MatPutInValidTriggerEvent(2, prodPlan, string.Empty, "投料校验失败,物料信息不匹配,是否继续投料");
|
|
|
|
Thread.Sleep(5000);
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
else
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
result = true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (result)
|
|
|
|
|
|
|
|
|
|
|
|
// 获取本地执行中的叫料任务,完成本地任务
|
|
|
|
|
|
|
|
localPlan = _baseRealTaskService.GetExeTask();
|
|
|
|
|
|
|
|
if (localPlan == null)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
bool isRes = _basePalletInfoService.UnBindPalletInfo(palletInfo);
|
|
|
|
|
|
|
|
RefreshMessage($"物料校验通过,托盘信息解绑{(isRes ? "成功" : "失败")}");
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#region 物料校验 -- TODO 如果计划暂停,开启新计划,校验当前滞留物料是与前一条计划判断,还是当前开始的计划判断
|
|
|
|
|
|
|
|
//RefreshMessage("物料到位信号触发成功,读取托盘RFID信息进行投料校验");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
//ReadEpcStrByRfidKey("test", out string epcStr);
|
|
|
|
|
|
|
|
//RefreshMessage($"投料校验RFID标签读取成功,标签信息:{epcStr}");
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
////获取物料托盘上的物料信息
|
|
|
|
|
|
|
|
//BasePalletInfo palletInfo = _basePalletInfoService.GetPalletInfoByPalletCode(epcStr);
|
|
|
|
|
|
|
|
//if (palletInfo == null)
|
|
|
|
|
|
|
|
//{
|
|
|
|
|
|
|
|
// result = MatPutInValidTriggerEvent(2, prodPlan, string.Empty, "投料校验失败,未获取到托盘信息,是否继续投料");
|
|
|
|
|
|
|
|
//}
|
|
|
|
|
|
|
|
//else
|
|
|
|
|
|
|
|
//{
|
|
|
|
|
|
|
|
// if (palletInfo.MaterialId != prodPlan.MaterialId)
|
|
|
|
|
|
|
|
// {
|
|
|
|
|
|
|
|
// result = MatPutInValidTriggerEvent(2, prodPlan, string.Empty, "投料校验失败,物料信息不匹配,是否继续投料");
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
// else
|
|
|
|
|
|
|
|
// {
|
|
|
|
|
|
|
|
// result = true;
|
|
|
|
|
|
|
|
// }
|
|
|
|
|
|
|
|
//}
|
|
|
|
|
|
|
|
//if (result)
|
|
|
|
|
|
|
|
//{
|
|
|
|
|
|
|
|
// bool isRes = _basePalletInfoService.UnBindPalletInfo(palletInfo);
|
|
|
|
|
|
|
|
// RefreshMessage($"物料校验通过,托盘信息解绑{(isRes ? "成功" : "失败")}");
|
|
|
|
|
|
|
|
//}
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
catch (Exception e)
|
|
|
|
catch (Exception e)
|
|
|
|
{
|
|
|
|
{
|
|
|
|
_logger.LogInformation($"投料校验异常:{e.Message}");
|
|
|
|
_logger.LogInformation($"投料校验异常:{e.Message}");
|
|
|
|
result = MatPutInValidTriggerEvent(2, prodPlan, string.Empty, $"投料校验异常:{e.Message},是否继续投料");
|
|
|
|
// TODO修改成传入本地计划查询出的mes计划
|
|
|
|
|
|
|
|
// result = MatPutInValidTriggerEvent(2, prodPlan, string.Empty, $"投料校验异常:{e.Message},是否继续投料");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (prodPlan != null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
// TODO修改成传入本地计划查询出的mes计划,停止计划导致的滞留物料归属哪个订单计划对应更新
|
|
|
|
|
|
|
|
PutInResutlHandle(prodPlan, result);
|
|
|
|
|
|
|
|
|
|
|
|
PutInResutlHandle(prodPlan, result);
|
|
|
|
// 完成本地计划
|
|
|
|
|
|
|
|
if(localPlan!= null)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
localPlan.Status = 2;
|
|
|
|
|
|
|
|
localPlan.UpdateTime = DateTime.Now;
|
|
|
|
|
|
|
|
_baseRealTaskService.Update(localPlan);
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Thread.Sleep(3000);
|
|
|
|
Thread.Sleep(3000);
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -291,7 +356,7 @@ namespace SlnMesnac.Business
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// <summary>
|
|
|
|
/// 申请叫料
|
|
|
|
/// 申请叫料,TODO等待调度接口
|
|
|
|
/// </summary>
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="materialCode"></param>
|
|
|
|
/// <param name="materialCode"></param>
|
|
|
|
/// <param name="result"></param>
|
|
|
|
/// <param name="result"></param>
|
|
|
@ -310,8 +375,11 @@ namespace SlnMesnac.Business
|
|
|
|
throw new ArgumentException("申请叫料处理异常:物料参数为空");
|
|
|
|
throw new ArgumentException("申请叫料处理异常:物料参数为空");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
plc.writeInt16ByAddress(base.GetPlcAddressByConfigKey("WCS叫料"), 1);
|
|
|
|
|
|
|
|
plc.writeStringByAddress(base.GetPlcAddressByConfigKey("物料型号"), materialCode);
|
|
|
|
// TODO -- 修改为调用wcs接口,参数为申请单id,同时本地生成一个叫料任务,任务状态为执行中
|
|
|
|
|
|
|
|
// plc.writeInt16ByAddress(base.GetPlcAddressByConfigKey("WCS叫料"), 1);
|
|
|
|
|
|
|
|
// plc.writeStringByAddress(base.GetPlcAddressByConfigKey("物料型号"), materialCode);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
result = true;
|
|
|
|
result = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -358,5 +426,45 @@ namespace SlnMesnac.Business
|
|
|
|
throw new InvalidOperationException($"生产计划更新异常:{e.Message}");
|
|
|
|
throw new InvalidOperationException($"生产计划更新异常:{e.Message}");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
///
|
|
|
|
|
|
|
|
/// <summary
|
|
|
|
|
|
|
|
/// 距离上次投料时间合格判断
|
|
|
|
|
|
|
|
/// </summary
|
|
|
|
|
|
|
|
private bool JudgeProductTime()
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
int IntervalMin = int.Parse(GetPlcAddressByConfigKey("投料时间间隔")); // 单位分钟
|
|
|
|
|
|
|
|
if (debugConfig.LastTime != "")
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DateTime lastTime = DateTime.Parse(debugConfig.LastTime);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
TimeSpan timeDifference = DateTime.Now - lastTime;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (timeDifference.TotalMinutes > IntervalMin)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
else
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
_logger.LogError($"距离上次投料时间合格判断异常JudgeProductTime:{ex.Message}");
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|