Microsoft Dynamics AX 2012 Manufacturing – Lean IoT Scenario Part 3: Software/Demo Automation
Purpose: The purpose of this document is to illustrate how to automate Make to Order Lean Manufacturing scenario in Microsoft Dynamics AX 2012 using IoT device.
Challenge: Microsoft Dynamics AX 2012 out-of-the-box enables mixed mode manufacturing including discrete, process, project and Lean approaches. In the previous part we explored the capabilities of Raspberry Pi IoT device which will help us to implement an end-to-end functional flow for Make to Order Lean Manufacturing scenario. We have discussed on a high level what developer experience will look like when programming IoT devices using Microsoft technology. Now the goal is to dig deeper into a developer experience and implement all necessary software components required for Lean IoT Demo scenario.
Solution: From the development perspective first we are going to need Visual Studio (2015) to develop Headless Background App running on Raspberry Pi under Windows 10 IoT Core OS which will communicate with Microsoft Dynamics AX 2012 R3 backend. Second, we are going to need to expose Microsoft Dynamics AX 2012 Web Service on Inbound port via HTTP/HTTPS to receive a signal from IoT device and perform appropriate actions. Third, we are going to automate the demo flow for a self-driven demo: Sales order demand will be automatically introduced and all further actions will be triggered accordingly.
Please find Part 2 article of this series which describes a hardware part here: http://ax2012manufacturing.blogspot.com/2015/09/microsoft-dynamics-ax-2012_11.html
Walkthrough
Before we begin our deep dive into software part I'll quickly refresh your memory on a functionality we are trying to automate in this scenario.
In the context of Lean Manufacturing workcell worker will perform assembly task at the designated workcell and will require a particular part. Water spider will be responsible to timely supply this part to workcell worker. The process will be controlled by kanbans, process and transfer kanban jobs will be assigned to workcell worker and water spider respectively. In this scenario we will automate kanban replenishment process using industrial IoT device. Specifically water spider will refill emptied part location from main storage location for workcell worker to be solely focused on assembly task. Raspberry Pi IoT device powered by Windows 10 IoT Core will be used to automatically determine when part location should be replenished and send signals for kanban jobs assignments and updates
A workcell is an arrangement of resources in a manufacturing environment to improve the quality, speed and cost of the process. Workcells are designed to improve these by improving process flow and eliminating waste. Workcell workers perform "value-added" tasks in their designated workcells
A water spider means a person who is responsible for performing a wide range of tasks which allows workers to perform "value-added" tasks without distraction
Here's the conceptual diagram of the process
Diagram
And this is a more detailed diagram which has hardware implementation details too
Diagram
We'll break the explanation down into 3 parts as was described above and tackle one piece at the time
Section: Microsoft Dynamics AX 2012 R3 Web Service
In Part 1 of this series (http://ax2012manufacturing.blogspot.com/2015/09/microsoft-dynamics-ax-2012.html) I described a classic Lean Make to Order scenario. Based on a detailed process diagram above "Intelligent" location will trigger a completion of Transfer and Process jobs as Workcell worker and Water spider will do their part of the job. Specifically, we need to be able to programmatically complete Transfer and Process jobs on demand.
For this scenario we will implement WCF Custom Web Service in Microsoft Dynamics AX 2012 R3. Please note that I only needed Service Contract class with a few operations and no Data Contract class
This is how Service Contract class will look like
Source code
class AlexLeanIoTDemoService
{
AlexLeanIoTDemoTrans demoTrans;
AlexLeanIoTDemoLotId lotId;
AlexLeanIoTDemoTransferJob transferJob;
AlexLeanIoTDemoProcessJob processJob;
}
[SysEntryPointAttribute(true)]
public void completeProcessJob(RecId _recId)
{
//element.runMenuItemAction(menuitemActionStr(KanbanJobCompleteSilent), kanbanBoardTmpProcessJob);
MenuFunction menuFunction;
Args args;
/* Tmp */
KanbanJobSchedule kanbanJobSchedule;
KanbanJob kanbanJob;
Kanban kanban;
KanbanRule kanbanRule;
KanbanRuleFixed kanbanRuleFixed;
InventTable inventTable;
KanbanBoardTmpProcessJob kanbanBoardTmpProcessJob;
/* Test */
AlexLeanIoTDemoTrans demoTransLocal;
select firstonly demoTransLocal
where demoTransLocal.TransferComplete == true &&
demoTransLocal.ProcessComplete == false;
_recId = demoTransLocal.ProcessJob;
if (!_recId)
return;
/* Test */
kanban = Kanban::findKanbanJobRecId(_recId);
kanbanJob = kanbanJob::find(_recId);
kanbanJobSchedule = kanbanJob.kanbanJobSchedule();
kanbanRule = KanbanRule::find(kanban.KanbanRule);
kanbanRuleFixed = KanbanRuleFixed::findParentRecId(kanbanRule.RecId);
inventTable = InventTable::find(kanban.ItemId);
kanbanBoardTmpProcessJob.clear();
kanbanBoardTmpProcessJob.Kanban = kanban.RecId;
kanbanBoardTmpProcessJob.KanbanRule = kanban.KanbanRule;
kanbanBoardTmpProcessJob.ItemId = kanban.ItemId;
kanbanBoardTmpProcessJob.InventDimId = kanban.InventDimId;
kanbanBoardTmpProcessJob.Express = kanban.Express;
kanbanBoardTmpProcessJob.CardId = kanban.KanbanCardId;
kanbanBoardTmpProcessJob.QuantityOrdered = kanbanJob.QuantityOrdered;
kanbanBoardTmpProcessJob.Status = kanbanJob.Status;
kanbanBoardTmpProcessJob.Job = kanbanJob.RecId;
kanbanBoardTmpProcessJob.ExpectedDateTime = kanbanJob.ExpectedDateTime;
kanbanBoardTmpProcessJob.DueDateTime = kanbanJob.DueDateTime;
kanbanBoardTmpProcessJob.ActualEndDateTime = kanbanJob.ActualEndDateTime;
kanbanBoardTmpProcessJob.PlannedPeriod = kanbanJobSchedule.PlannedPeriod;
kanbanBoardTmpProcessJob.Sequence = kanbanJobSchedule.Sequence;
kanbanBoardTmpProcessJob.ActivityName = kanbanJob.PlanActivityName;
kanbanBoardTmpProcessJob.ReceiptInventLocationId = kanbanJob.InventLocationId;
kanbanBoardTmpProcessJob.ReceiptWMSLocationId = kanbanJob.wmsLocationId;
kanbanBoardTmpProcessJob.ItemName = inventTable.defaultProductName();
kanbanBoardTmpProcessJob.Color = kanbanJob.LeanScheduleGroupColor;
kanbanBoardTmpProcessJob.ScheduleGroupName = kanbanJob.LeanScheduleGroupName;
kanbanBoardTmpProcessJob.IsOverdue = KanbanJob::isOverdue(
kanbanJob.DueDateTime,
kanbanJob.ExpectedDateTime,
kanbanJob.Status,
kanbanRule.ReplenishmentStrategy,
kanbanRuleFixed.ReplenishmentLeadTime);
kanbanBoardTmpProcessJob.insert();
/* Tmp */
args = new Args();
args.record(kanbanBoardTmpProcessJob);
//args.caller(this);
menuFunction = new MenuFunction(menuitemActionStr(KanbanJobCompleteSilent), MenuItemType::Action);
if (menuFunction)
{
menuFunction.run(args);
}
this.updateDemoTrans(demoTransLocal.LotId, LeanKanbanJobType::Process);
}
|
[SysEntryPointAttribute(true)]
public void completeTransferJob(RecId _recId)
{
//kanbanBoardTransferJobForm.runMenuItem(menuitemActionStr(KanbanTransferJobCompleteSilent),MenuItemType::Action,kanbanBoardTmpTransferJob);
MenuFunction menuFunction;
Args args;
/* Tmp */
KanbanJob kanbanJob;
Kanban kanban;
KanbanRule kanbanRule;
KanbanRuleFixed kanbanRuleFixed;
PlanReference planReference;
PlanActivity planActivity;
PlanActivityLocation planActivityLocation;
PlanActivityService planActivityService;
WMSShipment wmsShipment;
InventTable inventTable;
KanbanBoardTmpTransferJob kanbanBoardTmpTransferJob;
/* Test */
AlexLeanIoTDemoTrans demoTransLocal;
select firstonly demoTransLocal
where demoTransLocal.TransferComplete == false &&
demoTransLocal.ProcessComplete == false;
_recId = demoTransLocal.TransferJob;
if (!_recId)
return;
/* Test */
kanban = Kanban::findKanbanJobRecId(_recId);
kanbanJob = kanbanJob::find(_recId);
planReference = kanbanJob.planReference();
planActivity = kanbanJob.planActivity();
kanbanRule = KanbanRule::find(kanban.KanbanRule);
kanbanRuleFixed = KanbanRuleFixed::findParentRecId(kanbanRule.RecId);
inventTable = InventTable::find(kanban.ItemId);
planActivityService = PlanActivityService::findKanbanJob(kanbanJob, true);
planActivityLocation = planActivity.issueLocation();
wmsShipment = kanbanJob.wmsShipment();
kanbanBoardTmpTransferJob.clear();
kanbanBoardTmpTransferJob.IssueInventLocationId = planActivityLocation.InventLocationId;
kanbanBoardTmpTransferJob.IssueWMSLocationId = planActivityLocation.wmsLocationId;
kanbanBoardTmpTransferJob.ReceiptInventLocationId = kanbanJob.InventLocationId;
kanbanBoardTmpTransferJob.ReceiptWMSLocationId = kanbanJob.wmsLocationId;
kanbanBoardTmpTransferJob.KanbanRule = kanban.KanbanRule;
kanbanBoardTmpTransferJob.ItemId = kanban.ItemId;
kanbanBoardTmpTransferJob.InventDimId = kanban.InventDimId;
kanbanBoardTmpTransferJob.Express = kanban.Express;
kanbanBoardTmpTransferJob.CardId = kanban.KanbanCardId;
kanbanBoardTmpTransferJob.QuantityOrdered = kanban.QuantityOrdered;
kanbanBoardTmpTransferJob.Status = kanbanJob.Status;
kanbanBoardTmpTransferJob.Job = kanbanJob.RecId;
kanbanBoardTmpTransferJob.ExpectedDateTime = kanbanJob.ExpectedDateTime;
kanbanBoardTmpTransferJob.DueDateTime = kanbanJob.DueDateTime;
kanbanBoardTmpTransferJob.ActualEndDateTime = kanbanJob.ActualEndDateTime;
kanbanBoardTmpTransferJob.Color = kanbanJob.LeanScheduleGroupColor;
kanbanBoardTmpTransferJob.ScheduleGroupName = kanbanJob.LeanScheduleGroupName;
kanbanBoardTmpTransferJob.KanbanId = kanban.KanbanId;
kanbanBoardTmpTransferJob.Kanban = kanban.RecId;
kanbanBoardTmpTransferJob.KanbanStatus = kanban.Status;
kanbanBoardTmpTransferJob.ActivityName = planActivity.Name;
kanbanBoardTmpTransferJob.PlanReferenceName = planReference.PlanName;
kanbanBoardTmpTransferJob.InventUnitId = inventTable.inventUnitId();
kanbanBoardTmpTransferJob.ShipmentId = wmsShipment.ShipmentId;
kanbanBoardTmpTransferJob.ShippingDateTime = wmsShipment.ShippingDateTime;
kanbanBoardTmpTransferJob.Quantity = kanbanJob.QuantityOrdered;
kanbanBoardTmpTransferJob.IsOverdue = KanbanJob::isOverdue(
kanbanJob.DueDateTime,
kanbanJob.ExpectedDateTime,
kanbanJob.Status,
kanbanRule.ReplenishmentStrategy,
kanbanRuleFixed.ReplenishmentLeadTime);
if (planActivityService.RecId)
{
kanbanBoardTmpTransferJob.CarrierIdDataAreaId = planActivityService.CarrierIdDataAreaId;
kanbanBoardTmpTransferJob.CarrierId = planActivityService.CarrierId;
kanbanBoardTmpTransferJob.FreightedBy = planActivity.FreightedBy;
kanbanBoardTmpTransferJob.VendAccount = planActivityService.vendorAccount();
}
kanbanBoardTmpTransferJob.insert();
/* Tmp */
menuFunction = new MenuFunction(menuitemActionStr(KanbanTransferJobCompleteSilent), MenuItemType::Action);
if (menuFunction)
{
args = new Args();
args.record(kanbanBoardTmpTransferJob);
//args.caller(this);
menuFunction.run(args);
}
this.updateDemoTrans(demoTransLocal.LotId, LeanKanbanJobType::Transfer);
}
|
private void createDemoTrans(AlexLeanIoTDemoLotId _lotId,
AlexLeanIoTDemoTransferJob _transferJob,
AlexLeanIoTDemoProcessJob _processJob)
{
ttsBegin;
demoTrans.clear();
demoTrans.initValue();
demoTrans.LotId = _lotId;
demoTrans.TransferJob = _transferJob;
demoTrans.ProcessJob = _processJob;
demoTrans.insert();
ttsCommit;
}
|
private InventTransId createSalesLine()
{
#define.Customer("US-001")
#define.ItemId("AlexMotorcycle")
#define.Qty(1)
#define.Unit("ea")
#define.Site("1")
#define.Warehouse("13")
SalesTable salesTable;
SalesLine salesLine;
InventDim inventDim;
try
{
ttsbegin;
//Order header
salesTable.clear();
salesTable.initValue(SalesType::Sales);
salesTable.SalesId = NumberSeq::newGetNum(SalesParameters::numRefSalesId()).num();
salesTable.DeliveryDate = today();
salesTable.CustAccount = #Customer;
salesTable.initFromCustTable();
if (salesTable.validateWrite())
{
salesTable.insert();
//Order line
inventDim.clear();
inventDim.InventSiteId = #Site;
inventDim.InventLocationId = #Warehouse;
salesLine.clear();
salesLine.initValue(salesTable.SalesType);
salesLine.initFromSalesTable(salesTable);
salesLine.ItemId = #ItemId;
salesLine.initFromInventTable(InventTable::find(#ItemId));
salesLine.InventDimId = InventDim::findOrCreate(inventDim).inventDimId;
salesLine.SalesQty = #Qty;
salesLine.RemainSalesPhysical = salesLine.SalesQty;
salesLine.SalesUnit = #Unit;
salesLine.QtyOrdered = salesLine.calcQtyOrdered();
salesLine.RemainInventPhysical = salesLine.QtyOrdered;
salesLine.setPriceDisc(InventDim::find(salesLine.InventDimId));
if (salesLine.validateWrite())
{
salesLine.insert();
}
else
throw error("Order line");
}
else
throw error("Order header");
ttscommit;
}
catch
{
return "";
}
return salesLine.InventTransId;
}
|
private RecId findProcessJob(InventTransId _lotId)
{
SalesLine salesLine;
SourceDocumentLine sourceDocumentLineRequirement;
ReqPeggingAssignment reqPeggingAssignmentRequirement;
ReqPegging reqPegging;
ReqPeggingAssignment reqPeggingAssignmentSupply;
KanbanJobReceipt kanbanJobReceipt;
KanbanJob kanbanJob;
Kanban kanban;
select firstonly kanban
join kanbanJob
where kanbanJob.Kanban == kanban.RecId
exists join kanbanJobReceipt
where kanbanJobReceipt.KanbanJob == kanbanJob.RecId
exists join reqPeggingAssignmentSupply
where reqPeggingAssignmentSupply.SourceDocumentLine == kanbanJobReceipt.SourceDocumentLine &&
reqPeggingAssignmentSupply.ReqPeggingAssignmentType == ReqPeggingAssignmentType::Supply
exists join reqPegging
where reqPegging.PeggingAssignedSupply == reqPeggingAssignmentSupply.RecId
exists join reqPeggingAssignmentRequirement
where reqPeggingAssignmentRequirement.RecId == reqPegging.PeggingAssignedRequirement &&
reqPeggingAssignmentRequirement.ReqPeggingAssignmentType == ReqPeggingAssignmentType::Requirement
exists join sourceDocumentLineRequirement
where sourceDocumentLineRequirement.RecId == reqPeggingAssignmentRequirement.SourceDocumentLine &&
sourceDocumentLineRequirement.SourceRelationType == tableNum(SalesLine)
exists join salesLine
where salesLine.SourceDocumentLine == sourceDocumentLineRequirement.RecId &&
salesLine.InventTransId == _lotId;
return kanbanJob.RecId;
}
|
private RecId findTransferJob(RecId _recId)
{
KanbanJobPickingList kanbanJobPickingList;
SourceDocumentLine sourceDocumentLineRequirement;
ReqPeggingAssignment reqPeggingAssignmentRequirement;
ReqPegging reqPegging;
ReqPeggingAssignment reqPeggingAssignmentSupply;
KanbanJobReceipt kanbanJobReceipt;
KanbanJob kanbanJob;
Kanban kanban;
select firstonly kanban
join kanbanJob
where kanbanJob.Kanban == kanban.RecId
exists join kanbanJobReceipt
where kanbanJobReceipt.KanbanJob == kanbanJob.RecId
exists join reqPeggingAssignmentSupply
where reqPeggingAssignmentSupply.SourceDocumentLine == kanbanJobReceipt.SourceDocumentLine &&
reqPeggingAssignmentSupply.ReqPeggingAssignmentType == ReqPeggingAssignmentType::Supply
exists join reqPegging
where reqPegging.PeggingAssignedSupply == reqPeggingAssignmentSupply.RecId
exists join reqPeggingAssignmentRequirement
where reqPeggingAssignmentRequirement.RecId == reqPegging.PeggingAssignedRequirement &&
reqPeggingAssignmentRequirement.ReqPeggingAssignmentType == ReqPeggingAssignmentType::Requirement
exists join sourceDocumentLineRequirement
where sourceDocumentLineRequirement.RecId == reqPeggingAssignmentRequirement.SourceDocumentLine &&
sourceDocumentLineRequirement.SourceRelationType == tableNum(KanbanJobPickingList)
exists join kanbanJobPickingList
where kanbanJobPickingList.SourceDocumentLine == sourceDocumentLineRequirement.RecId &&
kanbanJobPickingList.Job == _recId;
return kanbanJob.RecId;
}
|
private void planPeggingTree(RecId _recId)
{
Args args = new Args();
Kanban kanban = Kanban::findKanbanJobRecId(_recId);
List list = new List(Types::Record);
list.addEnd(kanban);
args.caller(this);
args.object(list);
args.parmEnumType(enumNum(NoYes));
args.parmEnum(NoYes::Yes);
KanbanJobPeggingTreePlanEvent::main(args);
}
|
public void runScenario()
{
SalesId salesId;
ttsBegin;
lotId = this.createSalesLine();
processJob = this.findProcessJob(lotId);
transferJob = this.findTransferJob(processJob);
this.planPeggingTree(processJob);
this.createDemoTrans(lotId, transferJob, processJob);
salesId = SalesLine::findInventTransId(lotId).SalesId;
info(strFmt("Sales order %1 has been created!", salesId), "", SysInfoAction_TableField::newBuffer(SalesTable::find(salesId)));
ttsCommit;
}
|
private void updateDemoTrans(AlexLeanIoTDemoLotId _lotId,
LeanKanbanJobType _type)
{
ttsBegin;
select firstonly forupdate demoTrans
where demoTrans.LotId == _lotId;
if (demoTrans)
{
if (_type == LeanKanbanJobType::Transfer)
{
demoTrans.TransferComplete = true;
}
else //LeanKanbanJobType::Process
{
demoTrans.ProcessComplete = true;
}
demoTrans.update();
}
ttsCommit;
}
|
Let's review the list of methods implemented in Service Contract class
Name
|
Purpose
|
runScenario
|
Method used for demo automation (Section 3): It introduces Sales order demand, plans the entire pegging tree and creates a Staging Demo transaction
|
createSalesLine
|
Method used for demo automation (Section 3): It introduces Sales order demand
|
createDemoTrans
|
Method used for demo automation (Section 3): It creates Staging Demo transaction with links to Sales order line and associated Transfer job and Process job
|
planPeggingTree
|
Method used for demo automation (Section 3): It plan the entire pegging tree for a Sales order line
|
findTransferJob
|
Method used for demo automation (Section 3): It finds a corresponding to Sales order line Transfer job. Please note that typically using UI you find associated to Sales order line Transfer job(s), but we have to do it vice versa for this scenario
|
findProcessJob
|
Method used for demo automation (Section 3): It finds a corresponding to Sales order line Process job. Please note that typically using UI you find associated to Sales order line Process job(s), but we have to do it vice versa for this scenario
|
completeTransferJob
|
Method completes Transfer job. This method will be invoked from IoT device
|
completeProcessJob
|
Method completes Process job. This method will be invoked from IoT device
|
updateDemoTrans
|
Method updates Staging Demo transaction upon successful completion of Transfer or Process job accordingly. This method is called from completeTransferJob and completeProcessJob methods
|
After we implemented Service Contract Class we can create an associated Service, register it and then created an Inbound port to expose this Web Service
Inbound port
Section: Headless Background App
Now let's switch to Raspberry Pi device and develop a Headless Background App to control and read state(s) of sensors using Visual Studio (2015)
In order to begin with Background Application (IoT) development please select appropriate template in Visual Studio (2015) when creating a new project
New Project
Now you've got a blank app
Source code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Http;
using Windows.ApplicationModel.Background;
// The Background Application template is documented at http://go.microsoft.com/fwlink/?LinkID=533884&clcid=0x409
namespace AlexBackgroundApplication
{
public sealed class StartupTask : IBackgroundTask
{
public void Run(IBackgroundTaskInstance taskInstance)
{
//
// TODO: Insert code to start one or more asynchronous methods
//
}
}
}
|
Variation 1 using 1 sensor (Obstacle detection sensor)
Source code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Http;
using Windows.ApplicationModel.Background;
using Windows.Devices.Gpio;
using Windows.System.Threading;
using System.Diagnostics;
using System.Threading.Tasks;
using AlexBackgroundApplication.ServiceReference1;
namespace AlexBackgroundApplication
{
public sealed class StartupTask : IBackgroundTask
{
BackgroundTaskDeferral deferral;
private GpioPin pinR, pinG, pinB;
private GpioPin pinIRTIRR;
public void Run(IBackgroundTaskInstance taskInstance)
{
deferral = taskInstance.GetDeferral();
InitGPIO();
}
static async Task TransferComplete()
{
AlexLeanIoTDemoServiceClient client = new AlexLeanIoTDemoServiceClient();
client.ClientCredentials.Windows.ClientCredential.Domain = "CONTOSO";
client.ClientCredentials.Windows.ClientCredential.UserName = "Admin";
client.ClientCredentials.Windows.ClientCredential.Password = "pass@word1";
//CallContext context = new CallContext();
//context.Company = "USMF";
AlexLeanIoTDemoServiceCompleteTransferJobResponse x = await client.completeTransferJobAsync(0);
}
static async Task ProcessComplete()
{
AlexLeanIoTDemoServiceClient client = new AlexLeanIoTDemoServiceClient();
client.ClientCredentials.Windows.ClientCredential.Domain = "CONTOSO";
client.ClientCredentials.Windows.ClientCredential.UserName = "Admin";
client.ClientCredentials.Windows.ClientCredential.Password = "pass@word1";
//CallContext context = new CallContext();
//context.Company = "USMF";
AlexLeanIoTDemoServiceCompleteProcessJobResponse x = await client.completeProcessJobAsync(0);
}
private void InitGPIO()
{
Task t;
DateTime startTime = DateTime.Now, endTime;
double elapsedMillisecs;
bool flag = false;//Empty by default
pinIRTIRR = GpioController.GetDefault().OpenPin(12);
pinIRTIRR.SetDriveMode(GpioPinDriveMode.Input);
pinR = GpioController.GetDefault().OpenPin(13);
pinR.SetDriveMode(GpioPinDriveMode.Output);
pinG = GpioController.GetDefault().OpenPin(26);
pinG.SetDriveMode(GpioPinDriveMode.Output);
pinB = GpioController.GetDefault().OpenPin(16);
pinB.SetDriveMode(GpioPinDriveMode.Output);
pinR.Write(GpioPinValue.High);
pinG.Write(GpioPinValue.Low);
pinB.Write(GpioPinValue.Low);
while (true)
{
endTime = DateTime.Now;
elapsedMillisecs = ((TimeSpan)(endTime - startTime)).TotalMilliseconds;
if (elapsedMillisecs > 1000)
{
if (pinIRTIRR.Read() == GpioPinValue.Low)
{
if (flag)//Full -> Empty
{
t = ProcessComplete();
//t.Wait();
}
else //Empty -> Full
{
t = TransferComplete();
//t.Wait();
}
pinR.Write(flag ? GpioPinValue.High : GpioPinValue.Low);
pinG.Write(flag ? GpioPinValue.Low : GpioPinValue.High);
flag = flag ? false : true;
startTime = DateTime.Now;
}
}
}
}
}
}
|
As you can see from the code above I utilized GPIO Pin to collect info from Obstacle detection sensor
Please see the connection details for Variation 1 using 1 sensor below
Obstacle detection sensor
GPIO 12 <-> OUT
3.3V <-> +
GND <-> GND
LED
GPIO 13 <-> R
GPIO 26 <-> G
GPIO 16 <-> B
GND <-> -
|
Connections
Implementing Variation 1 using 1 sensor is pretty straightforward: all you need to watch for is when the signal level changes on Obstacle detection sensor GPIO Pin
Variation 2 (Infrared transmitter and Infrared receiver sensors)
Source code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Http;
using Windows.ApplicationModel.Background;
using Windows.Devices.Gpio;
using Windows.System.Threading;
using System.Diagnostics;
using System.Threading.Tasks;
using AlexBackgroundApplication.ServiceReference1;
namespace AlexBackgroundApplication
{
public sealed class StartupTask : IBackgroundTask
{
BackgroundTaskDeferral deferral;
private GpioPin pinR, pinG, pinB;
private GpioPin pinIRT, pinIRR;
private string color = "Blue";
public void Run(IBackgroundTaskInstance taskInstance)
{
deferral = taskInstance.GetDeferral();
InitGPIO();
}
static async Task TransferComplete()
{
AlexLeanIoTDemoServiceClient client = new AlexLeanIoTDemoServiceClient();
client.ClientCredentials.Windows.ClientCredential.Domain = "CONTOSO";
client.ClientCredentials.Windows.ClientCredential.UserName = "Admin";
client.ClientCredentials.Windows.ClientCredential.Password = "pass@word1";
//CallContext context = new CallContext();
//context.Company = "USMF";
AlexLeanIoTDemoServiceCompleteTransferJobResponse x = await client.completeTransferJobAsync(0);
}
static async Task ProcessComplete()
{
AlexLeanIoTDemoServiceClient client = new AlexLeanIoTDemoServiceClient();
client.ClientCredentials.Windows.ClientCredential.Domain = "CONTOSO";
client.ClientCredentials.Windows.ClientCredential.UserName = "Admin";
client.ClientCredentials.Windows.ClientCredential.Password = "pass@word1";
//CallContext context = new CallContext();
//context.Company = "USMF";
AlexLeanIoTDemoServiceCompleteProcessJobResponse x = await client.completeProcessJobAsync(0);
}
private void InitGPIO()
{
Task t;
DateTime startTimeImpulse = DateTime.Now, endTimeImpulse;
double elapsedMillisecsImpulse;
DateTime startTimeFound = DateTime.Now, endTimeFound;
double elapsedMillisecsFound;
bool impulse = false;
bool found = false;
pinR = GpioController.GetDefault().OpenPin(13);
pinR.SetDriveMode(GpioPinDriveMode.Output);
pinG = GpioController.GetDefault().OpenPin(26);
pinG.SetDriveMode(GpioPinDriveMode.Output);
pinB = GpioController.GetDefault().OpenPin(16);
pinB.SetDriveMode(GpioPinDriveMode.Output);
pinIRT = GpioController.GetDefault().OpenPin(27);
pinIRT.Write(GpioPinValue.Low);
pinIRT.SetDriveMode(GpioPinDriveMode.Output);
pinIRR = GpioController.GetDefault().OpenPin(18);
pinIRR.SetDriveMode(GpioPinDriveMode.Input);
pinR.Write(GpioPinValue.Low);
pinG.Write(GpioPinValue.Low);
pinB.Write(GpioPinValue.High);
color = "Blue";
while (true)
{
endTimeImpulse = DateTime.Now;
elapsedMillisecsImpulse = ((TimeSpan)(endTimeImpulse - startTimeImpulse)).TotalMilliseconds;
endTimeFound = DateTime.Now;
elapsedMillisecsFound = ((TimeSpan)(endTimeFound - startTimeFound)).TotalMilliseconds;
/* Impulse */
if (elapsedMillisecsImpulse > 500)
{
if (impulse == false)
{
pinIRT.Write(GpioPinValue.High);
}
else
{
pinIRT.Write(GpioPinValue.Low);
}
impulse = impulse ? false : true;
startTimeImpulse = DateTime.Now;
}
/* Impulse */
/* Scan */
if (pinIRR.Read() == GpioPinValue.Low)
{
found = true;
}
/* Scan */
/* Found */
if (elapsedMillisecsFound > 1000)
{
if (found == true)
{
if (color != "Red")
{
if (color == "Green")
{
t = ProcessComplete();
//t.Wait();
}
pinR.Write(GpioPinValue.High);
pinG.Write(GpioPinValue.Low);
pinB.Write(GpioPinValue.Low);
}
color = "Red";
}
else
{
if (color != "Green")
{
if (color == "Red")
{
t = TransferComplete();
//t.Wait();
}
pinR.Write(GpioPinValue.Low);
pinG.Write(GpioPinValue.High);
pinB.Write(GpioPinValue.Low);
}
color = "Green";
}
found = false;
startTimeFound = DateTime.Now;
}
/* Found */
}
}
}
}
|
The principle of how this scenario works is the following:
<![if !supportLists]>- <![endif]>Infrared transmitter will continuously generate impulses (every 500 milliseconds) – Blue sin graph below
<![if !supportLists]>- <![endif]>Infrared receiver will be continuously listening for impulses (check if receiver received an impulse within every 1000 milliseconds) – Red checkpoints below
Graph
As you can see from the code above I utilized GPIO Pins to control Infrared transmitter and Infrared receiver sensors
Please see the connection details for Variation 2 using 2 sensors below
Infrared transmitter
GPIO 27 <-> S
GND <-> -
Infrared receiver
GPIO 18 <-> S
3.3V <-> +
GND <-> -
LED
GPIO 13 <-> R
GPIO 26 <-> G
GPIO 16 <-> B
GND <-> -
|
Connections
Please notice that I introduced some time thresholds to make sensors less sensitive to changing conditions around and to make the scenario more stable.
Please note that in order to connect to Microsoft Dynamics AX 2012 R3 I added Service Reference to my project
Add Service Reference
There's a little caveat related to Add Service Reference step. In order to run Headless Background App on Raspberry Pi device or deploy the app to Raspberry Pi device you should be using Windows 10 machine with Visual Studio 2015 on it. In fact my Microsoft Dynamics AX 2012 R3 Demo environment was Windows 2012 OS with Visual Studio 2013.
Thus to Add Service Reference to Headless Background App project you need to make sure that WSDL URI for Microsoft Dynamics AX 2012 R3 is accessible
For the sake of this POC for simplicity I enabled HTTP/HTTPS endpoints on Microsoft Dynamics AX 2012 R3 Demo VM deployed by LCS as IaaS in Azure Cloud [not secure]
Azure VM endpoints
After that if using Microsoft Dynamics AX 2012 R3 Demo VM when Adding Service Reference you will need to enter domain credentials (for example, Admin)
IIS
Alternatively you may Add Service Reference to the project inside Microsoft Dynamics AX 2012 R3 Demo VM. I prefer to keep my development artifacts in a central place, so I went this route. Of course, I installed Visual Studio 2015 on Windows 2012 inside of Microsoft Dynamics AX 2012 R3 Demo VM. In fact before you can successfully load Background Application (IoT) in Visual Studio 2015 on Windows 2012 you need to make sure you install
<![if !supportLists]>- <![endif]>.NET Core on Windows: https://dotnet.readthedocs.org/en/latest/getting-started/installing-core-windows.html
<![if !supportLists]>- <![endif]>Windows IoT Core Project Templates: https://visualstudiogallery.msdn.microsoft.com/06507e74-41cf-47b2-b7fe-8a2624202d36 and follow the prompt Install missing features in Visual Studio if needed
Section: Demo automation
Finally we're going to create a Lean IoT Demo Cockpit for self-driven demo. Similar to Workflow processor Demo form in standard Microsoft Dynamics AX 2012 R3
This is how this form looks like
Demo
Start button begins the simulation process and Sales order demand will be automatically introduced based on timer. Stop button stop the simulation process. Clear button wipes Demo Staging table with Demo transactions
Clear
Please note that I introduced Demo Staging table to hold the data about what has been simulated. Thus IoT device will only see demand which has been simulated by Lean IoT Demo Cockpit
Source code
public class FormRun extends ObjectRun
{
#define.waitTime(60000) //1 min = 60000 millisecs
AlexLeanIoTDemoTrans demoTrans;
boolean running;
int i;
}
|
public void run()
{
super();
running = false;
i = 0;//counter
//this.runScenario();
}
|
void runScenario()
{
str message;
if (!running)
return;
AlexLeanIoTDemoService::main(new Args());
while select demoTrans
{
message += strFmt("Lot:%1|Transfer:%2|Process:%3", demoTrans.LotId,
demoTrans.TransferComplete,
demoTrans.ProcessComplete) + '\n';
}
i++;
statusDynamic.text(strFmt("runScenario (pass %1)", i) + '\n\n' + message);
this.setTimeOut(identifierstr(runScenario), #waitTime, false);
}
|
void setStart()
{
startStop.text("@SYS112484");
element.runScenario();
}
|
void setStartStop(boolean _running)
{
;
running = _running;
if (running)
element.setStart();
else
element.setStop();
}
|
void setStop()
{
startStop.text("@SYS112485");
statusDynamic.text('');
}
|
void clicked()
{
super();
delete_from demoTrans;
info("Demo Staging table is empty now!");
}
|
void clicked()
{
AlexLeanIoTDemoService service = new AlexLeanIoTDemoService();
super();
service.completeProcessJob(0);
info("Process job complete!");
}
|
void clicked()
{
AlexLeanIoTDemoService service = new AlexLeanIoTDemoService();
super();
service.runScenario();
}
|
void clicked()
{
super();
element.setStartStop(!running);
}
|
void clicked()
{
AlexLeanIoTDemoService service = new AlexLeanIoTDemoService();
super();
service.completeTransferJob(0);
info("Transfer job complete!");
}
|
Now let's begin the simulation by pressing Start button
Infolog
Infolog pops up indicating that Sales order demand has been introduced and the entire pegging tree was planned for Sales order line.
Sales order
Pegging tree
At this point I can use IoT device and replenish/pick products to/from Part location – and this will be reflected in Microsoft Dynamics AX 2012 R3 in form of completed Transfer or Process jobs.
When I physically put a part into Part location Transfer job will be completed automatically and the light will light up with Green indicating that the location is Full
Transfer job
Pegging tree
We can also verify the status of Transfer job which changes to Completed
When I physically pick a part from Part location Process job will be completed automatically and the light will light up with Red indicating that the location is Empty
Process job
Pegging tree
We can also verify the status of Process job which changes to Completed
During the simulation execution you can also review the results in real time
Cockpit
Note: Connection with IoT device is done by means of Web Services. Then IoT device reports completion of Transfer and Process jobs.
Infolog
Behind the scenes records in Demo Staging table get populated and updated
Demo Staging table
As you interact with IoT device putting and picking parts to/from Part location appropriate Transfer and Process jobs will automatically be updated/completed in Microsoft Dynamics AX 2012 R3
Please review the following videos describing how hardware part of the scenario works:
<![if !supportLists]>- <![endif]>Using 1 sensor (Obstacle detection sensor) for "Intelligent" location monitoring: http://1drv.ms/1Q7E2mS
<![if !supportLists]>- <![endif]>Using 2 sensors (IR-T + IR-R) for "Intelligent" location monitoring: http://1drv.ms/1Q7E3av
Summary: In this walkthrough I illustrated how to automate Make to Order Lean Manufacturing scenario in Microsoft Dynamics AX 2012 using IoT device. We discussed how you can invoke necessary functionality in Microsoft Dynamics AX 2012 R3 from IoT device, how to control sensors programmatically on Raspberry Pi and how to automate the demo scenario for self-driven demo experience.
Tags: Microsoft Dynamics AX 2012 R3, Internet of Things, IoT, Windows 10 IoT Core, Visual Studio 2015, Background Application (IoT), WCF Custom Web Services, X++, C#.NET.
Note: This document is intended for information purposes only, presented as it is with no warranties from the author. This document may be updated with more content to better outline the issues and describe the solutions.
Author: Alex Anikiev, PhD, MCP
Special thanks for collaboration in building this scenario goes to my colleague, Microsoft Dynamics AX Manufacturing expert, Dan Burke
Special thanks for collaboration in building this scenario goes to my colleague, Microsoft Dynamics AX Manufacturing expert, Dan Burke
A work of art Alex - Bravo !!! Documentation of your project is incredible. Can only imagine how long it took to pull this all together. Not sure why the choice to use infrared sensor technology to determine presence or absence of an object, has limitations as you correctly point out. Maybe that was all you had at your disposal to work with?
ReplyDeleteThinking about this same scenario myself in a real world environment I have leaned toward use of a pressure transducer based weight sensor. Not only can it determine the presence or absence of a physical object, knowing the weight of a single object that is expected in the location also allows determination of quantities present.
Hi Jack!
DeleteThank you for the interest in this scenario! All great feedback!
The choice of IR sensors was exactly because I had Sunfounder sensors kit (http://www.amazon.com/SunFounder-modules-Sensor-Raspberry-Extension/dp/B00P66XRNK) and those were the most appropriate sensors to use from the kit
Fully agree, using weight sensor would allow for a much better flexibility and control of quantities. I found this weight sensor to try in the future: http://www.amazon.com/Keyes-Weighing-Sensor-Module-Arduino/dp/B00NPZ4CPG
Also one of the considerations when building this scenario was simplicity, so I was looking for a setup without complex electrical circuits and a need for a breadboard
Appreciate your great feedback!
Thank you!
/Alex
This comment has been removed by the author.
ReplyDeleteIs your full source code for this project documented anywhere? I see you create an instance of the AlexLeanIoTDemoServiceClient, but I can't find where that class is defined. With that class, a connection to an AX system seems simple, but we don't have access to it in this post.
ReplyDeleteThanks!
Hi James!
DeleteAll the source code needed is listed throughout the article. Answering your question: AlexLeanIoTDemoServiceClient class is a proxy class that gets generated automatically in/by Visual Studio based on metadata when you add a Web Service reference
Thanks!
/Alex
Thank you so much for your answer. This is my first time programming a Web Service, so it's all new to me. I appreciate the help.
DeleteOne more question, Alex:
ReplyDeleteWhen I deploy my program to the raspberry pi, everything compiles fine and it starts to execute but then hits an exception on the line that instantiates our proxy client. In your example, the equivalent line is:
Alex... ...Client client = new AlexLeanIoTDemoServiceClient();
The code never gets to the point where I even pass the credentials.
The error message I get says:
PlatformNotSupportedException was unhandled by user code
An exception of type 'System.PlatformNotSupportedException' occurred in
System.Private.ServiceModel.dll but was not handled in user code
Additional information: UpnEndpointIdentity is not supported
Did you ever experience any issues like this? I'm able to use the web service in normal console apps, and only see this problem when I use it in a windows 10 universal app. I can't figure out if the issue is with my visual studio project or on the AX end.
Thanks so much for your help. I can't wait to see the pi in our office work like yours did in the demo we watched.
James