Accessing a custom DLL

I have built a C# Class Library (.dll) standalone project. I would like to include this dll in EPicor and make calls to it via a UD form by clicking a button on the UD form (e.g. UD26). Anyone know how I can do that? Where does my dll need to reside? I also would like this to play nice when our Epicor goes to the cloud. Can any of this be done?

If you’re running your own VM in the Cloud then you can do this. If you plan on moving to Epicor SaaS, you cannot load your code onto SaaS servers. Not for Epicor, not for Microsoft, na da.

What are you doing in the external dll? What version of Epicor are you running?

Ideally you would create a wrapper for this .dll (REST) and then it will be much easier to deal with now and in the future. If on-prem and needing to stay dll I would tell you to call it in a BPM (easy to add reference, etc.) but … if moving cloud, your options become limited to client side and referencing in customization.

Epicor 10. I have specific API calls to interface with a Fairbanks scale that weighs production piece parts.

It is going to the cloud. What’s the initial step I should do? I have a dll that works now in the manner I want it to but the folks that use Epicor want to use it as well so I would like to make it part of my system. How can I do that?

OK. That doesn’t have to be on the server then - unless you’re talking about a Terminal Server.

In .Net, you have access to the SerialPorts in a customization. I assume this is for Customer Shipment Entry. It was pretty easy to open the port when the form is opened. (You can keep the parameters in the workstation object or a UD Object.) Provide a button to read the scale, or if you’re like Chris Conn, add a timer to read the scale and update a control on the screen. And there you go. No dll required and you’re good to go in the cloud.

This is most of my code and what I have complied into a dll. This is what I would like to run from Epicor. Am I able to?

