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)] publicvoid 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; selectfirstonly 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)] publicvoid 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; selectfirstonly 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); } |
privatevoid 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; selectfirstonly kanban join kanbanJob where kanbanJob.Kanban == kanban.RecId existsjoin kanbanJobReceipt where kanbanJobReceipt.KanbanJob == kanbanJob.RecId existsjoin reqPeggingAssignmentSupply where reqPeggingAssignmentSupply.SourceDocumentLine == kanbanJobReceipt.SourceDocumentLine && reqPeggingAssignmentSupply.ReqPeggingAssignmentType == ReqPeggingAssignmentType::Supply existsjoin reqPegging where reqPegging.PeggingAssignedSupply == reqPeggingAssignmentSupply.RecId existsjoin reqPeggingAssignmentRequirement where reqPeggingAssignmentRequirement.RecId == reqPegging.PeggingAssignedRequirement && reqPeggingAssignmentRequirement.ReqPeggingAssignmentType == ReqPeggingAssignmentType::Requirement existsjoin sourceDocumentLineRequirement where sourceDocumentLineRequirement.RecId == reqPeggingAssignmentRequirement.SourceDocumentLine && sourceDocumentLineRequirement.SourceRelationType == tableNum(SalesLine) existsjoin 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; selectfirstonly kanban join kanbanJob where kanbanJob.Kanban == kanban.RecId existsjoin kanbanJobReceipt where kanbanJobReceipt.KanbanJob == kanbanJob.RecId existsjoin reqPeggingAssignmentSupply where reqPeggingAssignmentSupply.SourceDocumentLine == kanbanJobReceipt.SourceDocumentLine && reqPeggingAssignmentSupply.ReqPeggingAssignmentType == ReqPeggingAssignmentType::Supply existsjoin reqPegging where reqPegging.PeggingAssignedSupply == reqPeggingAssignmentSupply.RecId existsjoin reqPeggingAssignmentRequirement where reqPeggingAssignmentRequirement.RecId == reqPegging.PeggingAssignedRequirement && reqPeggingAssignmentRequirement.ReqPeggingAssignmentType == ReqPeggingAssignmentType::Requirement existsjoin sourceDocumentLineRequirement where sourceDocumentLineRequirement.RecId == reqPeggingAssignmentRequirement.SourceDocumentLine && sourceDocumentLineRequirement.SourceRelationType == tableNum(KanbanJobPickingList) existsjoin kanbanJobPickingList where kanbanJobPickingList.SourceDocumentLine == sourceDocumentLineRequirement.RecId && kanbanJobPickingList.Job == _recId; return kanbanJob.RecId; } |
privatevoid 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); } |
publicvoid 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; } |
privatevoid updateDemoTrans(AlexLeanIoTDemoLotId _lotId, LeanKanbanJobType _type) { ttsBegin; selectfirstonlyforupdate 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 { publicsealedclassStartupTask : IBackgroundTask { publicvoid 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 { publicsealedclassStartupTask : IBackgroundTask { BackgroundTaskDeferral deferral; privateGpioPin pinR, pinG, pinB; privateGpioPin pinIRTIRR; publicvoid Run(IBackgroundTaskInstance taskInstance) { deferral = taskInstance.GetDeferral(); InitGPIO(); } staticasyncTask TransferComplete() { AlexLeanIoTDemoServiceClient client = newAlexLeanIoTDemoServiceClient(); 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); } staticasyncTask ProcessComplete() { AlexLeanIoTDemoServiceClient client = newAlexLeanIoTDemoServiceClient(); 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); } privatevoid 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 { publicsealedclassStartupTask : IBackgroundTask { BackgroundTaskDeferral deferral; privateGpioPin pinR, pinG, pinB; privateGpioPin pinIRT, pinIRR; privatestring color = "Blue"; publicvoid Run(IBackgroundTaskInstance taskInstance) { deferral = taskInstance.GetDeferral(); InitGPIO(); } staticasyncTask TransferComplete() { AlexLeanIoTDemoServiceClient client = newAlexLeanIoTDemoServiceClient(); 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); } staticasyncTask ProcessComplete() { AlexLeanIoTDemoServiceClient client = newAlexLeanIoTDemoServiceClient(); 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); } privatevoid 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
publicclass FormRun extends ObjectRun { #define.waitTime(60000) //1 min = 60000 millisecs AlexLeanIoTDemoTrans demoTrans; boolean running; int i; } |
publicvoid run() { super(); running = false; i = 0;//counter //this.runScenario(); } |
void runScenario() { str message; if (!running) return; AlexLeanIoTDemoService::main(new Args()); whileselect 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