Saturday, September 12, 2015

Microsoft Dynamics AX 2012 Manufacturing – Lean IoT Scenario Part 3: Software/Demo Automation

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

<![if !vml]><![endif]>
 
And this is a more detailed diagram which has hardware implementation details too
 
Diagram

<![if !vml]><![endif]>
 
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
 
<![if !vml]><![endif]>

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

7 comments:

  1. 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?

    Thinking 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.

    ReplyDelete
    Replies
    1. Hi Jack!

      Thank 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

      Delete
  2. This comment has been removed by the author.

    ReplyDelete
  3. Is 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.

    Thanks!

    ReplyDelete
    Replies
    1. Hi James!
      All 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

      Delete
    2. 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.

      Delete
  4. One more question, Alex:

    When 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

    ReplyDelete