6

I have 2 types of devices that have different protocols and are connected with a single serial port. By protocol, I mean that serial port configurations are different.

I have a protocol id p_id by which I can check which device is being currently read. Below is my code

Below is my main function which calls a class named CombinedEngine

 static class Program
 {
   private static CombinedEngine _eng;
   static async Task Main(string[] args)
    {
      try
      {
         _eng = new CombinedEngine();
      }
      catch (Exception ex)
      {
            Debug.WriteLine(ex.Message.ToString());
                //_log.Error(ex, ex.Message);
      }
    }
     while(true);
 }

Combined Engine Class

class CombinedEngine
{
   SerialPort port = new SerialPort();
   public CombinedEngine()
    {          

        try
        {
            
            var p = mdc.mdc_protocol.ToList();
            
            if(p.Count > 0)
            {
                foreach(var pr in p)
                {
                    var p_id = pr.protocol_id;

                    if(p_id=="01")//modbus
                    {
                        if (port.IsOpen)
                            port.Close();

                        port = new SerialPort("COM8", 9600, Parity.Even, 8, StopBits.One);
                        port.ReadTimeout = 500;
                        port.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);                          
                        port.Open();

                        Console.WriteLine("Port opened successfully for modbus...");
                        Console.WriteLine("I am Recieving for modbus...");


                        var result = mdc.mdc_meter_config.Where(m => m.config_flag == 0)
                            .Where(m=>m.p_id == p_id).ToList();

                        if (result.Count > 0)
                        {
                            foreach (var item in result)
                            {
                                var iteration = new Iterations()
                                {
                                    hex = (string)item.m_hex,
                                    row_id = (string)item.row_id,
                                    device_id = (int)item.meter_id,
                                    protocol_id = (string)item.p_id,
                                    command_id = (string)item.command_id,
                                    config_flag = (int)item.config_flag,
                                    msn = (string)item.msn,
                                    time = (string)item.time
                                };
                                confList.Add(iteration);
                                time = Convert.ToDouble(item.time);
                            }

                            var modbus = confList.Where(x => x.protocol_id == "01").ToList();
                            
                            aTimer = new System.Timers.Timer();


                            // Create a timer...
                            aTimer = new System.Timers.Timer();
                            // Hook up the Elapsed event for the timer. 
                            aTimer.Interval = time * 1000.0;
                            aTimer.Elapsed += (sender, e) => MyModbusMethod(sender, e, modbus, aTimer);            
                            aTimer.AutoReset = true;
                            aTimer.Enabled = true;

                        }
                        else
                        {

                            Console.WriteLine("No Data available");
                        }
                    }
                    else if(p_id=="02")//ytl_bus
                    {
                        if (port.IsOpen)
                            port.Close();

                        port = new SerialPort("COM8", 38400, Parity.None, 8, StopBits.One);
                        port.ReadTimeout = 500;
                        port.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
                        port.Open();

                        Console.WriteLine("Port opened successfully for ytlbus...");
                        Console.WriteLine("I am Recieving for ytlbus...");


                        var result = mdc.mdc_meter_config.Where(m => m.config_flag == 0)
                            .Where(m => m.p_id == p_id).ToList();

                        if (result.Count > 0)
                        {
                            foreach (var item in result)
                            {
                                var iteration = new Iterations()
                                {
                                    hex = (string)item.m_hex,
                                    row_id = (string)item.row_id,
                                    device_id = (int)item.meter_id,
                                    protocol_id = (string)item.p_id,
                                    command_id = (string)item.command_id,
                                    config_flag = (int)item.config_flag,
                                    msn = (string)item.msn,
                                    time = (string)item.time
                                };
                                confList.Add(iteration);
                                time = Convert.ToDouble(item.time);
                            }

                            
                            var ytlbus = confList.Where(x => x.protocol_id == "02").ToList();

                            

                            aTimer = new System.Timers.Timer();


                            // Create a timer...
                            aTimer = new System.Timers.Timer();
                            // Hook up the Elapsed event for the timer. 
                            aTimer.Interval = time * 1000.0;
                            aTimer.Elapsed += (sender, e) => MyElapsedMethod(sender, e,ytlbus , aTimer);            
                            aTimer.AutoReset = true;
                            aTimer.Enabled = true;

                        }
                        else
                        {

                            Console.WriteLine("No Data available");
                        }

                    }

                   
                    
                   
                }
               
            }
           

        }
        catch (Exception ex)
        {
            Console.WriteLine("Error at Line " + LineNumber(), ex.Message.ToString());
            throw ex;
        }
       
       
    }


}

