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

IVR меню для Умного Дома, удаленное управление без Internet (на примере MajorDoMo и FreeSWITCH)

Уровень сложностиСредний
Время на прочтение16 мин
Количество просмотров2.3K
Небольшое решение по организации нового способа удаленного контроля и управления и системы домашней автоматизации с помощью SIP телефонии (в частности IVR меню).
IVR (англ. Interactive Voice Response), интерактивное голосовое меню — система предварительно записанных голосовых сообщений, выполняющая функцию маршрутизации звонков внутри АТС с использованием информации, вводимой клиентом на клавиатуре телефона с помощью тонального набора.).
Управление не такое удобное, как управление посредством Web-интерфейса или приложениями, и несколько необычное. Однако это способ управления не зависит ни от работоспособности как отдельных сервисов (облачные решения, VPN/VPS, Telegram и т.д.), так и от наличия вообще доступа к Internet или самого «Умного Дома» или устройства для управления (обычно смартфона). Думаю, этот способ возможно рассматривать в случае необходимости при разного рода пропаданиях доступа к УД, как резервный.
Для решения использовался одноплатный компьютер Raspberry Pi 3b и VoIP GSM шлюз Yestar TG-01. Программная часть: система домашней автоматизации MajorDoMo и SIP сервер FreeSWITCH.

Постановка задачи


Начнем с постановки задачи: требуется, при отсутствии или используемого для удаленного управления сервиса УД или канала к Интернету, обеспечить удаленный контроль и управление работой «Умного дома» наиболее важных устройств.
Алгоритм работы: при отсутствии стандартного сервиса связи (или вообще доступа к Intertnet) для управления или контроля, осуществляем голосовой вызов в наш Умный Дом, через GSM сеть мобильного оператора, далее срабатывает IVR меню с голосовым приветствием и возможностью выбора действий. Упрощенный пример: при нажатии на клавиатуре смартфона цифры 1 – переходим в подменю контроля «Умного Дома», при нажатии цифры 2 — в подменю управления. В меню контроля при нажатии цифры 0 – узнаём состояние всей системы, при нажатии цифры 1 – состояние определенного устройства к примере реле, при нажатии 2 – другого устройства (какого либо датчика) и т.д., при нажатии * — выход на раздел выше.
В меню управления – при нажатии 1 включаем устройство (к примеру реле), при нажатии цифры 0 – выключаем, при нажатии * — выход на раздел выше.

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


Для записи и преобразования текстовой информации в голосовой файл я буду использовать синтезатор речи TTS (в комплекте с MajorDoMo установлен сервис RHVoice). Конфигурационный файл по умолчанию располагается в /usr/local/etc/RHVoice/RHVoice.conf или /etc/RHVoice/RHVoice.conf. Отредактировав файл, можем изменить голос, скорость и т.д.
Создадим директорию для голосовых звуков нашего Умного Дома, в которой будем записывать аудио файлы в формате *.wav:
sudo -u www-data mkdir /var/www/html/smathome_IVR_sounds

Первая задача – получить звуковой файл об общем состояние о системе домашней автоматизации.
По умолчанию в MajorDoMo имеется ряд встроенных сценариев. Посмотреть их можно, перейдя в Панель управления- Объекты – Сценарии.
Откроем сценарий reportStatus.
image
Он проверяет работу всей системы и её компонентов. К примеру, наличие или отсутствие доступа к сети Internet, сетевым устройствам, критичных аварий (авария датчика протечки, газа и прочее), наличие системных устройств и т.д.
Изменим (или создадим новый сценарий, полностью скопировав код существующего) и добавим строчку в самом конце кода, аудиофайл текстовой информации в аудиофайл.
shell_exec ('echo "'.$res.'" | RHVoice-test -p anna -o /var/www/html/smathome_IVR_sounds/IVR_report_status.wav');

Внизу страницы есть небольшая подсказка, как его можно запустить, обратим на это внимание, запуск по ссылке нам понадобится в дальнейшем: 192.168.1.128/objects/?script=reportStatus
Примеры ответа работы этого сценария (в первом варианте – когда всё работает, второй вариант – примеры с аварийными сообщениями от УД):
Все системы работают в штатном режиме.

