Как стать автором
Обновить

Пробрасываем порт UART из Linux в Windows через SSH-соединение

Время на прочтение15 мин
Количество просмотров8.7K
Не так давно в нашем сервисе All-Hardware произошло знаковое событие. Если раньше порт UART был подключён только к терминалу внутри браузера, то теперь можно установить в Windows специальный драйвер, через который этот порт будет проброшен в вашу локальную систему. Теперь работа с удалённой платой даёт что-то большее, чем написание «Hello World». Вы можете запустить на своей локальной машине программы, которые работают с COM-портом, и обмениваться с удалённой платой по сложным протоколам. Из того, что уже реализовано на практике, – измерение тока потребления платой STM32G474RE DPOW1 Discovery.

Лично я в эпопее по внедрению данной функциональности участия не принимал, так как в то время одомашнивал оборотней (осваивал работу с синтезируемым ядром RISC-V — SweRVolf), но расспросил участников, какие технологии они применяли. В этой статье я покажу физические принципы, лежащие в основе проброса. Практический выхлоп будет состоять в том, что вы сможете пробрасывать порт, не устанавливая никаких драйверов, но реализуя особую (а не универсальную) логику в своей программе. Ну, и вы поймёте, как можно пробросить порт не только для нашего сервиса, а вообще из ОС Linux.



1. Введение


Сначала стоит напомнить, что такое сервис All-Hardware. Подробно про него можно почитать в моих предыдущих статьях (раз и два). Есть также статья моего коллеги. А если кратко, то это такой сайт, через который можно поработать с отладочными платами на базе микроконтроллеров. Сейчас там есть STM32 и NXP. При этом вы пишете и компилируете код на своей машине, а затем просто подключаетесь отладчиком к нашему серверу. Все современные средства отладки позволяют работать не только локально, но и удалённо, и сервис эту возможность использует. Ну, а для обратной связи на сервере имеется видеокамера, которая снимает состояние платы (её светодиоды и экран).

Но реальная работа с платами обычно подразумевает что-то большее, чем пошаговую отладку и просмотр экрана. Платы должны что-то делать. И для этого у них имеются порты. С древнейших времён в нашем сервисе был терминал, через который можно было общаться с UART. Однако интерактивная работа – это хорошо, но работа собственных программ – лучше. И при внедрении платы STM32G474RE DPOW1 Discovery производитель решил, что на локальных машинах пользователей просто необходимо запускать готовую программу, которая в обычной жизни общается с платой через COM-порт Windows. Ребята написали драйвер уровня ядра, благодаря которому на локальных машинах появляется обычный COM-порт. Но физически он пробрасывается на наш сервер. Какие при этом используются технологии – мы сейчас и рассмотрим. Свой драйвер мы не сделаем. Но при разработке своей программы под Windows нет особой разницы, общаться с COM-портом или вызывать иные функции. А вот вызывать функции, которые будут реализовывать проброс на любой Линуксовый сервер, а не только наш – мы научимся.

Однако сначала я просто обязан упомянуть про конкурс разработки прошивок для плат, размещенных на сервисе All-Hardware, который идёт как раз в то время, когда статья выкладывается в публикацию. Вдруг это будет кому-то интересно. Конкурс продлится до 24 апреля.

2. Теория со стороны Linux


2.1 Полезная утилита socat


Центральным элементом для проброса портов является забавная утилита socat. Она позволяет пробрасывать данные из одного источника в другой. Список возможных источников – просто широчайший. Установите эту программу:
sudo apt-get install socat

и затем – подайте:
socat –h

Под катом – простыня, описывающая все возможности пробросов…

