C#: управляем девайсом с компа

Некоторые задачи требуют подключения электронного устройства к персональному компьютеру. Самый простой и распространенный способ это сделать — использовать переходник USB-COM, или микросхему-мост USB-UART. Однако, на стороне ПК нам понадобится какая-то программа, с помощью которой мы будем управлять нашим девайсом.

Самое простое решение — использовать что-то готовое, например, терминальную программу PuTTY. Через нее в режиме командной строки можно отправлять команды в наше устройство и получать из него ответ:

Но в некоторых случаях недостаточно простой командной строки, нужен GUI (графический пользовательский интерфейс). Олдскульные парни для этих целей используют что-то типа Borland Delphi или Borland C++ Builder. Но нам в универе преподавали C#, поэтому я его и использую (к тому же я не перевариваю паскаль-подобный синтаксис, только c-like languages)) ). К тому же у C# есть удобный класс работы с последовательным портом, и ни какие костыли нам не понадобятся!

Недавно на работе поставили мне задачу, сделать переходник USB-UART к одному хитрому девайсу и написать для него прогу управления. Сам переходник описывать не буду, там ничего интересного нет, UART да двухполярное питание +-12 вольт. А вот на протоколе обмена немного остановлюсь.

Документация естественно на девайс написана через всем известное место, разбираться пришлось с помощью исходников прошивки на него.

;---------------------------------------------------------------------------
;
;
;       SDU     ATMEL   ATtiny2313
;---------------------------------------------------------------------------
;Программа контроллера детектора SDU-3C ФЭУ R6094
;команды:
;---------------------------------------------------------------------------
;Напряжение питаня ФЭУ
;VxxCR
;VxxCRLFOK.CRLF>
;80<xx<FF 
;---------------------------------------------------------------------------
;Усиление тракта
;GxxCR
;GxxCRLFOK.CRLF>
;01<xx<0F
;---------------------------------------------------------------------------
;Верхний уровень дискриминатора
;HxxCR
;HxxCRLFOK.CRLF>
;07<xx<FA
;---------------------------------------------------------------------------
;Нижний уровень дискриминатора
;LxxCR
;LxxCRLFOK.CRLF>
;07<xx<FA
;---------------------------------------------------------------------------;
;Окно сканирования (амплитудный спектр)
;Устанавливает Нижний уровень дискриминатора=0, Верхний=xx
;OxxCR
;OxxCRLFOK.CRLF>
;01<xx<32
;---------------------------------------------------------------------------
;Шаг сканирования (амплитудный спектр)
;Инкремент нижнего и верхнего порога дискриминатора
;возвращает значение нижнего порога
;+
;CRLFB=xxCRLF>
;01<xx<FF  - Нижний уровень дискриминатора
;---------------------------------------------------------------------------
;Чтение параметров из EEROM
;RCR
;RCRLFOK.CRLF>
;---------------------------------------------------------------------------
;Запись параметров в EEROM
;WCR
;WCRLFOK.CRLF>
;---------------------------------------------------------------------------
;Состояние
;Возвращает значения параметров
;SCR
;SCRLFL=xxCRLFH=yyCRLFV=zzCRLFG=vvCRLFO=wwCRLF>
;xx/yy/zz/vv/ww/  - значения параметров
;---------------------------------------------------------------------------
;Пробел - пустой символ
;
;---------------------------------------------------------------------------
;Возврат каретки
;CR
;CRLF>
;---------------------------------------------------------------------------
;---------------------------------------------------------------------------
;---------------------------------------------------------------------------

В заголовке исходников оказался хорошо расписан весь протокол обмена. В общих чертах он следующий: первый символ — некая буква, которая указывает, какую именно команду мы хотим выполнить. Затем, параметр, который представляет собой шестнадцатеричное число (может отсутствовать) и в конце команды символ ‘\r’. После получения символа ‘\r’ девайс начинает анализировать, что ему за команда пришла. После выполнения всех необходимых действий он выдает ответ, содержащий эхо входной команды, возвращаемое значение и признак конца ответа, который является символом ‘>’. Таким образом, алгоритм обмена прост: отправляем команду, заканчивающуюся на ‘\r’ и читаем ответ до тех пор, пока не получим символ ‘>’. Потом анализируем ответ.

Итак, приступаем. Создадим класс, который будет отвечать за низкоуровневый обмен с устройством и предоставлять удобный программный интерфейс управления.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO.Ports;
using System.Globalization;

namespace SDU3_GUI
{ 
    SerialPort _serialPort;
    class SDU3
    {
        public SDU3()
        {
            _serialPort = new SerialPort();
        }
    }
}

Класс называется SDU3 (так называется сам девайс). С помощью using System.IO.Ports; подключим пространство имен, в котором находится класс работы с COM-портом, _serialPort — наш объект СОМ-порта. У класса SerialPort есть очень удобный метод, с помощью которого можно получить все доступные в системе COM-порты. Это очень удобно, когда мы не знаем номер порта, который система присвоила переходнику USB-COM (или USB-UART, что в принципе то же самое) и нам не придется перебирать все порты от COM1 до COM_овер_дохрена. Чтож, добавим такой метод в наш класс SDU3

public string[] GetPorts()
{
    return SerialPort.GetPortNames();
}

Вызвав GetPorts() мы получим массив строк с именами доступных портов. После этого искать нужный порт методом перебора намного легче.

Следующий метод — открытие порта

public void Open(string port)
{
    //если порт открыть был раньше, 
    //то закрываем его
    if (_serialPort.IsOpen)
        _serialPort.Close();

    _serialPort.PortName = port; //присваиваем имя порта, например, "COM1"

    //это тайм-аут на чтение
    //очень полезная штука
    //если мы вызвали прочитать строку из порта,
    //и не получили ответ втечение ReadTimeout миллисекунд,
    //то получим исключение.
    //Удобно на случай, если девайс отвалился
    //и не дает ответ,
    //мы можем это правильно обратобать
    //и наша программа не повиснет намертво
    _serialPort.ReadTimeout = 500; 

    //тайм-аут на запись
    //насколько я понимаю, актуально только
    //при аппаратном управлении потоком
    //ну или вдруг кто-то внезапно выдернул шнур USB
    //с нашим переходником из компа.
    //У меня это не используется.
    _serialPort.WriteTimeout = 500;

    //Открываем порт.
    //так как мы не указывали скорость, контроль четности,
    //количество стоповых битов, то значения будут приняты
    //"стандартные": 9600, 1 стоп бит, нет контроля четности
    _serialPort.Open();
}

Более подробно все расписано на сайте мелкософта: https://docs.microsoft.com/ru-ru/dotnet/api/system.io.ports.serialport?view=netframework-4.7.1 или тут https://msdn.microsoft.com/ru-ru/library/system.io.ports.serialport(v=vs.110).aspx . Если вдруг ссылки протухнут, всегда можно воспользоваться гуглом, и по запросу c# serial port можно найти нужную инфу.

Ну и добавим несколько полезных методов, которые просто транслируют запрос далее:

public Boolean IsOpen()
{
    return _serialPort.IsOpen;
}

public void Close()
{
    _serialPort.Close();
}

Далее, перейдем к самому обмену. Начнем с приема ответа. Согласно протоколу обмена, ответ девайса заканчивается символом ‘>’. То есть, нам надо читать из порта до тех пор, пока не наткнемся на данный символ. В SerialPort как раз для этих целей есть очень удобный метод, который называется .ReadTo(). Таким образом, нам не надо самим анализировать поток данных из порта на предмет наличия онного символа, а просто написать так:

private string sdu3_Read()
{
    return _serialPort.ReadTo(">");
}

Стоит отметить, что сам символ ‘>’ в возвращаемой строке будет отсутствовать. А помните параметр ReadTimeout, который мы указали как 500 на этапе открытия порта? Так вот, если через 500 миллисекунд после вызова _serialPort.ReadTo(«>») девайс не ответит строкой с завершающим символом ‘>’, возникнет исключение, которое можно отловить в нужном месте кода, и вывести ошибку типа «Девайс отвалился!!!! Проверь провода!!!!!11111».

Перейдем теперь к реализации команд обмена. Начнем с команды получения всех настроек устройства. Она имеет вид «S\r». Согласно «документации» ответ мы получим в следующем виде:

S\r\nL=xx\r\nH=yy\r\nV=zz\r\nG=vv\r\nO=ww\r\n>
xx/yy/zz/vv/ww/  - значения параметров

Имена настроек обозначены буквами L, H, V, G, O. После буквы идет символ ‘=’, затем значение параметра в шестнадцатеричном значении, затем «\r\n», затем следующая настройка. Причем порядок следования настроек всегда одинаков, то есть на анализ имен настроек можно забить, а парсить только то, что идет после знака ‘=’.

Давайте разберемся, что надо сделать. Для начала разобьем строку ответа на подстроки по разделителю ‘\r’:

S
\nL=xx
\nH=yy
\nV=zz
\nG=vv
\nO=ww
\n>

У нас получилось 7 новых строк. Первую строку пропускаем, в ней нет ничего интересного для нас. Во второй строке находится параметр L. Давайте разобьем ее по разделителю ‘=’:

\nL
xx

Получим отдельно ‘\nL’ и значение данного параметра в шестнадцатеричном виде ‘xx’, которое далее можно преобразовать в число и сохранить куда надо.

Проделаем данную операцию и со строками 3, 4, 5, 6. Строка 7 не содержит для нас ничего интересного, выбрасываем. Стоит отметить, что символ ‘>’ будет в ней отсутствовать, так как функция sdu3_Read() его отбросит, но это не особо важно.

Для удобства создадим класс, который будет содержать все перечисленные параметры девайса для удобства работы с ними.

class SDU3_Status
{
    public int Gain;
    public int HiVoltage;
    public int UpBound;
    public int BottBound;
    public int WinSize;

    public string StatusToString()
    {
        string ret = "";
        ret += "Верняя граница:    " + Convert.ToString(this.UpBound) + "\r\n";
        ret += "Нижняя граница:    " + Convert.ToString(this.BottBound) + "\r\n";
        ret += "Окно сканирования: " + Convert.ToString(this.WinSize) + "\r\n";
        ret += "Напряжение ФЭУ:    " + Convert.ToString(this.HiVoltage) + "\r\n";
        ret += "Усиление:          " + Convert.ToString(this.Gain) + "\r\n";
        return ret;
    }
}

Ну и метод StatusToString() добавим, на случай, если мы захотим «красиво» вывести все значения переменных в виде текста. Ну и использовать как напоминалку себе, чтоб не забыть, что какой параметр означает)).

public SDU3_Status GetStatus()
{
    SDU3_Status ret = new SDU3_Status();
    _serialPort.DiscardInBuffer();
    _serialPort.DiscardOutBuffer();
    _serialPort.Write("S\r");
    string tmp;

    tmp = sdu3_Read();
    string[] arr_tmp = tmp.Split('\r');

    ret.BottBound = int.Parse(arr_tmp[1].Split('=')[1].Trim(), NumberStyles.HexNumber);
    ret.UpBound = int.Parse(arr_tmp[2].Split('=')[1].Trim(), NumberStyles.HexNumber);
    ret.HiVoltage = int.Parse(arr_tmp[3].Split('=')[1].Trim(), NumberStyles.HexNumber) * 4;
    ret.Gain = int.Parse(arr_tmp[4].Split('=')[1].Trim(), NumberStyles.HexNumber);
    ret.WinSize = int.Parse(arr_tmp[5].Split('=')[1].Trim(), NumberStyles.HexNumber);
    return ret;
}

Вот и сам метод получения всех параметров устройства. С помощью _serialPort.DiscardInBuffer() и _serialPort.DiscardOutBuffer() очищаем входные и выходные буферы порта от возможного мусора в них. Далее, отправляем в порт команду чтения всех параметров и читаем ответ в tmp. Далее, ответ парсим описанным выше способом, засовываем в ret и возвращаем считанные значения.

Это была самая сложная функция, дальше будет легче)).

Рассмотрим функцию, которая называется «Установить верхнюю границу дискриминатора» (не спрашивайте, что это означает)) ) «Lxx\r», где xx — шестнадцатеричное число.

Принимаемые значения лежат в диапазоне 0..255. Вот код функции:

public void SetBottBound(byte val)
{
    string tmp = string.Format("{0:X2}", val);
    _serialPort.Write("L" + tmp + "\r");
    tmp = sdu3_Read();
}

С помощью string.Format(«{0:X2}», val) мы преобразуем восьмибитное число в шестнадцатеричный формат, причем с незначащим нулем и с заглавным буквами. Например, число 13 преобразуется в 0D, а не в d, 0d, D, или 0xD. Далее, отправляем запрос в девайс и ждем ответ. Следует отметить, что нам интересен только сам факт ответа, а не он сам: если что-то вразумительное ответил, значит все норм. Остальные функции однотипны, и на реализации каждой из них останавливаться подробно не буду.

В итоге, у нас получился вот такой класс работы с девайсом:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO.Ports;
using System.Globalization;

namespace SDU3_GUI
{
    class SDU3_Status
    {
        public int Gain;
        public int HiVoltage;
        public int UpBound;
        public int BottBound;
        public int WinSize;

        public string StatusToString()
        {
            string ret = "";
            ret += "Верняя граница:    " + Convert.ToString(this.UpBound) + "\r\n";
            ret += "Нижняя граница:    " + Convert.ToString(this.BottBound) + "\r\n";
            ret += "Окно сканирования: " + Convert.ToString(this.WinSize) + "\r\n";
            ret += "Напряжение ФЭУ:    " + Convert.ToString(this.HiVoltage) + "\r\n";
            ret += "Усиление:          " + Convert.ToString(this.Gain) + "\r\n";
            return ret;
        }
    }

    class SDU3
    {
        SerialPort _serialPort;

        public SDU3()
        {
            _serialPort = new SerialPort();
        }

        public string[] GetPorts()
        {
            return SerialPort.GetPortNames();
        }

        public void Open(string port)
        {
            if (_serialPort.IsOpen)
                _serialPort.Close();

            _serialPort.PortName = port;
            _serialPort.ReadTimeout = 500;
            _serialPort.WriteTimeout = 500;
            _serialPort.Open();
        }

        public Boolean IsOpen()
        {
            return _serialPort.IsOpen;
        }

        public void Close()
        {
            _serialPort.Close();
        }

        public SDU3_Status GetStatus()
        {
            SDU3_Status ret = new SDU3_Status();
            _serialPort.DiscardInBuffer();
            _serialPort.DiscardOutBuffer();
            _serialPort.Write("S\r");
            string tmp;

            tmp = sdu3_Read();
            string[] arr_tmp = tmp.Split('\r');

            ret.BottBound = int.Parse(arr_tmp[1].Split('=')[1].Trim(), NumberStyles.HexNumber);
            ret.UpBound = int.Parse(arr_tmp[2].Split('=')[1].Trim(), NumberStyles.HexNumber);
            ret.HiVoltage = int.Parse(arr_tmp[3].Split('=')[1].Trim(), NumberStyles.HexNumber) * 4;
            ret.Gain = int.Parse(arr_tmp[4].Split('=')[1].Trim(), NumberStyles.HexNumber);
            ret.WinSize = int.Parse(arr_tmp[5].Split('=')[1].Trim(), NumberStyles.HexNumber);
            return ret;

        }

        private string sdu3_Read()
        {
            return _serialPort.ReadTo(">");
        }


        public void SetBottBound(byte val)
        {
            string tmp = string.Format("{0:X2}", val);
            _serialPort.Write("L" + tmp + "\r");
            tmp = sdu3_Read();
        }

        public void SetUpBound(byte val)
        {
            _serialPort.DiscardInBuffer();
            _serialPort.DiscardOutBuffer();
            string tmp = string.Format("{0:X2}", val);
            _serialPort.Write("H" + tmp + "\r");
            tmp = sdu3_Read();
        }

        public void SetGain(byte val)
        {
            _serialPort.DiscardInBuffer();
            _serialPort.DiscardOutBuffer();
            string tmp = string.Format("{0:X2}", val);
            _serialPort.Write("G" + tmp + "\r");
            tmp = sdu3_Read();
        }

        public void SetHiVoltage(int val)
        {
            _serialPort.DiscardInBuffer();
            _serialPort.DiscardOutBuffer();
            string tmp = string.Format("{0:X2}", (byte)(val/4));
            _serialPort.Write("V" + tmp + "\r");
            tmp = sdu3_Read();
        }

        public void SetWinSize(byte val)
        {
            _serialPort.DiscardInBuffer();
            _serialPort.DiscardOutBuffer();
            string tmp = string.Format("{0:X2}", val);
            _serialPort.Write("O" + tmp + "\r");
            tmp = sdu3_Read();
        }

        public void WriteEEPROM()
        {
            _serialPort.DiscardInBuffer();
            _serialPort.DiscardOutBuffer();
            string tmp;
            _serialPort.Write("W\r");
            tmp = sdu3_Read();
        }

        public void ReadEEPROM()
        {
            _serialPort.DiscardInBuffer();
            _serialPort.DiscardOutBuffer();
            string tmp;
            _serialPort.Write("R\r");
            tmp = sdu3_Read();
        }

    }

}

Далее, на основе этого класса уже довольно просто нарисовать GUI управления прибором. У меня получилось вот так:

Подробно останавливаться на реализации не буду, так как там все просто: нажали на кнопку — команда сквозняком пошла в наш класс SDU3.

На этом все, спасибо за внимание!))

Метки: , . Закладка Постоянная ссылка.

4 комментария: C#: управляем девайсом с компа

  1. RAVIL BURAEV пишет:

    вышлите пож-та исходники проекта для C# этой статьи

  2. Sergej пишет:

    Спасибо за хорошую статью!
    Вышлите пожалуйста исходники проекта для C# этой статьи. За ранее благодарен.

  3. kuznets пишет:

    Подскажите в чем вы делали проект(вижуал студио или еще что).
    зы с с# для винды не знаком(хочется ваш проект слегка переделать,да и GUI отредактировать)

Добавить комментарий для DiMoon Отменить ответ