// Get the Guid for all system HIDs
Guid thisGuid;
HidD_GetHidGuid(out thisGuid);

        // Get a handle to a device information set for all devices present
        IntPtr h = SetupDiGetClassDevs(ref thisGuid, IntPtr.Zero, IntPtr.Zero, DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);

        if (h != (IntPtr)INVALID_HANDLE_VALUE)
        {
            Boolean bSuccess = true;
            uint i = 0;

            while (bSuccess)
            {
                // create a Device Interface Data structure
                SP_DEVICE_INTERFACE_DATA dia = new SP_DEVICE_INTERFACE_DATA();
                dia.cbSize = Marshal.SizeOf(dia);

                // start the enumeration
                bSuccess = SetupDiEnumDeviceInterfaces(h, IntPtr.Zero, ref thisGuid, i, ref dia);
                if (bSuccess)
                {
                    // build a device info structure
                    SP_DEVINFO_DATA da = new SP_DEVINFO_DATA();
                    da.cbSize = (uint)Marshal.SizeOf(da);

                    // build a Device Interface Detail Data structure
                    SP_DEVICE_INTERFACE_DETAIL_DATA didd = new SP_DEVICE_INTERFACE_DETAIL_DATA();
                    didd.cbSize = 4 + Marshal.SystemDefaultCharSize;

                    // Get more detailed info
                    uint nRequiredSize = 0;
                    uint nBytes = 255;
                    if (SetupDiGetDeviceInterfaceDetail(h, ref dia, ref didd, nBytes, ref nRequiredSize, ref da))
                    {
                        UInt32 ptrPrevious;
                        CM_Get_Parent(out ptrPrevious, da.DevInst, 0);

                        IntPtr ptrInstanceBuf = Marshal.AllocHGlobal((int)nBytes);
                        CM_Get_Device_ID(ptrPrevious, ptrInstanceBuf, (int)nBytes, 0);
                        string InstanceID = Marshal.PtrToStringAuto(ptrInstanceBuf);

                        Marshal.FreeHGlobal(ptrInstanceBuf);
                    }

                    //SetupDiGetDeviceInterfaceDetail(h, ref dia, ref didd, nBytes, ref nRequiredSize, ref da);

                    // We will use this handle (HidDevice) to read from the scale
                    HidDevice = CreateFile(didd.DevicePath.ToString(), GENERIC_READ|GENERIC_WRITE, 
                        (FILE_SHARE_READ|FILE_SHARE_WRITE), IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);

                    HIDD_ATTRIBUTES DeviceAttributes = new HIDD_ATTRIBUTES();
                    HidD_GetAttributes(HidDevice, ref DeviceAttributes);
                    byte[] ReadBuffer = new byte[6];
                    int numRead = 0;

                    // If, and only if, the device is our scale, we will read and parse data
                    if ((DeviceAttributes.ProductID == 0x555e) && (DeviceAttributes.VendorID == 0xb67))
                    {
                        // We have found our scale so now let's read the data from it
                        ReadFile(HidDevice, ReadBuffer, 6, out numRead, IntPtr.Zero);

                        String strReportID = "";
                        String strScaleStatus = "";
                        String strWeightUnit = "";
                        String strWeightData = "";
                        String strScaleData = "";

                        // Alway 3 for Fairbanks scales
                        strReportID = ReadBuffer[0].ToString();

                        // Positive weight
                        strScaleStatus = ReadBuffer[1] == 0x04 ? "":"";

                        // Negative weight 
                        strScaleStatus = ReadBuffer[1] == 0x05 ? "-":"";

                        // 0x03 for kilograms, 0x0c for pounds
                        strWeightUnit = ReadBuffer[2] == 0x03 ? "kg":"pounds";
                       

                        byte[] byWeight = { ReadBuffer[5], ReadBuffer[4] };
                        int nByteData = 0;

                        // Handle negative weight values
                        if (strScaleStatus.CompareTo("-") == 0)
                        {
                            nByteData = unchecked((sbyte)(ReadBuffer[5] * 256) + unchecked((sbyte)(ReadBuffer[4])));
                        }
                        else
                        {
                            // Its a positive weight
                            nByteData = (ReadBuffer[5] * 256) + (ReadBuffer[4]);
                        }                       
                        
                        // Format the weight bytes so the scaling exponent can be applied
                        // For example, 0xfe = -2. We then raise the multiplier to 10 ^ -2
                        // and multiply the read weight by the multiplier
                        strWeightData = nByteData.ToString().Replace("-", "").PadLeft(4, '0');
                        sbyte s = unchecked((sbyte)ReadBuffer[3]);
                       
                        double dbWeightData = double.Parse(strWeightData);
                        int nMultiplier = (int)(s);
                        double dbMultiplier = Convert.ToDouble(nMultiplier);
                        double dbWeight = Math.Pow(10, dbMultiplier) * dbWeightData;

                        // Display the weight in the text box and be done
                        strScaleData = strScaleStatus + dbWeight.ToString("0.00") + " " + strWeightUnit;
                        txtWeight.Text = strScaleData;
                        lblTxt.Text = strScaleData;

                        break;
                    }

                    i++;
                }
            }

            SetupDiDestroyDeviceInfoList(h);

When dealing with scales in the past, one usually just sends a CR or LF to the scale and it returns what’s going on. The code above is generic and is search your port space to find the scale. You’ll know what the port is, so that code is easy. Check out pages 173-4 in this Fairbanks scale guide.

You just send a LF. It sends back a string and you parse it out as described. There are examples of how to open the port in the message above at the Microsoft Docs site.This can all be done in a Customization.

First of all I agree with Mark Wonsil, you might not even need it.

Second, it looks like it is merely a Client Assembly (Not Server Side). Your Client files are on your PC and every other users PC, so as long as you remember to push out “Your Client Files” after upgrades, you shouldn’t have any problems. (But cant garuantee it’ll work with Kinetic Forms).

Now if you are on-prem, you can use Epicor Auto-Update to push out a .zip of additional client files (that is something you would be limited to in the cloud, unless they allow you to mess with the SaaS.sysconfig).

Thanks guys. I will try to do this through a customization as iterated above.

Give a shout if you run into trouble.

Will do.

Mark :slight_smile: at larger corporations atleast, we don’t use Auto-Update, that will destroy the bandwith, and folks come in the morning to clock in and boom “they have to wait 5min”. We push it out via SCCM or Citrix, so we always have our own bundle version of Client.

How about your company?

That’s smart. When the content network went down or the address was wrong, the AutoUpdate prevented users from logging in unless they added -skip to each workstation. :roll_eyes:

However, in the cloud, there was no customization of the clientrelease.zip. This proved inconvenient at times even for the Cloud Team when the Help reference was incorrect. We had to wait for the next patch cycle to fix it across the enterprises. I’d like to see the .sysconfig - especially for the Cloud - be stored…wait for it…in the cloud. Of course, when we get to an all web client, that will be the case. :wink:

Getting there slowly but surely :slight_smile:
https://download.epicorsaas.com/

1 Like

I put this together to read from a set of pallet wrapping scales that we have. Activated from a button click, but had to do some dancing around with event handlers or listeners rather than all of the code being on a button click:

using System.IO.Ports;

public class Script
{
	// ** Wizard Insert Location - Do Not Remove 'Begin/End Wizard Added Module Level Variables' Comments! **
	// Begin Wizard Added Module Level Variables **
	// End Wizard Added Module Level Variables **

	// Add Custom Module Level Variables Here **
	SerialPort _serialPort = new SerialPort("COM11", 9600, Parity.None, 8, StopBits.One); 

	public void InitializeCustomCode()
	{
		// ** Wizard Insert Location - Do not delete 'Begin/End Wizard Added Variable Initialization' lines **
		// Begin Wizard Added Variable Initialization
		// End Wizard Added Variable Initialization

		// Begin Wizard Added Custom Method Calls
			this.btnWeigh.Click += new System.EventHandler(this.btnWeigh_Click);
		// End Wizard Added Custom Method Calls

			_serialPort.DataReceived += new SerialDataReceivedEventHandler(sp_DataReceived);


	}

	public void DestroyCustomCode()
	{
		// ** Wizard Insert Location - Do not delete 'Begin/End Wizard Added Object Disposal' lines **
		// Begin Wizard Added Object Disposal

			this.btnWeigh.Click -= new System.EventHandler(this.btnWeigh_Click);

		// End Wizard Added Object Disposal

		// Begin Custom Code Disposal

			_serialPort.Dispose() ;
			_serialPort = null ;
			_serialPort.DataReceived -= new SerialDataReceivedEventHandler(sp_DataReceived);

		// End Custom Code Disposal
	}
                
	private delegate void SetTextDeleg(string text);
                
	private void sp_DataReceived(object sender, SerialDataReceivedEventArgs e)
	{              
		string data = _serialPort.ReadLine(); 
		double weight = Convert.ToDouble(data.Substring(6, 8));

		numActWeight.Value = weight;

		try
		{ 
			if(_serialPort.IsOpen) 
			_serialPort.Close(); 
		}
		catch(UnauthorizedAccessException ex)
		{
			MessageBox.Show(ex.Message);
		}              
	}

	private void si_DataReceived(string data) { numActWeight.Value = data.Trim(); } 

	private void btnWeigh_Click(object sender, System.EventArgs args)
	{
		_serialPort.WriteTimeout = 500; 

		try
		{ 
			if(!(_serialPort.IsOpen)) 

			_serialPort.Open(); 

			//_serialPort.Write("READ\r\n"); 
			_serialPort.Write("GR10\r\n");    // Added by MD - get decimal places from scale
		} 
		catch (Exception ex)
		{ 
			MessageBox.Show("Error opening/writing to serial port :: " + ex.Message, "Error!"); 
		} 
	}            
 

}


1 Like

I am trying to execute this in a customization. It only does the function twice and then opts out. On a C# windows app it goes through many times and finds my device. dia is a SP_DEVICE_INTERFACE_DATA struct and this Guid is a guid of HID device.

Boolean bSuccess = SetupDiEnumDeviceInterfaces(h, IntPtr.Zero, ref thisGuid, i, ref dia);

Do you see any reason why this function would return false prematurely?

Mark,
This is not a serial port device like we’ve been used to and worked with all these years but rather a HID device.It doesn’t map to a comport. The code I have above works in a C# Windows app but I need it to be accessible in Epicor. I originally tried to make it into a Class Library project (.dll) and wanted to try to make a call into the dll from Epicor but it was pointed out, as also seen from above, that the code can be placed into a customization. However, the SetupDiEnumInterfaces aborts the program after two passes where this same code works many times, about 6 or 7, in the windows app until it finds my specific HID scale. I am really not sure what to do now. I can’t make the customization work nor can I make a call into a dll because I don’t know how. I am a newbie to my company and Epicor but this was a primary reason why I got my job. To do stuff like this. If any of you guys have any other suggestions or hints, I would love to hear them. I am extremely discouraged and frustrated at this time. My experience lies in developing Windows desktop apps which I have done for 20 years. I guess that is why the appeal to my company. I do well with WYSIWYG type stuff.

The C# code above looks similar to the C++. You will probably need to add a reference and import this library:
Namespace: Windows.Devices.HumanInterfaceDevice
Assemblies: Windows.Devices.HumanInterfaceDevice.dll

It allows you to numerate the HID devices, and find your scale. The next step would be to register the InputReportReceived event to process the data:

HidInputReport inputReport = await device.GetInputReportAsync();

private void InputReportReceived(
    HidDevice sender, 
    HidInputReportReceivedEventArgs args)
    {
        HidInputReport inputReport = args.Report;
        IBuffer buffer = inputReport.Data;
        DataReader dr = DataReader.FromBuffer(buffer);
        byte[] bytes = new byte[inputReport.Data.Length];
        dr.ReadBytes(bytes);

        String inputReportContent = 
           System.Text.Encoding.ASCII.GetString(bytes);
    }

Start slow. Do one piece at a time and you’ll get through it. Epicor is a .Net application. Expand your skill set to the .Net world. PluralSight is free for April. Take some courses to learn the Framework. If you understand the far more complicated Win32 world, .Net will be a breeze for you. No more pointers!