Смотреть.
socat by Gerhard Rieger and contributors - see www.dest-unreach.org
Usage:
socat [options] <bi-address> <bi-address>
   options:
      -V     print version and feature information to stdout, and exit
      -h|-?  print a help text describing command line options and addresses
      -hh    like -h, plus a list of all common address option names
      -hhh   like -hh, plus a list of all available address option names
      -d[ddd]         increase verbosity (use up to 4 times; 2 are recommended)
      -D     analyze file descriptors before loop
      -ly[facility]  log to syslog, using facility (default is daemon)
      -lf<logfile>   log to file
      -ls            log to stderr (default if no other log)
      -lm[facility]  mixed log mode (stderr during initialization, then syslog)
      -lp<progname>  set the program name used for logging
      -lu            use microseconds for logging timestamps
      -lh            add hostname to log messages
      -v     verbose data traffic, text
      -x     verbose data traffic, hexadecimal
      -b<size_t>     set data buffer size (8192)
      -s     sloppy (continue on error)
      -t<timeout>    wait seconds before closing second channel
      -T<timeout>    total inactivity timeout in seconds
      -u     unidirectional mode (left to right)
      -U     unidirectional mode (right to left)
      -g     do not check option groups
      -L <lockfile>  try to obtain lock, or fail
      -W <lockfile>  try to obtain lock, or wait
      -4     prefer IPv4 if version is not explicitly specified
      -6     prefer IPv6 if version is not explicitly specified
   bi-address:
      pipe[,<opts>]     groups=FD,FIFO
      <single-address>!!<single-address>
      <single-address>
   single-address:
      <address-head>[,<opts>]
   address-head:
      abstract-client:<filename>        groups=FD,SOCKET,RETRY,UNIX
      abstract-connect:<filename>       groups=FD,SOCKET,RETRY,UNIX
      abstract-listen:<filename>        groups=FD,SOCKET,LISTEN,CHILD,RETRY,UNIX
      abstract-recv:<filename>  groups=FD,SOCKET,RETRY,UNIX
      abstract-recvfrom:<filename>      groups=FD,SOCKET,CHILD,RETRY,UNIX
      abstract-sendto:<filename>        groups=FD,SOCKET,RETRY,UNIX
      create:<filename> groups=FD,REG,NAMED
      exec:<command-line>       groups=FD,FIFO,SOCKET,EXEC,FORK,TERMIOS,PTY,PARENT,UNIX
      fd:<num>  groups=FD,FIFO,CHR,BLK,REG,SOCKET,TERMIOS,UNIX,IP4,IP6,UDP,TCP,SCTP
      gopen:<filename>  groups=FD,FIFO,CHR,BLK,REG,SOCKET,NAMED,OPEN,TERMIOS,UNIX
      interface:<interface>     groups=FD,SOCKET
      ip-datagram:<host>:<protocol>     groups=FD,SOCKET,RANGE,IP4,IP6
      ip-recv:<protocol>        groups=FD,SOCKET,RANGE,IP4,IP6
      ip-recvfrom:<protocol>    groups=FD,SOCKET,CHILD,RANGE,IP4,IP6
      ip-sendto:<host>:<protocol>       groups=FD,SOCKET,IP4,IP6
      ip4-datagram:<host>:<protocol>    groups=FD,SOCKET,RANGE,IP4
      ip4-recv:<protocol>       groups=FD,SOCKET,RANGE,IP4
      ip4-recvfrom:<protocol>   groups=FD,SOCKET,CHILD,RANGE,IP4
      ip4-sendto:<host>:<protocol>      groups=FD,SOCKET,IP4
      ip6-datagram:<host>:<protocol>    groups=FD,SOCKET,RANGE,IP6
      ip6-recv:<protocol>       groups=FD,SOCKET,RANGE,IP6
      ip6-recvfrom:<protocol>   groups=FD,SOCKET,CHILD,RANGE,IP6
      ip6-sendto:<host>:<protocol>      groups=FD,SOCKET,IP6
      open:<filename>   groups=FD,FIFO,CHR,BLK,REG,NAMED,OPEN,TERMIOS
      openssl:<host>:<port>     groups=FD,SOCKET,CHILD,RETRY,IP4,IP6,TCP,OPENSSL
      openssl-listen:<port>     groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP4,IP6,TCP,OPENSSL
      pipe:<filename>   groups=FD,FIFO,NAMED,OPEN
      proxy:<proxy-server>:<host>:<port>        groups=FD,SOCKET,CHILD,RETRY,IP4,IP6,TCP,HTTP
      pty       groups=FD,NAMED,TERMIOS,PTY
      sctp-connect:<host>:<port>        groups=FD,SOCKET,CHILD,RETRY,IP4,IP6,SCTP
      sctp-listen:<port>        groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP4,IP6,SCTP
      sctp4-connect:<host>:<port>       groups=FD,SOCKET,CHILD,RETRY,IP4,SCTP
      sctp4-listen:<port>       groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP4,SCTP
      sctp6-connect:<host>:<port>       groups=FD,SOCKET,CHILD,RETRY,IP6,SCTP
      sctp6-listen:<port>       groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP6,SCTP
      socket-connect:<domain>:<protocol>:<remote-address>       groups=FD,SOCKET,CHILD,RETRY
      socket-datagram:<domain>:<type>:<protocol>:<remote-address>       groups=FD,SOCKET,RANGE
      socket-listen:<domain>:<protocol>:<local-address> groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE
      socket-recv:<domain>:<type>:<protocol>:<local-address>    groups=FD,SOCKET,RANGE
      socket-recvfrom:<domain>:<type>:<protocol>:<local-address>        groups=FD,SOCKET,CHILD,RANGE
      socket-sendto:<domain>:<type>:<protocol>:<remote-address> groups=FD,SOCKET
      socks4:<socks-server>:<host>:<port>       groups=FD,SOCKET,CHILD,RETRY,IP4,IP6,TCP,SOCKS4
      socks4a:<socks-server>:<host>:<port>      groups=FD,SOCKET,CHILD,RETRY,IP4,IP6,TCP,SOCKS4
      stderr    groups=FD,FIFO,CHR,BLK,REG,SOCKET,TERMIOS,UNIX,IP4,IP6,UDP,TCP,SCTP
      stdin     groups=FD,FIFO,CHR,BLK,REG,SOCKET,TERMIOS,UNIX,IP4,IP6,UDP,TCP,SCTP
      stdio     groups=FD,FIFO,CHR,BLK,REG,SOCKET,TERMIOS,UNIX,IP4,IP6,UDP,TCP,SCTP
      stdout    groups=FD,FIFO,CHR,BLK,REG,SOCKET,TERMIOS,UNIX,IP4,IP6,UDP,TCP,SCTP
      system:<shell-command>    groups=FD,FIFO,SOCKET,EXEC,FORK,TERMIOS,PTY,PARENT,UNIX
      tcp-connect:<host>:<port> groups=FD,SOCKET,CHILD,RETRY,IP4,IP6,TCP
      tcp-listen:<port> groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP4,IP6,TCP
      tcp4-connect:<host>:<port>        groups=FD,SOCKET,CHILD,RETRY,IP4,TCP
      tcp4-listen:<port>        groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP4,TCP
      tcp6-connect:<host>:<port>        groups=FD,SOCKET,CHILD,RETRY,IP6,TCP
      tcp6-listen:<port>        groups=FD,SOCKET,LISTEN,CHILD,RETRY,RANGE,IP6,TCP
      tun[:<ip-addr>/<bits>]    groups=FD,CHR,NAMED,OPEN,INTERFACE
      udp-connect:<host>:<port> groups=FD,SOCKET,IP4,IP6,UDP
      udp-datagram:<host>:<port>        groups=FD,SOCKET,RANGE,IP4,IP6,UDP
      udp-listen:<port> groups=FD,SOCKET,LISTEN,CHILD,RANGE,IP4,IP6,UDP
      udp-recv:<port>   groups=FD,SOCKET,RANGE,IP4,IP6,UDP
      udp-recvfrom:<port>       groups=FD,SOCKET,CHILD,RANGE,IP4,IP6,UDP
      udp-sendto:<host>:<port>  groups=FD,SOCKET,IP4,IP6,UDP
      udp4-connect:<host>:<port>        groups=FD,SOCKET,IP4,UDP
      udp4-datagram:<remote-address>:<port>     groups=FD,SOCKET,RANGE,IP4,UDP
      udp4-listen:<port>        groups=FD,SOCKET,LISTEN,CHILD,RANGE,IP4,UDP
      udp4-recv:<port>  groups=FD,SOCKET,RANGE,IP4,UDP
      udp4-recvfrom:<host>:<port>       groups=FD,SOCKET,CHILD,RANGE,IP4,UDP
      udp4-sendto:<host>:<port> groups=FD,SOCKET,IP4,UDP
      udp6-connect:<host>:<port>        groups=FD,SOCKET,IP6,UDP
      udp6-datagram:<host>:<port>       groups=FD,SOCKET,RANGE,IP6,UDP
      udp6-listen:<port>        groups=FD,SOCKET,LISTEN,CHILD,RANGE,IP6,UDP
      udp6-recv:<port>  groups=FD,SOCKET,RANGE,IP6,UDP
      udp6-recvfrom:<port>      groups=FD,SOCKET,CHILD,RANGE,IP6,UDP
      udp6-sendto:<host>:<port> groups=FD,SOCKET,IP6,UDP
      unix-client:<filename>    groups=FD,SOCKET,NAMED,RETRY,UNIX
      unix-connect:<filename>   groups=FD,SOCKET,NAMED,RETRY,UNIX
      unix-listen:<filename>    groups=FD,SOCKET,NAMED,LISTEN,CHILD,RETRY,UNIX
      unix-recv:<filename>      groups=FD,SOCKET,NAMED,RETRY,UNIX
      unix-recvfrom:<filename>  groups=FD,SOCKET,NAMED,CHILD,RETRY,UNIX
      unix-sendto:<filename>    groups=FD,SOCKET,NAMED,RETRY,UNIX