Системная проблема: Тестовый замок двери не обновляется
Проблема связи: Нет доступа в Интернет.
Внимание! Сигнал тревоги от датчика: Протечка расположенный в комнате Кухня.

Сделаем ещё несколько сценариев, для контроля работы устройств УД назвав их Ivr_state_OpenClose01 и Ivr_state_rele01. В первом сценарии мы будем записывать аудио файл с информацией о состоянии датчика открытия/закрытия, во втором о состоянии управляемого реле.
Пример сценария состояния датчика
open=gg("Openclose01.status");
echo $open;
if ($open==1)
shell_exec ('echo "Тестовый датчик замка двери закрыт" | RHVoice-test -p anna -o /var/www/html/smathome_IVR_sounds/IVR_check_status_openclose01.wav');
else 
shell_exec ('echo "Тестовый датчик замка двери открыт, не забудьте закрыть замок" | RHVoice-test -p anna -o /var/www/html/smathome_IVR_sounds/IVR_check_status_openclose01.wav');


Пример сценария состояния реле
$rele1=gg("Switch1.status");
echo $rele1;
if ($rele1==0)
shell_exec ('echo "Реле выключено" | RHVoice-test -p anna -o /var/www/html/smathome_IVR_sounds/IVR_state_rele01.wav');
else 
shell_exec ('echo " Реле включено " | RHVoice-test -p anna -o /var/www/html/smathome_IVR_sounds/IVR_state_rele01.wav');


Точно так же, как в первом сценарии, при выполнении этих сценариев, создаются файлы IVR_check_status_openclose01.wav и IVR_state_rele01.wav, при последующих запусках сценария – файлы полностью перезаписываются. Запуск любого сценария в MajorDoMo возможен по ссылке.
Управление устройствами в MajorDoMo, тоже реализовано довольно просто: у каждого устройства есть методы и свойства, перейдя в Панель Управления – Объекты — Методы можем увидеть применяемые методы для конкретного устройства. Для удаленного управления наиболее часто используется простое включение / выключение объекта (методы turnOn, turnOff).
image

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

Установка сервера Freeswitch


Наиболее быстрый и простой способ установки – установка из репозитория. Подробная описание есть на сайте FS. Для установки требуется регистрация и Token из личного кабинета на сайте разработчика FreeSWITCH Signalware.
Ссылка на официальную документацию по установке FreeSWITCH на Raspberry Pi
Все действия выполняются от sudo.
Установка FreeSWITCH из репозитория

sudo -i
TOKEN=YOURSIGNALWIRETOKEN

apt-get update && apt-get install -y gnupg2 wget lsb-release apt-transport-https
wget --http-user=signalwire --http-password=$TOKEN -O /usr/share/keyrings/signalwire-freeswitch-repo.gpg https://freeswitch.signalwire.com/repo/deb/rpi/debian-release/signalwire-freeswitch-repo.gpg

echo "machine freeswitch.signalwire.com login signalwire password $TOKEN" > /etc/apt/auth.conf
chmod 600 /etc/apt/auth.conf

echo "deb [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] https://freeswitch.signalwire.com/repo/deb/rpi/debian-release/ `lsb_release -sc` main" > /etc/apt/sources.list.d/freeswitch.list
echo "deb-src [signed-by=/usr/share/keyrings/signalwire-freeswitch-repo.gpg] https://freeswitch.signalwire.com/repo/deb/rpi/debian-release/ `lsb_release -sc` main" >> /etc/apt/sources.list.d/freeswitch.list
apt-get update && apt-get install -y freeswitch-meta-all


Также возможна сборка из исходных кодов (в этом случае Token можно не использовать), но процесс сборки более сложный и занимает несколько длительное время.
*** Внимание! сборка и установка из репозитория – для архитектуры ARM (Raspberry Pi и других одноплатных компьютеров у меня осуществилась успешно на ОС на базе Debian 11. На Debian 10 и 12 — нормального результата мне не удалось добиться.

Пути к файлам при установке из репозитория и сборке из исходных кодов различаются. В этой публикации указаны пути при установке из репозитория.
После установки SIP сервера, требуется незначительная подстройка для работоспособности SIP сервера FreeSWITCH из «коробки». Резервные файлы конфигурации по умолчанию — /usr/share/freeswitch/conf/vanilla в директории conf также хранятся и несколько других конфигураций.
Файлы текущей конфигурации находятся в директории /etc/freeswitch/.
FreeSwitch имеет модульную структуру, подключаемые модули можно посмотреть в файле modules.conf.xml. Отредактируем этот файл, подключаем mod_xml_rpc, включающий поддержку WEB API, удалив символы комментария в этой строке с. Также подключаем голосовой модуль русского языка и отключаем английский.
sudo nano /etc/freeswitch/autoload_configs/modules.conf.xml

<!-- XML Interfaces -->
<load module="mod_xml_rpc"/>  
<!-- Say -->
<!--    <load module="mod_say_en"/> -->
<load module="mod_say_ru"/>

В файле настройки модуля xml_rpc.con.xml укажем свободный порт, на котором будет работать этот модуль. Порт по умолчанию 8080, часто бывает занят разными сервисами (у меня это сервис zigbee2mqtt, установим свободный, к примеру порт 9090, />, так же можно сменить пароль и пользователя по умолчанию ( указан freeswitch, works).
sudo nano /etc/freeswitch/autoload_configs/xml_rpc.conf.xml

Отредактируем файл c глобальными переменными SIP сервера: vars.xml
sudo nano /etc/freeswitch/vars.xml

Изменим пароль по умолчанию с 1234, к примеру на 1111 (устраняем задержку в 10 секунд при наборе номера), укажем правильную директорию для голосовых файлов, изменим профиль с external на internal, так же поменяем страну и международный код установленные по умолчанию. Приведём строки к следующему виду:
Изменения в файле профиля vars.xml
<X-PRE-PROCESS cmd="set" data="default_password=1111"/>
<X-PRE-PROCESS cmd="set" data="sound_prefix=$${sounds_dir}/ru/RU/vika"/>
<X-PRE-PROCESS cmd="set" data="use_profile=internal"/>
<X-PRE-PROCESS cmd="set" data="default_areacode=375"/>
<X-PRE-PROCESS cmd="set" data="default_country=BY"/>


Перейдём к файлу sip профилей. Я буду использовать профиль internal (внутренний). В секции gateways добавим возможность чтения *.xml файлов из директории internal (которую нужно будет потом создать) и выставим параметры ext-rtp-ip и ext-sip-ip. Заменим значение NAT сервера или на IP адрес нашего SIP сервера FreeSWITCH в локальной сети или используем функцию auto-nat. Откорректируем следующие строки:
sudo nano /etc/freeswitch/sip_profiles/internal.xml
Изменения в файле профиля internal.xml

<gateways>
<X-PRE-PROCESS cmd="include" data="internal/*.xml"/>
  </gateways>
...
   <param name="ext-rtp-ip" value="auto-nat"/>
    <param name="ext-sip-ip" value="auto-nat"/>


Создаём директорию для SIP профилей:
sudo -u freeswitch mkdir /etc/freeswitch/sip_profiles/internal 

Ещё исправим одну строку с указанием неправильной директории звуковых файлов (которые находятся /usr/share/freeswitch/lang/ru/ru.xml) в файле языковых настроек: приводя строку к виду:
sudo nano /etc/freeswitch/lang/ru/ru.xml  


<language name="ru" sound-prefix="$${sounds_dir}/ru/RU/vika" tts-engine="cepstral" tts-voice="elena">

Сейчас основные файлы конфигурации отредактированы, SIP сервер сейчас готов к работе в локальной сети, после перезапуска сервиса freeswitch они применятся.
 sudo systemctl restart freeswitch.service 

Можно подключить SIP клиент (На ПК я обычно использую MicroSIP или PhonerLite, на смартфоне Linphone и MizuDroid) и произвести тестовые наборы основных функций АТС в локальной сети.
Выполнив тестовый набор на номер 5000 услышим IVR меню по умолчанию. «Добро пожаловать в Freeswitch будущее голосовой телефонии….»

Настройка IVR меню для Умного Дома


Сделаем своё голосовое меню IVR: по набору номера 5002 должны попасть в диалог со своим УД.
Создадим звуковые файлы для нашего IVR меню. Вариантов действий довольно много, можно использовать стандартные голосовые файлы FreesWitch, расположенные в директории /usr/share/freeswitch/lang/ru/ и из них собирать фразы (phrases), можно найти необходимые в Интернет, можно записать с помощью микрофона. Опять используем синтезатор речи TTS RHVoice, и по аналогии создания звуковых файлов в системе домашней автоматизации, создадим файлы для IVR меню.
Создадим директорию в домашней папке и запишем фразы в звуковые файлы:
Создание звуковых файлов для IVR меню
mkdir /home/pi/my_smarthome_ivr

echo "Привет. Что будем делать. Чтоб войти в меню контроля нажмите цифру один. Чтоб войти в меню управления нажмите цифру два. " | RHVoice-test -p anna -o /home/pi/my_smarthome_ivr/privet.wav


echo "Вы в меню контроля. Узнать общее состояние системы нажмите ноль. Узнать состояние реле нажмите цифру один. Узнать состояние датчика замка нажмите два" | RHVoice-test -p anna -o /home/pi/my_smarthome_ivr/control.wav


echo "Вы в меню управления. Включить реле – нажмите цифру один. Выключить реле нажмите цифру ноль " | RHVoice-test -p anna -o /home/pi/my_smarthome_ivr/management.wav


echo "Чтоб войти в меню управления нажмите цифру два " | RHVoice-test -p anna -o /home/pi/my_smarthome_ivr/dva.wav


Перейдём к диалплану (номерной план, dialplan) — формальное описание схемы маршрутизации и обработки телефонных звонков. Номерной план подробно описывает, что система должна делать со входящими и исходящими звонками: передавать их дальше, сохранять, отвечать на них самостоятельно и так далее. Основные настройки по умолчанию, находятся в общем файле default.xml однако будем для каждого направления создавать свой отдельный файл в директории /etc/freeswitch/dialplan/default/
В самом конце файла default.xml добавим строчку
<X-PRE-PROCESS cmd="include" data="default/*.xml"/>

Создадим следующий диалплан (назначим каждому номеру своё действие):
5002 – вход в общее IVR меню
Для контроля: 5010 – получение общего статуса системы, 5011 – получение статуса датчика, 5012 – получение статуса реле.
Для управления: выключить реле – 5020 включить — 5021.
Для номера 5002 создадим файл, который переадресовывает абонента для входа в главное IVR меню (запускает приложение IVR с названием my_smarthome_ivr).
Направление 5002 - запуск главного IVR меню
sudo -u freeswitch nano /etc/freeswitch/dialplan/default/my_smarthome_ivr.xml

 <extension name="my_smarthome_ivr">
      <condition field="destination_number" expression="^5002$">
        <action application="answer"/>
        <action application="sleep" data="2000"/>
        <action application="ivr" data="my_smarthome_ivr"/>
      </condition>
    </extension>


Создаём следующий xml файл для направления 5010 (получение общего статуса системы домашней автоматизации):
Направление 5010 - получение статуса системы
sudo -u freeswitch nano /etc/freeswitch/dialplan/default/ivr_report_status.xml

<extension name=" ivr_report_status.">
      <condition field="destination_number" expression="^5010$">
<action application="system" data="curl  http://192.168.1.128/objects/?script=reportStatus"/>
<action application="sleep" data="5000"/>
        <action application="answer"/>
        <action application="sleep" data="2000"/>
<action application="playback" data="/var/www/html/smathome_IVR_sounds/IVR_report_status.wav"/>
<action application="sleep" data="2000"/>
<action application="transfer" data="5002"/>
<!--        <action application="hangup"/> -->
      </condition>
    </extension>


Алгоритм следующий: при наборе номера 5010 отрабатывает скрипт, который создали в системе домашней автоматизации, для получения статуса системы, ждём его выполнения и формирования звукового файла, проигрывание статуса системы и возврат в главное IVR меню (переадресация на номер 5002). Закомментирована строка с действием отбоя (на случай если нам не нужно возвращаться в главное меню).
Таким же образом создадим и другие действия, как информирование о статусе элементов системы, так и управления устройствами. Для примеру приведу получение текущего состояния устройства и управление реле на выключение:
Примеры файлов диалплана для направлений 5012 (статус реле), 5020 (выключение реле)
sudo -u freeswitch nano /etc/freeswitch/dialplan/default/IVR_state_rele01.xml

<extension name="IVR_state_rele01">
<condition field="destination_number" expression="^5012$">
<action application="system" data="curl 'http://192.168.1.128/objects/?script=IVR_state_rele01'"/>
        <action application="sleep" data="2000"/>
<action application="playback" data="/var/www/html/smathome_IVR_sounds/IVR_state_rele01.wav"/>
<action application="sleep" data="2000"/>
<action application="transfer" data="5002"/>
<!--        <action application="hangup"/> -->
      </condition>


sudo -u freeswitch nano /etc/freeswitch/dialplan/default/ivr_rele01_turnOff


<<extension name="ivr_rele01_turnOff">
      <condition field="destination_number" expression="^5020$">
<action application="system" data="curl  'http://192.168.1.128/objects/?object=Switch1&op=m&m=turnOff'"/>
<action application="sleep" data="2000"/>
        <action application="answer"/>
<action application="system" data="curl 'http://192.168.1.128/objects/?script=IVR_state_rele01'"/>
        <action application="sleep" data="2000"/>
<action application="playback" data="/var/www/html/smathome_IVR_sounds/IVR_state_rele01.wav"/>
<action application="sleep" data="2000"/>
<action application="transfer" data="5002"/>
<!--        <action application="hangup"/> -->
      </condition>
    </extension>


Перейдём сейчас к созданию главного IVR меню для Умного Дома.
Уже в директории для файлов IVR меню /etc/freeswitch/ivr_menus/ создаём файл с содержимым описывающим главное IVR меню:
Main IVR Menu
sudo nano /etc/freeswitch/ivr_menus/my_smarthome_ivr.xml


<include>
  <!-- my_smarthome_ivr setup -->
  <!-- my_smarthome_ivr, Main Menu -->
  <menu name="my_smarthome_ivr"
greet-long="/home/pi/my_smarthome_ivr/privet.wav"
greet-short=="/home/pi/my_smarthome_ivr/privet.wav"
      invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav"
      exit-sound="voicemail/vm-goodbye.wav"
      confirm-macro=""
      confirm-key=""
      tts-engine="flite"
      tts-voice="rms"
      confirm-attempts="3"
      timeout="10000"
      inter-digit-timeout="2000"
      max-failures="3"
      max-timeouts="3"
      digit-len="4">

    <!-- The following are the definitions for the digits the user dials -->
    <!-- Digit 1 transfer caller to menu control -->
    <entry action="menu-sub" digits="1" param="control_ivr_submenu"/>
    <entry action="menu-sub" digits="2" param="management_ivr_submenu"/>
    <entry action="menu-top" digits="9"/>          <!-- Repeat this menu -->
  </menu>

<!-- Control IVR Sub Menu -->
  <menu name="control_ivr_submenu"
      greet-long="/home/pi/my_smarthome_ivr/control.wav"
      greet-short="/home/pi/my_smarthome_ivr/control.wav"
      invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav"
      exit-sound="voicemail/vm-goodbye.wav"
      timeout="15000"
      max-failures="3"
      max-timeouts="3">

<entry action="menu-exec-app" digits="0" param="transfer 5010 XML default"/>    <!-- System status MajorDoMo -->
    <entry action="menu-exec-app" digits="1" param="transfer 5011 XML default"/>    <!-- status OpenClose01 -->
    <entry action="menu-exec-app" digits="2" param="transfer 5012 XML default"/>    <!-- status Switch_Rele -->
    <entry action="menu-top" digits="9"/>          <!-- Repeat this menu -->
    <entry action="menu-top" digits="*"/>           <!-- Return main menu -->
</menu>

<!-- management IVR Sub Menu -->
  <menu name="management_ivr_submenu"
      greet-long="/home/pi/my_smarthome_ivr/management.wav"
      greet-short="/home/pi/my_smarthome_ivr/management.wav"
      invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav"
      exit-sound="voicemail/vm-goodbye.wav"
      timeout="15000"
      max-failures="3"
      max-timeouts="3">

    <entry action="menu-exec-app" digits="0" param="transfer 5020 XML default"/>    <!--  Rele1 TurnOff -->
    <entry action="menu-exec-app" digits="1" param="transfer 5021 XML default"/>    <!-- Rele1 TurnOff -->
    <entry action="menu-top" digits="9"/>          <!-- Repeat this menu -->
    <entry action="menu-top" digits="*"/>           <!-- Return main menu -->

  </menu>
</include>


В этом файле описаны действия для главного (Main menu IVR) и двух подменю контроля и управления.

Подключение FreeSWITCH и MajorDoMo к мобильной сети


Для стыковки локальной сети Умного Дома с мобильной сетью, я использовал VoIP GSM шлюз Yestar TG-01 (предназначенный для перевода голосового трафика между сетями традиционной телефонии и сетями передачи данных).
По умолчанию на шлюзе имеется 2 Sip аккаунта. Для примера буду использовать один из них с именем 20001 и паролем на подключение pincode20001, никаких настроек менять (кроме назначения IP адреса) в нём не пришлось.
Первым делом подкорректируем настройки ACL SIP сервера Freeswitch. Разрешим подключение не только IP телефонов, но и других устройств. Добавляем в секции gateway разрешение на подключение устройств (шлюзов) из своей локальной сети, по умолчанию подключение запрещено, сделано исключение для своей домашней сети:
<list name="domains" default="deny">
 <node type="allow" cidr="192.168.1.0/24"/>

Создадим файл, который отвечают за входящую GSM шлюза с freeswitch сервером. В файле from_GSM.xml все входящие звонки, поступающие от шлюза запускают новое IVR меню.
sudo -u freeswitch nano /etc/freeswitch/dialplan/default/from_GSM.xml
содержимое файла диалплана from_GSM.xml
<include>
<extension name="call from GSM">
<condition field="destination_number" expression="^20001$">
 <action application="answer"/>
 <action application="sleep" data="800"/>
<action application="set" data="transfer_ringback=$${ru-ring}"/>
 <action application="ivr" data="from_GSM_ivr"/>
    </condition>
</extension>
</include>


Далее создаём файл IVR меню, специально для внешних (с телефонной сети общего пользования вызовов ТСОП) вызовов.
Содержимое IVR меню для ТСОП
<include>
  <!-- from_GSM_ivr.xml -->
  <!-- from_GSM_ivr, Main Menu -->
  <menu name="from_GSM_ivr"
greet-long="ivr/ivr-please_state_your_name_and_reason_for_calling.wav"
greet-short=="ivr/ivr-please_state_your_name_and_reason_for_calling.wav"
      invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav"
      exit-sound="voicemail/vm-goodbye.wav"
      confirm-macro=""
      confirm-key=""
      tts-engine="flite"
      tts-voice="rms"
      confirm-attempts="3"
      timeout="10000"
      inter-digit-timeout="2000"
      max-failures="3"
      max-timeouts="3"
      digit-len="4">

  <entry action="menu-exec-app" digits="/^(1234)$/" param="transfer 5002 XML default"/>
 <!-- Check PIN and transfer to main IVR menu -->
</menu>
</include>


В этом IVR есть всего одна строка действия — требуется ввод символов 1234 для входа в основное меню. Я установил небольшую защиту (заглушку) от доступа посторонних. Попав на мобильный номер GSM шлюза слышим «приветствие» — «Пожалуйста назовите ваше имя и цель вашего звонка». Файл в этом случае, я брал из стандартных звуковых файлов FreeSWITCH. Идёт 3 раза повтор этого сообщения и после отбой. Также даётся 3 попытки ввода символов.
Перегружаем сервис freeswitch:
 sudo systemctl restart freeswitch.service

Пробуем позвонить на номер GSM шлюза, вначале запускается IVR меню на вход (проверка PIN), при правильном наборе попадаем в главное IVR и далее можем управлять и контролировать устройствами своего «Умного Дома».

На этом все, большое спасибо что прочитали, если у вас есть вопросы и замечания буду рад ответить в комментариях.
В качестве основного средства управлением УД, этот способ неудобен, а вот в качестве резервного (особенно в текущих реалиях) вполне может кому-нибудь пригодится.
Теги:
Хабы:
Всего голосов 2: ↑2 и ↓0+4
Комментарии2

Публикации

Истории

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

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