In the above code, I have checked that if p_id is equal to 01 then modbus serial port configurations should be done. But if p_id is 02 then ytlbus serial port configurations should be encountered. Both devices have a different baud rate and a parity bit. So I have tried to set them

Also, I have a timer which is 60 seconds. So after every 60 seconds, the next timer will be initialized.

For example. If p_id is 01 the code sets the baud rate to 9600 and parity to Even. Then SerialDataRecievedEventHandler is called which will check for any incoming data from the devices and it will manage the data dumping into the DB.

Then the code will check the device details from a table mdc_meter_config and take out relevant information from it. All the details are added to the list one by one for all the devices. Also, the time would be carried out. In this case, all device's time is the same i.e. 60 seconds.

The list is then passed to a variable which is then passed to an ElapsedEventHandler function. The frame sending is handled by it.

The same will be done for p_id equals 02 the only difference is that it will set the baud rate to 38400 and parity to None.

What problem I am facing?

The above code runs, the issue that I am facing is that both conditions worked at the same time. i.e. for 01, it will work and then simultaneously it will jump to the 02 condition. Below is the image

enter image description here

It should complete the work for any p_id value, then complete the work for other p_id value.

Update 1

I have updated my code. Added a new async function, added only a single timer. and added a class for serial port extentions

    public static class SerialPortExtensions
{
    public async static Task ReadAsync(this SerialPort serialPort, byte[] buffer, int offset, int count)
    {
        var bytesToRead = count;
        var temp = new byte[count];

        while (bytesToRead > 0)
        {
            var readBytes = await serialPort.BaseStream.ReadAsync(temp, 0, bytesToRead);
            Array.Copy(temp, 0, buffer, offset + count - bytesToRead, readBytes);
            bytesToRead -= readBytes;
        }
    }

    public async static Task<byte[]> ReadAsync(this SerialPort serialPort, int count)
    {
        var buffer = new byte[count];
        await serialPort.ReadAsync(buffer, 0, count);
        return buffer;
    }
}

public CombinedEngine()
    {
        try
        {
            var p = mdc.mdc_protocol.ToList();

            if (p.Count > 0)
            {
                foreach (var pr in p)
                {
                    var p_id = pr.protocol_id;

                    if (p_id == "01")//modbus
                    {
                        if (port.IsOpen)
                            port.Close();
                        comm = true;
                        port = new SerialPort("COM8", 9600, Parity.Even, 8, StopBits.One);
                        port.ReadTimeout = 500;
                        //port.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
                        port.Open();
                        Work();

                        Console.WriteLine("Port opened successfully for modbus...");
                        Console.WriteLine("I am Recieving for modbus...");
                        
                    }
                    else if (p_id == "02")//ytl_bus
                    {
                        if (port.IsOpen)
                            port.Close();
                        comm = true;
                        port = new SerialPort("COM8", 38400, Parity.None, 8, StopBits.One);
                        port.ReadTimeout = 500;
                        //port.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);
                        port.Open();
                        Work();

                        Console.WriteLine("Port opened successfully for ytlbus...");
                        Console.WriteLine("I am Recieving for ytlbus...");                          

                    }
                    var result = mdc.mdc_meter_config.Where(m => m.config_flag == 0).ToList();
                    if (result.Count > 0)
                    {
                        foreach (var item in result)
                        {
                            var iteration = new Iterations()
                            {
                                hex = (string)item.m_hex,
                                row_id = (string)item.row_id,
                                device_id = (int)item.meter_id,
                                protocol_id = (string)item.p_id,
                                command_id = (string)item.command_id,
                                config_flag = (int)item.config_flag,
                                msn = (string)item.msn,
                                time = (string)item.time
                            };
                            confList.Add(iteration);
                            time = Convert.ToDouble(item.time);
                        }

                        var modbus = confList.Where(x => x.protocol_id == "01").ToList();
                        var ytlbus = confList.Where(x => x.protocol_id == "02").ToList();

                        //ModbusMethod(modbus);

                        aTimer = new System.Timers.Timer();
                        // Create a timer...
                        aTimer = new System.Timers.Timer();
                        // Hook up the Elapsed event for the timer. 
                        aTimer.Interval = time * 1000.0;
                        aTimer.Elapsed += (sender, e) => MyElapsedMethod(sender, e, ytlbus, modbus, aTimer);
                        //aTimer.Elapsed += OnTimedEvent(iterations, dataItems);            
                        aTimer.AutoReset = true;
                        aTimer.Enabled = true;

                    }
                    else
                    {

                        //Console.WriteLine("No Data available");
                    }

                }

            }


        }
        catch (Exception ex)
        {
            Console.WriteLine("Error at Line " + LineNumber(), ex.Message.ToString());
            throw ex;
        }
        finally
        {
        }


    }