Впечатляет? Сейчас мы попробуем при помощи этой утилиты пробросить порт UART в терминал. Я подключил к системе наш интеллектуальный контроллер реле, который был описан в этой статье. В ней же был описан способ, как определить имя получившегося устройства.



Собственно, конкретно у меня в системе сейчас имеется всего один последовательный порт, но если бы их было несколько, я бы догадался, с каким из них работать, по ID (голубая часть идентификатора). Для простоты, я буду использовать короткое имя (оранжевая часть идентификатора). В общем, имя – /dev/ttyACM0.

Вбиваю вот такое заклинание:

sudo socat $(tty),raw,echo=0 /dev/ttyACM0,raw,echo=0,nonblock 2>/dev/null

Это я запустил программу socat, соединив устройство tty (терминал) с последовательным портом. Работа неблокирующая, данные передаются в сыром виде. Все служебные сообщения уйдут в пустоту.



И тишина. Но зная, что наш контроллер интеллектуального реле реагирует на символ «знак вопроса», я нажимаю его и вижу ответ «abc»:



Нажимая управляющие клавиши для контроллера, я вижу, что вообще-то нахожусь в терминале для последовательного порта, так как все они прекрасно обрабатываются USB-устройством.

Наверняка, кто-то уже хочет задать вопрос, как это я так лихо настроился на работу с портом, не указав его скорость? Ну, просто контроллер интеллектуально реле – это USB-устройство с интерфейсом CDC. Данные из канала USB сразу попадают в память, а в физический интерфейс UART они никогда не поступают. Именно поэтому я эту поделку и взял для проверки. Минимум возможных мест для сбоя! Но вопрос про скорость – верный. Чтобы её задать, перед запуском socat следовало подать команду вида:

/usr/bin/stty -F "$UART_DEV_LINK" $UARTSPEED 2>/dev/null

То есть, совсем честный запуск терминала должен был выглядеть так (на самом деле, можно обойтись и без sudo, как – скоро расскажу):

sudo /usr/bin/stty -F /dev/ttyACM0 115200 2>/dev/null
sudo socat $(tty),raw,echo=0 /dev/ttyACM0,raw,echo=0,nonblock 2>/dev/null

Для проверки я подключу к системе настоящий переходник USB-UART и задам скорость не 115200, а какую-нибудь не самую типовую. Скажем, 38400. Это надо, чтобы исключить случайное совпадение, вдруг мы запросили стандартную скорость, а она уже была выставлена. Сначала я хотел задать совсем нестандартную, но система не дала мне это сделать. В Windows таких проблем не возникало, хочу 12345 – задаю 12345. Линукс так не позволил.

После установки скорости я пошлю свой любимый символ «U» (его код 0x55, то есть, чередующиеся нули и единицы) и измерю фактическую скорость при помощи курсорных измерений осциллографа.

Проверяем





Скорость установлена верно (c поправкой на погрешность переходника). Замечательно. Мы смогли пробросить терминал ОС в последовательный порт. Теперь надо подключиться к системе и начать работать с этим терминалом. Но желательно сделать это с максимальным уровнем автоматизации. Давайте рассмотрим, как это сделали наши разработчики.