public async void Work()
    {
        try
        {
            var data = await port.ReadAsync(4096);
            Console.WriteLine("Data at Line " + LineNumber(), data.ToString());
            //DoStuff(data);
        }
        catch (Exception ex)
        {
            Console.WriteLine("Error at Line " + LineNumber(), ex.Message.ToString());
        }
    }

The error now I am getting is The I/O operation has been aborted because of either a thread exit or an application request.

at System.IO.Ports.InternalResources.WinIOError(Int32 errorCode, String str) at System.IO.Ports.SerialStream.EndRead(IAsyncResult asyncResult) at System.IO.Stream.<>c.b__43_1(Stream stream, IAsyncResult asyncResult) at System.Threading.Tasks.TaskFactory1.FromAsyncTrimPromise1.Complete(TInstance thisRef, Func3 endMethod, IAsyncResult asyncResult, Boolean requiresSynchronization) at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter1.GetResult() at CommunicationProfile.SerialPortExtensions.d__0.MoveNext() in F:\MDC Development\Scheduler\CommunicationProfile\CombinedEngine.cs:line 1198 at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter.GetResult() at CommunicationProfile.SerialPortExtensions.d__1.MoveNext() in F:\MDC Development\Scheduler\CommunicationProfile\CombinedEngine.cs:line 1207 at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task) at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task) at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult() at CommunicationProfile.CombinedEngine.d__27.MoveNext() in F:\MDC Development\Scheduler\CommunicationProfile\CombinedEngine.cs:line 368

The error is occurring at below lines

var readBytes = await serialPort.BaseStream.ReadAsync(temp, 0, bytesToRead);//1198 line
await serialPort.ReadAsync(buffer, 0, count);//1207 line 
var data = await port.ReadAsync(4096); // 368 line

Note: The above method should be running continuously as the devices are powered on and will send their data after every 60 seconds.

Any help would be highly appreciated.

Moeez
  • 494
  • 9
  • 55
  • 147
  • 1
    For all I know, you _cannot_ have two peripherals on the same COM port at the same time. – Fildor Oct 05 '20 at 08:11
  • 1
    `while(true);` ... roastin' some CPU. Try a `Console.ReadLine();` instead. – Fildor Oct 05 '20 at 08:28
  • @Fildor I don't want it at the same time. I want to complete all the tasks for one protocol and then move towards the other and then complete it's operations and then move to the first and so on. – Moeez Oct 05 '20 at 09:15
  • 3
    Serial Port Communication doesn't work that way. It's like trying to plug two cables into the same port. If you have two peripherals, you need two ports. Regardless of whether they are physical or logical. – Fildor Oct 05 '20 at 09:18
  • @Fildor so what I concluded that there is no way to use different protocols on the same port? – Moeez Oct 05 '20 at 09:31
  • There is no point in doing that because you cannot have two devices on the same port. – Fildor Oct 05 '20 at 09:50
  • @Fildor But I can communicate devices with same protocol. – Moeez Oct 05 '20 at 10:08
  • No, you cannot unless they have some kind of bus functionality built in. That would also mean you connect device 1 to device2 and device2 to your machine. – Fildor Oct 05 '20 at 10:23
  • 5
    It may be connected by RS485 multi-drop instead of the generic RS232C. This is a hardware method that is often used in industrial equipment. In that case, at least the protocol for determining the device number/address to be communicated will need to be common. However, it is possible that some method has already been built. They are system-specific issues that you want to use, not general knowledge questions like this site. Talk to people such as retailers, vendors, and system consultants for your equipment. – kunif Oct 05 '20 at 10:33
  • @kunif I am using `RS232` to `RS485` convertor. – Moeez Oct 05 '20 at 10:58
  • @Fildor I have managed to communicate the same protocol devices. – Moeez Oct 05 '20 at 10:58
  • see kunif's comment then. This is _very_ specific to the hardware in use. – Fildor Oct 05 '20 at 10:59
  • Yes I have mentioned it in the comment that I am using `RS232` to `RS485` convertor. So I am able to communicate – Moeez Oct 05 '20 at 11:00
  • Right now your code just opens the port, attaches some handlers, and then skips to the next iteration (which closes it and reopens it). You need to rewrite this completely, the simplest way to use [`await` on the `SerialPort.BaseStream`](https://stackoverflow.com/a/32436104/69809), i.e. await the data, process it, and then skip to the next step. If you are planning to use the `DataReceived` event, then your state needs to be changed inside that event handler. As Fildor wrote, `while(true);` is a bad idea, just await on the method in Main. Also, constructors shouldn't do this much work. – vgru Oct 05 '20 at 13:07
  • ytlbus may be the proprietary protocol of this company. [Single Phase Multi-functional RS485 Meter](http://yongtl.wh-hwht.com/sell/itemid-91653.shtml) `D113017-01 is a single phase multi-functional energy meter with YTL-BUS protocol.` [Sub-metering, single-phase two-wire, 100A](https://mall.ytl-e.com/product/details/7) Please contact this company. – kunif Oct 05 '20 at 14:55
  • @Groo should i use await in my `DataRecieved` handler? – Moeez Oct 06 '20 at 05:13
  • @Moeez No, if you're going to use `await`/`async` you don't need the event at all, check out the [example I linked above](https://stackoverflow.com/a/32436104/69809). Just add the extensions class and call `await serialPort.ReadAsync(length);` inside a method marked with `async`. – vgru Oct 06 '20 at 09:49
  • @Groo I have tried to implement according to the source you have given to me. Kindly see the `update 1` – Moeez Oct 07 '20 at 10:33
  • are both bus's writing simultaneously? – justjoshin Oct 08 '20 at 06:16
  • @justjoshin yes – Moeez Oct 08 '20 at 06:36
  • 4
    do you have the pin mappings from dual rs-485 to rs-232? I don't think you can do what you are trying to do (at least not the way you're trying to). You are trying to read two different serial protocols at different speeds (at from one serial port). You would need to read in the entire stream at the highest speed (luckily it is a multiple of the lower speed), and then de-mux it in code. This would be a buttload easier if you talk the hardware guys into giving you 2 ports. With 2 ports, the code will take hours to write. With 1 port it might take months to write and still be unreliable. – justjoshin Oct 08 '20 at 06:46
  • @justjoshin no I don't have dual mapping. Using 2 ports is much easier and I have already implemented it. But the main reason for doing is this, that this solution will be going live and I have to manage it using a single port with multiple protocols. – Moeez Oct 09 '20 at 05:02
  • @Moeez are you able to configure the devices ? I mean changing baud rate and parity or example ? or you have not any control over devices ? it's very important . I think you can do at the same port but depend on the answers of my questions. – A Farmanbar Oct 09 '20 at 19:13
  • @Moeez: the problem here is not the serial port, it's the asynchronous programming. You want to execute two different operations in sequence, but instead you are doing in parallel. With this last update, the problem is that you are calling `Work()` without `await`, so the call just creates an async background task and doesn't wait for its completion. I believe the compiler is already telling you (check your warnings). Also, as I wrote above, don't write this inside a constructor, create a separate `async` method and again `await` it. Without `await`, you are just creating background tasks. – vgru Oct 12 '20 at 07:04

1 Answers1

2

The main problem with the last revision of your code is that you are calling Work() without await, so the call just creates an asynchronous background task and doesn't wait for its completion. Also, this functionality should not exist inside the constructor, but in a separate async method.

Second suggestion is to remove the if/switch statements from the loop, and place the data needed to differentiate these protocols in a separate class. You could place any additional properties needed for each protocol inside this class:

// contains specific settings for each ProtocolId
class ProtocolCfg
{
    public string ProtocolId { get; set; }
    public string PortName { get; set; }
    public int BaudRate { get; set; }
    public Parity Parity { get; set; }
    public int DataBits { get; set; }
    public StopBits StopBits { get; set; }

    public ProtocolCfg(string id, string port, int baud, Parity parity, int bits, StopBits stop)
    {
        ProtocolId = id; PortName = port; BaudRate = baud; Parity = parity;
        DataBits = bits; StopBits = stop;
    }
}

With this in place, your for loop doesn't need to differentiate between these protocols:

class CombinedEngine
{
    readonly ProtocolCfg[] _portConfigs;

    public CombinedEngine(ProtocolCfg[] portConfigs)
    {
        // just assign the field and do nothing else
        _portConfigs = portConfigs;
    }

    public async Task Run(CancellationToken cancelToken)
    {
        // repeat indefinitely
        while (!cancelToken.IsCancellationRequested)
        {
            // run all protocols
            foreach (var portcfg in _portConfigs)
            {
                SerialPort serialPort = null;

                try
                {
                    // init using current config
                    serialPort = new SerialPort(
                         portcfg.PortName, portcfg.BaudRate, portcfg.Parity,
                         portcfg.DataBits, portcfg.StopBits);

                    serialPort.ReadTimeout = 500;

                    // await data
                    var data = await serialPort.ReadAsync(4096);

                    // do something with this data
                    Console.WriteLine($"P{portcfg.ProtocolId}: {data.Length}B received");

                    // do other stuff here

                    // wait between protocol changes if needed?
                    await Task.Delay(500, cancelToken);
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.ToString());
                }
                finally
                {
                    serialPort?.Close();
                    serialPort?.Dispose();
                }
            }

            // wait between iterations?
            await Task.Delay(500, cancelToken);
        }
    }
}

When calling the Run function, remember that it's async, so you need to call await. However, you also may want to wait for a keypress inside the console, so in that case you would store the returned Task in a variable, and cancel it when needed:

class Program
{
    static void Main(string[] args)
    {
        // define all possible protocols
        var protocols = new[]
        {
            new ProtocolCfg("01", "COM8",  9600, Parity.Even, 8, StopBits.One),
            new ProtocolCfg("02", "COM8", 38400, Parity.None, 8, StopBits.One)
        };

        // we will need this to tell the async task to end
        var tokenSource = new CancellationTokenSource();
        var token = tokenSource.Token; 

        // note that this constructor does not do anything of importance
        var engine = new CombinedEngine(protocols);

        // this is where all the work is done, pass the cancellation token 
        var task = engine.Run(token);

        // wait until Q is pressed
        Console.WriteLine("Running, press Q to quit... ");
        ConsoleKey k;
        do { k = Console.ReadKey().Key; }
        while (k != ConsoleKey.Q);

        // shutdown
        tokenSource.Cancel();
        task.Wait();            
        Console.WriteLine("Done.");
    }        
}
vgru
  • 49,838
  • 16
  • 120
  • 201
  • One thing more I want to add is that. I have added a Timer, which is enabled after every `N` minutes lets say `1` minute. How can I manage/add it inside your provided solution? – Moeez Oct 14 '20 at 09:19
  • @Moeez: can you clarify this a bit, i.e. what does this Timer do every `N` minutes? Given the way this loop is implemented, it's important to clarify how exactly this periodic task would interact with the serial port in three different states (closed, connected with 9600 baudrate, connected with 38400 baudrate). For example, instead of using a timer, you can wait 1 minute using the `Task.Delay()` call at the end of the function. – vgru Oct 17 '20 at 18:36