2.2 Специально настроенный пользователь


При работе сторонних пользователей с нашим сервером не стоит давать им вводить команды в терминал. И дело даже не в безопасности (хотя, и в ней тоже). Дело в том, что пользователь зашёл на сервис All-Hardware, чтобы поработать с микроконтроллерами, а не для того, чтобы поработать с Линуксом. Своё отношение к работе с ним я уже многократно высказывал. Лично мне эта работа не нравится. 50 на 50, что приходящие на сервис пользователи будут такого же мнения. Скрываем работу с ОС, и вопрос «нравится или нет» исчезает сам собой. Поэтому мы создадим пользователя, при подключении которого к системе, автоматически будет проброшен порт. И пользователь, работая через SSH-терминал, сразу будет общаться именно с этим портом. Никакой подготовительной работы от него не потребуется.

Итак, начинаю цитировать методичку. Создаём аккаунт пользователя с именем, скажем, uart01:

useradd -m -b /var/redd -c " UART account #01" -r -g user -s /var/lib/redd/bin/uart/uart01 uart01

Важный ключик здесь «-s». Он задаёт скрипт, который будет запускаться при входе пользователя. У меня на языке крутится имя autoexec.bat или, скажем, STARTS.COM, если кто-то такое ещё помнит. Но здесь файл будет называться /var/lib/redd/bin/uart/uart01.

В скрипте бесполезно использовать команду sudo – некому вводить пароль. Чтобы созданному пользователю разрешали запускать программы socat, stty и прочие, работающие с портами, без прав администратора, добавляем его в группу dialout, для чего подаём команду:

adduser uart01 dialout

Что мы видим, когда подключаемся к удалённой машине по SSH? А давайте проверим на практике:



Чтобы эта простыня не выдавалась на экран при каждом подключении, надо подать следующую команду, настраивающую свойства пользователя:

touch /var/redd/uart01/.hushlogin; chown uart01 /var/redd/uart01/.hushlogin

Ну что ж. Методичку я с умным видом процитировал. Давайте хоть сам проверю, сработает ли эта последовательность.



Да-с. Фальстарт… Но мы знаем, что не хватает прав – добавляй sudo. Пробуем ещё раз, применяя это знание:



Было предупреждение, что нет того самого autoexec.bat, но мы его сейчас добавим. В остальном –всё чистенько. Как выяснилось в дальнейшем, чего в методичке не хватает, так это пароля для пользователя. Немного погуглив, я выяснил, что его можно назначить так:



2.3 Пишем autoexec.bat


На самом деле, боевой вариант скрипта у нас на сервере просто огромен. Сначала полтора экрана занимает зачистка ресурсов. Она необходима на случай, если предыдущая сессия неожиданно завершилась.

Даже основная работа занимает пол экрана. Там прописан цикл, на случай, если у переходника UART->USB будет отключено питание, ведь чаще всего мы пользуемся портом UART из состава JTAG-адаптера. Обесточили плату – обесточился и адаптер, физически распаянный на ней. Тогда утилита socat вылетит с ошибкой, но бесконечный цикл не даст вылететь сессии, а восстановит соединение.

Дальше большой участок занимает установка скорости UART в зависимости от заранее заданных параметров.

Но для понимания принципа всё это нам не только бесполезно, но ещё и крайне вредно. Мы утонем в куче строк, и рискуем не приметить слона. Поэтому для статьи я сделаю совершенно спартанский вариант скрипта. Он будет состоять из тех двух строк, которые мы вводили выше вручную. Первая задаёт скорость порта (я вбил константу 115200), вторая – делает проброс (опять же, я вбил имя пробрасываемого порта намертво). Ну, и перед ними – чисто технологическая строка.

#!/bin/bash
/usr/bin/stty -F /dev/ttyACM0 115200 2>/dev/null
socat $(tty),raw,echo=0 /dev/ttyACM0,raw,echo=0,nonblock 2>/dev/null

3. Практическая проверка со стороны Linux


3.1 Сбойный прогон


Хорошо, пользователь добавлен, файл autoexec.bat – создан. Давайте попробуем подключиться к системе по SSH от имени этого пользователя:



Ошибка авторизации. Почему? Начальник, знающий Linux в совершенстве, объяснил мне, что ответ на этот вопрос можно найти в файле
/var/log/auth.log

Вот этот ответ:

Dec 24 01:02:30 user-VirtualBox sshd[28239]: User uart01 not allowed because shell /var/lib/redd/bin/uart/uart01 is not executable

Начальник посоветовал мне написать:

sudo chmod +x /var/lib/redd/bin/uart/uart01

но я, будучи в душе мышевозником, решил поправить атрибуты через утилиту mc, для чего навёлся в ней на файл и выбрал пункт меню «Файл->Права Доступа»:



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



3.2 Успешный прогон


Повторяем попытку.



Ошибок нет… Нажимаю «знак вопроса».



Работает! Есть проброс!

Ну всё. Через обычный терминал мы уже можем достучаться. На самом деле, это, конечно, лучше, чем терминал в браузере, но не сильно. Поэтому приступаем к работе из собственной Windows-программы. Ведь цель проброса порта – именно реализовывать сложный обмен своей программы с удалённым контроллером.

4. Работа со стороны Windows на C#


4.1 Почему C#


Вообще, со стороны Windows необходимо и достаточно взять любую библиотеку, реализующую функцию SSH-клиента. Но всё не так просто. У разных библиотек разные лицензии. Как объяснил мне разработчик, принимавший соответствующее решение, библиотека SSH.NET от Renci бесплатна для коммерческих применений. А та же библиотека для C++ уже платная. Именно поэтому за основу в нашей системе был взят .NET вариант. С его разбора и начнём.

4.2 Как добыть и подключить библиотеку


Создаём проект на языке C#. Я в своё время немного работал с этим языком через Windows Forms, поэтому создал именно такой проект. В дереве проектов нажимаем правую кнопку «Мыши» и выбираем пункт меню «Управление пакетами NuGet».



В открывшемся окне выбираем вкладку «Обзор» и вбиваем в поиск слово SSH. У меня нужная библиотека оказалась первой в списке.



У меня проект в решении один, но, когда меня обучали, мне разработчик рассказывал, что он затем выбирал вкладку «Установлено» и отмечал, в какие проекты эту библиотеку добавить. Но это я пересказываю на уровне «слышал звон, да не знаю, где он». Просто вдруг у вас будут проблемы – вы будете знать, как их решать. А для случая первичной установки при одном проекте в решении – всё. У нас всё получилось. Вот она сама добавилась в зависимости моего проекта:



Я добавлю на форму три элемента – кнопку «Открыть соединение», строку для отсылки в UART и текст, принятый из UART.

В программе должна быть переменная-член:

        private Renci.SshNet.SshClient m_client;

4.3 Подключаемся к серверу


В сервисе All-Hardware мы подключаемся к серверу с использованием SSL-сертификата. Но для демонстрации можно использовать и метод попроще, с использованием имени пользователя и пароля. Тогда вся функциональность умещается в две строки плюс немного технологической мишуры:

private void OnOpen(object sender, EventArgs e)
{
  m_client = new Renci.SshNet.SshClient ("192.168.1.103", "uart01", "p");
  try
  {
     m_client.Connect();
  }
  catch (Exception ex)
  {
    MessageBox.Show(ex.ToString(), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
  }
}

4.4 Создание потока данных


Для того чтобы передавать данные, надо обязательно создать поток (не тот поток, который Thread, а который Stream). Для этого вызываем функцию CreateShellStream(). Удивительно, но ей надо передать массу сведений о консольном окне (число строк, число столбцов, размер окна в точках), хотя никакого физического терминала создано не будет. Просто так надо. Ну, и последний параметр функции – размер буфера. Итого, функция открытия соединения разрастается на одну строку и теперь выглядит так:

private void OnOpen(object sender, EventArgs e)
{
  m_client = new Renci.SshNet.SshClient ("192.168.1.103", "uart01", "p");
  try
  {
     m_client.Connect();
     m_shellStream = m_client.CreateShellStream("demo", 80, 25, 640, 480, 1024);
  }
  catch (Exception ex)
  {
     MessageBox.Show(ex.ToString(), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
  }
}

Теперь мы можем передавать данные, пользуясь этим потоком.

4.5 Передача на сервер


Как мы видим, функции библиотеки SSH.NET достаточно простые. Работать с нею – одно удовольствие. Функция передачи не является исключением в целом, но имеет один нюанс в частности. Если просто вызывать её, то данные сначала будут копиться в большом буфере, а затем – оптом улетят из него. Латентность системы окажется весьма высокой. Чтобы этого избежать, необходимо и достаточно, записав минимально необходимый блок (какой именно – это уже виднее прикладному программисту, он же знает, какие данные гоняет через порт), вызвать функцию Flush. В моём тестовом примере получаем (ловлю исключений я не стал добавлять, чтобы не перегружать пример, а так-то она нужна):

        private void OnSendData(object sender, EventArgs e)
        {
            m_shellStream.Write(m_textForSend.Text);
            m_shellStream.Flush();
        }

4.6 Приём с сервера


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

private void Ontimer1(object sender, EventArgs e)
{
    byte[] rcvData = new byte[128];
    Task<int>  task = m_shellStream.ReadAsync(rcvData, 0, rcvData.Length);
    task.Wait();

    int rcvd = task.Result;
    if (rcvd > 0)
    {
       for (int i = 0; i < rcvd; i++)
       {
          m_receivedData.Text += (char)rcvData[i];
       }
    }
 }

4.7 Проверяем


Для проверки открываем соединение, после чего шлём строку из трёх знаков вопроса. Убеждаемся, что пришли три ответа (а каждый ответ у интеллектуального реле выглядит, как строка из символов abc). Значит, система работает.



5. Работа со стороны Windows на прочих языках


Как пробросить данные из .NET библиотеки в обычную программу, я расскажу весьма тезисно. Дело в том, что тема, с одной стороны, для полного раскрытия требует ещё одной большой статьи, а текущую, боюсь, уже все устали читать. Так что если вдруг интересно – можно сделать ещё одну. С другой же стороны, тема весьма хорошо документирована, хоть в MSDN, хоть у того же Джеффри Рихтера.

Итак. Для проброса данных между процессами в Windows предусмотрен механизм именованных каналов (Named Pipes). В WIN32 API это функция CreateNamedPipe() с последующими ReadFile/WriteFile. В .NET можно воспользоваться классом NamedPipeClientStream. Правда, разработчик нашей системы сказал (напомню, в этой статье я – простой корреспондент, всё пишу с чужих слов), что очень важно даже для однонаправленного канала задавать направление PipeDirection.InOut. Почему-то у него в иных случаях ничего не работало.

Собственно, всё. Создаём канал от .NET к обычному коду и канал от обычного кода к .NET, создаём потоки (на этот раз – которые Thread) и начинаем обмениваться данными.

Муторно? Согласен. Но с организационной точки зрения, так было проще всего. Иначе пришлось бы либо покупать библиотеку для работы с SSH, либо писать свою, что вышло бы сложнее. По крайней мере, так мне сказали.

6. Заключение


Мы рассмотрели механизмы, позволяющие пробросить порты UART из ОС Linux в ОС Windows. Именно на таком методе реализован проброс портов и в сервисе All-Hardware, но для полной унификации ещё разработан драйвер виртуального порта. Поэтому все программы, которые работают с COM-портом, смогут работать и с проброшенным портом, как будто он установлен на локальной Windows-машине.

Пользуясь описанными методиками, пользователи смогут подключаться к нашему сервису, не устанавливая драйвера.

А лично автор при подготовке данной статьи почерпнул вещь, никак не связанную с её темой. В скриптах есть запись вида:
anyUtility 2>log.txt

Оказывается, это всё работает даже в Windows (не само перенаправление, а именно перенаправление с двойкой). Теперь автор умеет сохранять перечень ошибок, выдаваемых GNU компилятором, в файл. Перенаправлять поток надо именно так. Как учит народная мудрость: «Век живи – век учись».
Теги:
Хабы:
Всего голосов 7: ↑6 и ↓1+11
Комментарии6

Публикации

Истории

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань