Русский
Русский
English
Статистика
Реклама

Diy или сделай сам

Сверхточный Raspberry PI Stratum 1 NTP сервер

07.07.2020 12:09:02 | Автор: admin
В этой статье я расскажу, как собрать Stratum 1 NTP сервер на Raspberry PI для синхронизации времени за скромную сумму и навсегда забыть о проблемах, связанных с не совпадающим временем на всех ваших устройствах. А самое главное, он будет давать результат на два порядка точнее, чем обычный сервер.

В предыдущей статье, посвященной синхронизации времени по радио и СРНС (системы радионавигационной связи), я не успел рассказать про выбор приёмника GPS / ГЛОНАСС с выходом PPS. Между тем от этого зависит точность приёма сигнала, величина может составить от одной миллисекунды до нескольких микросекунд и зачастую это имеет решающее значение.

Для самого точного приема сигнала времени нужен приёмник GPS / ГЛОНАСС с выходом PPS. Дело однако в том, что на российском рынке не просто раздобыть устройство с такими характеристиками по доступной цене. Много таких моделей давно уже перестали выпускать, а в заброшенных интернет магазинах с версткой 1990-х остались лишь их описания с предложением подписаться на уведомление при поступлении товара.


Полный список протестированного GPS оборудования можно найти на GitLab ресурсе NTPSec. Не трудно заметить, что незначительное число представленных в списке устройств имеют отметку 3-4 звезды и опцию PPS. Таким образом, в шорт-лист попадают следующие приёмники.

  • Garmin GPS-18, не USB *** (приблизительная цена 10 тыс. р.)
  • GlobalSat MR-350P ****
  • Jackson Labs FireFly-II ***
  • Magellan Thales AC12 ***
  • Motorola Oncore GT+ ***
  • Navisys GR601-W ****
  • SkyTraq SKG16B ****
  • Trimble Lassen IQ ***
  • u-blox ANTARIS LEA-4T ***
  • u-blox EVK 6H ****
  • u-blox LEA SQ ****

4* Отличная производительность: gpsd распознает приёмник быстро и надежно, а отчеты сформировано полностью и правильно.

3* Хорошая производительность: gpsd с незначительными проблемами или задержкой распознаёт устройства, но отчеты сформировано полностью и правильно.

Если вас не пугает цена этих моделей, а также нет большого желания возиться с железками, можете не читать дальше. Приемник, подключенный к серверу по USB, или RS232 интерфейсу обеспечит гораздо большую точность определения времени, чем NTP сервер, работающий по tcp/ip. Но если путь самурая вам не чужд, тогда давайте собирать свой Raspberry PI NTP сервер с GPS синхронизацией времени.

Собираем Raspberry PI


Итак: берем следующие компоненты для нашего микро сервера.

  1. Плата Raspberry Pi 4 Model B, 4 GiB ОЗУ (6200 руб.);
  2. Корпус, например такой (890 руб.);
  3. Micro SD карта на 32 GiB, можно и 16 GiB; (540 руб.)
  4. GPS модуль на чипе u-blox NEO-M8 (1700 руб. с антенной);
  5. GPS антенна на 15 dB;
  6. Паяльник.

Вообще-то, u-blox NEO-M8 оснащен UART интерфейсом, но для PPS выхода необходимо припаять pin-3 на GPS модуле к соответствующему GPIO коннектору на плате Raspberri Pi. Модуль швейцарской компании завоевал популярность у специалистов и это не случайно, характеристики говорят сами за себя.

  • Поддерживаемые СРНС: BeiDou, Galileo, GNSS; GPS/QZSS, GLONASS;
  • Напряжение питания: 2.7...3.6 В;
  • Интерфейсы: UART, USB, SPI, DDC, I2C;
  • Поддерживаемые протоколы: NMEA 0.183 version 4.0, UBX (binary), RTCM 2.3;
  • Чувствительность при обнаружении: -167 дБм;
  • Чувствительность при слежении: -160 дБм;
  • Время холодного старта: 26 с;
  • Время горячего старта: 1.5 с;
  • Потребляемая мощность: 35 мВт;
  • Рабочая температура: -40...+85 С;
  • Размеры: 16х12.2х2.4 мм

В такой конфигурации с новейшим оборудованием примерная общая цена Raspberry PI в собранном виде составит 9330 руб. Можно сэкономить, купив Raspberry PI 3, или четверку с 2 GiB ОЗУ. Можно еще сэкономить на GPS чипе, u-blox NEO-6M с антенной стоит около 650 руб. Тогда цена NTP сервера упадет до 5500 руб.


GPS/Глонасс модуль UBLOX NEO 8M

Может возникнуть вопрос, для чего нужны все эти капиталовложения и какую точность обеспечивает тот, или иной способ синхронизации времени. Небольшая сводная табличка для справки.
Источник сигнала времени Погрешность
GPS с атомными часами 50 nSec
KPPS 1 Sec
PPS 5 Sec
Интерфейс USB 1.1 1 mSec
Интерфейс USB 2.0 100 Sec (100000 nSec)
NTP по сети ~30 mSec

Kernel PPS (KPPS) отличается от PPS тем, что использует функцию ядра Linux / Unix для точной временной отметки изменения состояния в строке PPS. Обычный же PPS реализован в user-space. Если ядро Linux поддерживает KPPS через API RFC 2783, gpsd воспользуется им для увеличения точности.

Во многих дистрибутивах Linux имеется пакет pps-tools, который обеспечивает поддержку KPPS и устанавливает timepps.h заголовочный файл. Обязательно установите этот пакет.

(1:1146)$ sudo emerge -av pps-toolsLocal copy of remote index is up-to-date and will be used.These are the packages that would be merged, in order:Calculating dependencies... done![binary   R    ] net-misc/pps-tools-0.0.20120407::gentoo  0 KiBTotal: 1 package (1 reinstall, 1 binary), Size of downloads: 0 KiBWould you like to merge these packages? [Yes/No] 

Таким образом, подключив GPS приёмник с PPS выходом по USB мы получаем 300-кратное повышение точности синхронизации времени. Чтение с чипа GPS на плате в режиме KPPS даёт прирост точности еще на два порядка.

Будем откровенны не все из нас умеют пользоваться паяльником, далеко не каждый пользуется этим инструментом регулярно. В таком случае имеет смысл поискать модуль GPS, не требующий пайки контактов.


Raspberry Pi GPS/RTC Expansion Board

Таких плат мало, и стоят они дорого. Разницу в цене вполне окупит приобретение паяльника и трудозатраты.

Установка ОС


Существует Raspberry PI OS, а. k. a. Raspbian, можно просто пойти по ссылке, скачать свежую версию и установить её. Многие так и делают, но давайте вспомним, что Raspberry PI 4 поддерживает 64-битную операционную систему, в то время как Raspberry PI OS пока имеет лишь 32-битные модификации Debian Linux для архитектуры Arm.

Существует такая точка зрения, что на 64-битная ОС неоправдана на Raspberry PI 4, так как нет возможности обеспечить прирост производительности из-за особенностей архитектуры и сборки. Мне эта точка зрения представляется сомнительной, об этом уже писали на Хабре 64-битная ОС быстрее.

Существует порт Debian Linux для архитектуры arm64, однако дистрибутив Убунту для Raspberry PI имеет внятную страницу и инструкцию. На странице находим дополнительное подтверждение тому, что лучше выбрать 64-битную ОС.



Инвентарь для установки:

  • Raspberry Pi 4;
  • USB-C кабель питания для Pi 4;
  • Micro SD карта с установочным образом Убунту;
  • Монитор с выходом HDMI;
  • Кабель MicroHDMI;
  • USB клавиатура.

Следующий этап запись установочного образа на Micro SD карту. Если у вас под рукой нет специализированной утилиты и программы под эти цели, в простейшем случае достаточно.

sudo dd if=/path/to/ubuntu-core-arm64.iso of=/dev/mmcblk0 status=progress

Точное название устройства видно в выводе dmesg при обнаружении нового устройства.

PM: Adding info for No Bus:179:0device: 'mmcblk0': device_addPM: Adding info for No Bus:mmcblk0

Вставив Micro SD карту, подключив HDMI-монитор, USB-клавиатуру, и кабель питания загружаетесь в Ubuntu Server на Raspberry Pi. Имя пользователя и пароль по умолчанию ubuntu.

Настройка NTP сервера


  1. Если Raspberry PI включен в консольном режиме (headless), то для начала необходимо определить IP адрес устройства. С рабочей станции наберите следующую команду.

    (1:1151)$ arp -na | grep -i "dc:a6:32"
    

    Ели же Pi подключен к HDMI монитору и USB клавиатуре, пропустите шаги 1-2 и переходите сразу к установке пакетов.
  2. Подключитесь по ssh

    (1:1152)$ ssh ubuntu@<Raspberry Pis IP address>
    
  3. Установите необходимые пакеты.

    user@server ~$ sudo apt-get install aptitudeuser@server ~$ sudo aptitude install wpasupplicant gpsd chrony
    
  4. Настройте Wi-Fi соединение с помощью wpasupplicant.
  5. В Linux UART0 интерфейс Pi представлен файлом устройства /dev/ttyAMA0. Для того чтобы освободить UART0 интерфейс для GPS приёмника нужно поменять параметры загрузки ядра Linux. Необходимо отключить console=ttyAMA0,115200, заменив на console=tty1. Для этого в файле /etc/default/grub надо поменять GRUB_CMDLINE_LINUX_DEFAULT. Если существует файл, /boot/config.txt, в нем также можно задать те же опции.

    Raspberry Pi 4 имеет 6 UART-ов
    Название Тип Устройство Назначение
    UART0 PLO11 /dev/ttyAMA0 вторичный (Bluetooth)
    UART1 mini UART /dev/ttyS0 основной
    UART2 PLO11
    UART3 PLO11
    UART4 PLO11
    UART4 PLO11
    По умолчанию UART2-5 выключены.

    Как видно из названия, UART0 полноценный серийный порт и он имеет более высокую производительность, чем обрезанный UART1, он же mini UART. Поэтому будет не лишним перевести Bluetooth на UART1 с тем, чтобы основной поток данных шел через UART0. Для этого в /etc/default/grub, или /boot/config.txt ставим enable_uart=1.
  6. В файле /etc/defaults/gpsd следует выставить.

    DEVICES="/dev/ttyAMA0 /dev/pps0"GPSD_OPTIONS="-n"USBAUTO="false"
    
  7. Запустите, или перезапустите gpsd.

    user@server ~$ sudo /etc/init.d/gpsd startuser@server ~$ sudo /etc/init.d/gpsd restart
    
  8. Проверка работы модуля GPS.

    user@server ~$ cat /dev/ttyAMA0user@server ~$ cgps -suser@server ~$ ppstest /dev/pps0
    
  9. Отредактируем файл /etc/ntp.conf.

    Все строки, содержащие сетевые публичные Stratum 1, 2 NTP сервера (такие, как pool [0-9].subdomain.pool.ntp.org) следует закомментировать, чтобы использовать лишь GPS/PPS источники данных.

    # GPS Serial data reference (NTP0)server 127.127.28.0 minpoll 4fudge 127.127.28.0 flag1 1 time1 0.9999 refid GPS #flag1 - PPS on
    

    # GPS PPS reference (NTP1)server 127.127.22.0 minpoll 4fudge 127.127.22.0 flag3 1 refid PPS #flag3 - enable KPPS API
    

    Верхняя запись NTP0 указывает на универсальный источник времени, доступный почти на всех устройствах GPS. Нижняя запись NTP1 определяет гораздо более точный PPS источник.
  10. Перезапустите ntpd

    user@server ~$ sudo /etc/init.d/ntpd restart
    

Использованные материалы



Подробнее..

Голосовой помощник для совершения операций на бирже

02.07.2020 18:23:23 | Автор: admin
Алиса, купи одну акцию Яндекс.
Заявка на покупку Яндекс по рыночной цене, тикер: YNDX, количество акций: 1, для подтверждения скажите подтверждаю, для отмены скажите нет.
Подтверждаю.
Заявка исполнена.


Я делаю голосовой интерфейс для совершения операций с ценными бумагами на Московской бирже с открытым исходным кодом. Идею вынашивал с детства. Помню, впервые увидел голосовой помощник в каком-то фильме около двадцати лет назад, в то время у меня даже проводного телефона не было в доме. А сейчас у меня безлимитное подключение к сети Интернет, брокерский счёт, которым я могу управлять через смартфон. За двадцать лет технологии стали доступнее.


Как всё начиналось



Полтора года назад у меня в доме появилась Яндекс.Станция. Я стал замечать, что иногда, лёжа в кровати, лень протянуть руку за смартфоном, чтобы почитать новости, и тогда я просил Алису рассказать, как изменился мир, пока я спал. С первых дней я оценил удобство голосовых помощников, и ожидал, что миллионы разработчиков накинуться на голосовые интерфейсы точно так же, как в своё время накинулись на разработку мобильных приложений на все случаи жизни.

Время шло, в каталоге навыков Алисы появлялись бесполезные банковские голосовые помощники (не в обиду разработчикам). У Сбербанка, например, помощник озвучивал условия кредита и предлагал прийти в отделение, у Тинькофф тоже самое, только вместо отделения предлагал перейти на сайт, чтобы заполнить заявку. Ребята, кто разрабатывал это, пожалуйста, не обижайтесь на меня, но, правда, я не хочу никуда идти, ни в отделение, ни на сайт, я хочу иметь возможность перевести 100 рублей другу фразой: Алиса, отправь 100 рублей Саше.

Идея лежит на поверхности, но по какой-то причине, никто ещё это не реализовал. Почему? Может никто не пользуется голосовыми помощниками? Может есть законодательные ограничения? Может есть проблемы с безопасностью? Что-то ещё? Поделитесь, пожалуйста, в комментариях, если знаете.

В какой-то момент, я просто встал с кровати и начал делать приватный навык для Алисы, чтобы голосом управлять своим брокерским счётом. Я перебрал своих брокеров и остановился на Тинькофф Инвестиции OpenAPI. Затем на месяц погрузился в изучение возможностей платформы Яндекс.Диалоги, и ещё через некоторое время купил первые ценные бумаги через голосовой помощник на Московской бирже один лот Банк ВТБ. Надеюсь, эта сделка войдёт в историю.

С самого начала я решил, что не буду прятать исходный код, чтобы любой желающий мог настроить себе голосовой помощник: https://github.com/denismosolov/oliver

Возьми с полки пирожок и расскажи наконец о проблемах



Компаний много, а я один



Когда я говорю Алисе: Купи одну акцию Яндекс, то платформа Яндекс.Диалоги извлекает название ценной бумаги из фразы и преобразовывает в специальный идентификатор FIGI (Financial Instrument Global Identifier), необходимый для взаимодействия с торговой платформой через OpenAPI. Вот так выглядит FIGI для акций компании Яндекс, торгующихся на Московской бирже: BBG006L8G4H1.

Преобразование в FIGI происходит только для тех названий компаний, которые я описал в разделе Сущности в Диалогах. Подробнее об этом, можно почитать в документации. Для примера, вот фрагмент кода сущности:

entity EFigi:    values:        BBG005DXJS36:            %exact            TCS            %lemma            тиньков(банк)?            тинькоф(банк)?            тинькофф(банк)?            ти си эс (груп)?


У этой работы большая трудоёмкость, нужно описать все компании, торгующиеся на Московской и Санкт-Петербургской биржах, но результат может быть полезен трейдерам, инвесторам, разработчикам голосовых помощников и даже компаниям, чьи акции торгуются на биржах. Вот файл с описанием сущности EFigi на GitHub, я потихоньку его дополняю.

Люди называют одни и те же компании по-разному, например, кто-то скажет Сбер, а кто-то Сбербанк. На бирже торгуются обычные акции Сбербанка и привилегированные (префы). Хочется учесть все популярные варианты.

Компания торгуется на двух биржах в разной валюте



Глобальные депозитарные расписки TCS Group можно купить на Московской бирже за рубли, а можно на Санкт-Петербургской за доллары. По хорошему, голосовой помощник должен уточнять, в какой валюте покупать расписки, но иногда, это можно понять из контекста.

Например, когда я говорю: Продай одну акцию TCS Group, и у меня на счёте есть только акции в рублях, то нужно без уточнений продавать в рублях на Московской бирже. Если у меня на счёте акции TCS Group в рублях и долларах, то Алиса должна задать уточняющий вопрос: У вас глобальные депозитарные расписки TCS Group в рублях и в долларах, в какой валюте вы хотите продать?.

Я пока это не реализовал и TCS Group всегда покупается и продаётся за доллары. Но, надеюсь, скоро возьмусь за выставление заявок на покупку и продажу ценных бумаг по заданной цене (лимитные заявки), а там уже научу голосовой помощник разливать акции на разных биржах по валюте, которая будет в цене покупки или продажи.

Ошибки распознавания



Люди спрашивают, а что будет, если Алиса распознает что-то не так, например, купит не ту бумагу или не то количество? Для этого я предусмотрел подтверждение сделок. После того, как Алиса распознает команду на покупку или продажу, она проговаривает детали сделки и ожидает подтверждения. Если подтверждения не последует, то сделка не состоится.

Сообщение при подтверждении сделки звучит так: Заявка на <покупку | продажу> <$название_ценной_бумаги> по <$цена_за_одну_бумагу>, тикер: <$тикер>, количество акций: <$количество>, для подтверждения скажите подтверждаю, для отмены скажите нет.

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

Я хотел написать функцию, которая бы разбирала тикеры по буквам, и для каждой буквы проигрывала звук из озвучки английского алфавита. Код принимал бы на вход строку YNDX, а возвращал вот такую строку в формате tts:
<speaker audio="sounds-y.opus"><speaker audio="sounds-n.opus"><speaker audio="sounds-d.opus"><speaker audio="sounds-x.opus">

Алиса будет проигрывать звуки, и в теории всё будет звучать хорошо.

Безопасность при совершении сделок



Чтобы использовать OpenAPI, приходится отключать подтверждение сделок по СМС в Тинькофф Инвестиции, а это создаёт риски для безопасности. Например, инвестор ушёл на прогулку с собакой, а дети решили поиграться с Алисой и продали все акции.

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

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

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

Вместо заключения


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

До встречи в будущем!
Подробнее..

Перевод Как делали набор для сборки классического калькулятора, воссозданного с нуля

18.06.2020 10:17:09 | Автор: admin

Реплика калькулятора Sinclair Scientific демонстрирует, как заставить дешёвый чип творить чудеса




Был ли калькулятор Sinclair Scientific элегантным? Он определённо стал хитом, и после выхода в 1974 украсил обложки журналов к примеру, Popular Mechanics. Хитроумная прошивка заставляла его весьма ограниченный процессор, способный лишь на базовые арифметические действия, выходить за пределы своих возможностей. Это позволило Sinclair продавать научный калькулятор тысячам людей, которые в ином случае просто не смогли бы себе его позволить. Он работал медленно, иногда выдавал ошибки, и его набора функций едва хватало, чтобы он считался научным калькулятором; кроме того, непосвящённым пользователям было трудно его использовать.

Я слышал о нём краем уха благодаря тому, что он стал вехой на пути к зарождению британской индустрии микрокомпьютеров и такой обожаемой многими машине, как Sinclair ZX Spectrum. И когда я увидел реплику калькулятора в магазине Tindie от Криса Чанга, внутри у меня что-то щёлкнуло. Потом я прочёл описание работы оригинального калькулятора (научная запись, и никакой клавиши равно?), и как реплика воспроизводит его поведение. Там используется эмулятор с прошивкой, которую получили реверс-инжинирингом путём визуального изучения металла оригинального процессора. И мне, конечно, захотелось попробовать такую штуку.




Реплика после рефакторинга. Кстати, оригинальный Sinclair Scientific тоже продавали в виде модели для сборки. Версия Криса Чанга меньше, использует меньше компонентов, и может воспроизводить поведение сразу двух калькуляторов на чипах семейства TMS080x: TI Datamath, простого арифметического калькулятора, и Sinclair Scientific. Поэтому трафарет на плате подходит под оба калькулятора.

Сначала рассмотрим железо. Набор представляет собой один из нескольких вариантов реплики Sinclair Scientific, однако это один из наиболее простых наборов. Всего один чип и плата размером с кредитку в комплекте с небольшим количеством компонентов. Чанг предлагает два варианта: оригинальный, разработанный в 2014 году и выложенный на Tindie в конце 2019, показывает цифры при помощи выпуклых светодиодных модулей QDSP-6064. Они выглядят как цифры с классических калькуляторов 1970-х, однако уже давно не выпускаются и их тяжело найти. Его обновление набора от 2020 года использует современные семисегментные светодиоды. Раритетная версия стоит дороже: $79 против $39 за вариант 2020 года. Однако у оригинальной версии есть одна приятная особенность: размеры отверстий платы подобраны так, что светодиоды вставляются просто с усилием, без пайки. Их можно вынуть и использовать в других проектах.

Функционально оба варианта одинаковы и основаны на микроконтроллере MSP430. Он устраняет необходимость в использовании почти всех остальных компонентов, которые использовались в Sinclair Scientific, и на нём работает эмулятор семейства чипов TMS080x. Семейство TMS080x производила компания Texas Instruments (TI), а конкретные версии чипов, например, TMS0805, использовавшийся в Sinclair Scientific, отличались содержимым их ROM на 3520 бит.


В стиле 70-х: в ранних калькуляторах часто использовались такие выпуклые светодиодные модули. Сферы служили линзами, увеличивавшими небольшие сегменты.

Много лет загадка того, как Sinclair заставила этот чип творить волшебство, оставалась запертой в ROM чипа TMS0805. Ситуация изменилась в 2013, когда Кен Ширрифф прослышал об Sinclair Scientific, пообщавшись с командой Visual 6502. Эта группа занимается реверс-инжинирингом классических чипов, оригинальные чертежи которых уже утеряны. Иногда для этого приходится растворять корпус чипа кислотой и аккуратно фотографировать открывшиеся кристаллы в микроскоп, изучая отдельные транзисторы. Ширрифф смог создать обобщённый симулятор чипов TMS080x на JavaScript, просто изучая заявки на патенты от TI. Однако конкретный код от Sinclair, использовавшийся в ROM чипа TMS0805, ему не давался до тех пор, пока один из членов команды Visual 6502, Джон Макмастер, не сделал фотографии кристалла в 2014.

Ширрифф активно изучает и описывает историю вычислительных машин, поэтому я написал ему вопрос о том, как ему удалось перейти от микрофотографий к рабочему коду. Он написал, что изучая, как были расположены вентили на том участке чипа, где у него был ROM, я сразу смог извлечь необработанные биты. Фил Майнваринг, Эд Спиттлс и я ещё один день потратили на то, чтобы понять, как эти биты связаны с кодом. Код состоял из 320 слов по 11 бит, однако физически в ROM было 55 строк и 64 столбца. Изучая схему, анализируя закономерности битов, перебирая методом грубой силы различные комбинации, мы разобрались в том, как там всё было устроено, и смогли извлечь код.

Когда Ширрифф загрузил код в симулятор, он смог разобраться в принципах его работы. Он, по сути, использовал простейшие алгоритмы перебора, способные дать ответ. Однако для большей точности использовались некоторые математические хитрости, не говоря уже о программных хитростях, пояснил он. Ширрифф поддерживает онлайн-версию симулятора, где можно отладчиком идти по коду построчно.


Вот это поворот: Sinclair Scientific подсчитывал тригонометрические функции, последовательно поворачивая изначальный вектор до тех пор, пока он не доходил до цели. Для большинства углов это занимало несколько секунд.

Sinclair Scientific смогли уменьшить сложность кода используя обратную польскую запись, в которой математические операторы идут после чисел, над которыми производится действия к примеру, действие 5 + 4 пишется как 5 4 +. Тригонометрические функции использовали технику итерационного приближения, которая могла подсчитывать результат по нескольку секунд, и иногда давала точность максимум до третьего числа после запятой. Калькулятор использовал фиксированную научную запись для всего десятичный разделитель ввести было нельзя. То есть, вместо ввода 521.4 нужно вводить 5214, а на дисплее это выглядит, как 5.214. Затем нужно нажать E и ввести 2, что даст число 5.214 102. За один раз можно вводить только одно число.

В таком описании это выглядит ужасно. К такому можно было бы привыкнуть, только если бы вы не могли позволить себе, допустим, HP-35, создатели которого гордились его точностью и функциональностью (хотя HP-35 тоже использовал обратную польскую запись, в нём всё было немного хитрее).

Однако Sinclair пыталась конкурировать не с другими калькуляторами, а с логарифмической линейкой. Я читал об этом, но реально осознал только тогда, когда получил набор. Случайно я недавно как раз купил винтажную логарифмическую линейку Pickett, и научился выполнять на ней базовые математические операции по урокам, доступным на сайте международного музея логарифмических линеек. Потом, попробовав в деле Sinclair Scientific, я поразился тому, насколько он был концептуально похож на мою логарифмическую линейку. Для неё точность до двух-трёх цифр после запятой вещь нормальная, вы выставляете скользящее окошко линейки так, чтобы между шкалами было всего одно число, без учёта порядка. Вы работаете со значимыми цифрами, а потом проставляете запятую у конечного результата поэтому, например, 52 2 и 5200 20 вычисляются абсолютно одинаково.

Поэтому наборы для воссоздания реплик так важны подобное понимание принципов работы устройства можно получить, только подержав его в руках. Это напомнило мне высказывание о том, что хороший дизайн это не абстрактный идеал, он существует только в конкретном контексте. Так был ли калькулятор Sinclair Scientific элегантным? С моей точки зрения, ещё каким.
Подробнее..

Android Camera2 API от чайника, часть 6. Стрим видео сначала кодировали, теперь декодируем

20.06.2020 18:10:45 | Автор: admin

Итак, в предыдущем посте мы занимались кодированием живого видео формата H.264 на Android устройстве, которое затем отправляли для просмотра на персональный компьютер под виндой. Там наш видеопоток успешно раскодировывался и лицезрелся с помощью VLC плеера. А так же с помощью библиотеки VLCJ CAPRICA благополучно впихивался и в окошки JAVA приложения. Правда, каким именно образом он (VLC плеер) всё это проделывал, так и осталось загадкой. Но с другой стороны работает, да и ладно.

Подстольный настольный компьютер, ноутбук, лэптоп всё это прекрасно, но тем не менее, всё больше народа смотрит видео и управляет разными девайсами не из-за стола, а чаще валяясь на диване, со смартфоном в руках. И соответственно, к примеру, даже нашей роботележкой ныне удобней управлять именно оттуда. Поэтому настало время выяснить, как наш закодированный видео поток принять и отобразить на экране такого же Android устройства. Естественно, как и раньше мы проделаем всё через Camera2 API, концепцию Surface, да ещё и асинхронно!

Кому интересно вперёд.

Собственно, для чего мне это понадобилось, кроме как обычно стремления, по выражению ослика Иа, к обалдеванию новыми знаниями? Откровенно говоря, про роботележку я упомянул только для примера (я думаю за такое количество статей она уже всем успела изрядно надоесть). На самом деле, кодируя-декодируя мы соберём незамысловатый видеотелефон для домашней сети.

Но это будет:
в следующей статье. Поскольку надо ещё и аудио канал прикрутить.

А в этом посте мы будем работать только со стримом видео (без звука). Но тоже с благородной целью, будем помогать сознанию выходить из тела. Есть такие интересные эксперименты у нейробиологов, когда одному человеку транслируют прямо в глаз то, что видит другой и соответственно тому другому в обратном порядке.

Этот эксперимент можно повторить с помощью нашего приложения, пары VR-очков для смартфона и естественно с помощью двух смартфонов. Одни очки хорошие и дорогие, которыми вы обычно пользуетесь, а другие Да, даже за четыреста рублей сойдут.

image

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

Если по существу, то изображение с камеры первого Android устройства передается на экран второго и раздваивается там (VR очки же). И наоборот, со второго девайса видео поток подается на первый в таком же порядке. То есть, вы видите то же, что должен видеть ваш партнер, а он (она), видит то, что должны видеть вы. Поскольку мы, в буквальном смысле, есть там, где есть наши глаза, ощущения будут у вас непередаваемые, особенно, если вы будете стараться двигаться синхронно и смотреть на свое (её) тело. Для неврологических экспериментов просто поле непаханное, ну или для камасутры всякой.

А ещё можно подавать для каждого глаза разные изображения, чтобы посмотреть, что получится и как переклинит ваши мозговые полушария.

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

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

А находится он, как известно, в классе:

MEDIA CODEC


В пред пред предыдущем посте с его помощью мы кодировали в формате H.264 видео поток полученный с камеры и отправляли его по UDP каналу на нужный адрес. Поэтому, чтобы начать работу, нам весьма пригодится код из того самого поста. Чтобы не парить голову с отладкой программы на двух телефонах сразу, для начала мы сделаем всё на одном. Просто для понимания принципа работы. В одном окошке мы будем снимать видео, кодировать и отправлять его по домашней сетке самим себе во второе окошко. Всё по-честному, никаких localhost и адресов 127.0.0.1. Только настоящий сетевой адрес, только хардкор!

В сущности, в дополнение к уже имеющейся программе нам надо добавить:

  1. Изменение UI, то есть добавление второго окна и ассоциированного с ним Surface
  2. Блок кода для получения видео потока по UDP каналу
  3. Процедуру самого декодирования.

Всё это делается легко и просто, но об несколько подводных камней удариться всё равно придется.

Итак, сначала модифицируем макет добавляем второе окно.

activity_main.xml
<?xml version="1.0" encoding="utf-8"?><androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://personeltest.ru/away/schemas.android.com/apk/res/android"    xmlns:app="http://personeltest.ru/away/schemas.android.com/apk/res-auto"    xmlns:tools="http://personeltest.ru/away/schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    tools:context=".MainActivity">    <TextureView        android:id="@+id/textureView"        android:layout_width="320dp"        android:layout_height="240dp"        android:layout_marginTop="88dp"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintHorizontal_bias="0.494"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toTopOf="parent" />    <TextureView        android:id="@+id/textureView3"        android:layout_width="320dp"        android:layout_height="240dp"        android:layout_marginTop="24dp"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintHorizontal_bias="0.494"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toBottomOf="@+id/textureView" />    <LinearLayout        android:layout_width="165dp"        android:layout_height="40dp"        app:layout_constraintBottom_toBottomOf="parent"        app:layout_constraintEnd_toEndOf="parent"        app:layout_constraintStart_toStartOf="parent"        app:layout_constraintTop_toBottomOf="@+id/textureView3"        app:layout_constraintVertical_bias="0.838">        <Button            android:id="@+id/button1"            android:layout_width="wrap_content"            android:layout_height="36dp"            android:text="вкл" />        <Button            android:id="@+id/button3"            android:layout_width="wrap_content"            android:layout_height="37dp"            android:text="выкл" />    </LinearLayout></androidx.constraintlayout.widget.ConstraintLayout>


Эта часть вообще объяснений не требует. Идем дальше.

Пишем код для получения дэйтаграмм


Тут есть пара тонкостей. Когда мы дэйтаграммы отправляли, то действовали совсем незамысловато. Мы дожидались, когда сработает коллбэк буфера выходных данных onOutputBufferAvailable, а затем легко и просто пихали полученный от него байтовый массив в UDP пакет. Дальше он уже без всякой помощи с нашей стороны уезжал по указанному адресу. Единственное, мы его ещё рубили на килобайтовые блоки (чтобы гарантированно влез в размер MTU), но сейчас это совершенно излишне (и даже, как мы увидим потом, вредно).

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

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

В итоге, код оказался несложным:

        Udp_recipient()        {            start();            mNewFrame = false;        }        public void run()        {                         try {                    byte buffer[] = new byte[50000];                    DatagramPacket p = new DatagramPacket(buffer, buffer.length);                    udpSocketIn.receive(p);                    byte bBuffer[] = p.getData();                    outDataForEncoder = new byte[p.getLength()];                     synchronized (outDataForEncoder)                     {                         for (int i = 0; i < outDataForEncoder.length; i++)                         {                             outDataForEncoder[i] = bBuffer[i];                         }                     }                    mNewFrame = true;                } catch (Exception e) {                    Log.i(LOG_TAG, e + "  ");                }             }        }

Итак, поток в наличии. После получения дэйтаграммы устанавливаем флаг NewFrame, чтобы декодер не путался и сбрасываем флаг перед прибытием нового UDP пакета. Здесь неявно предполагается, что декодер скуривает пакеты быстрее, чем они приходят. А это действительно, так и оказалось.

Переходим теперь к самому декодеру


Как и кодер он может работать в двух режимах синхронном и асинхронном. В первом случае вы учреждаете немало потоков, флагов и тщательно следите за тем, чтобы не впасть в состояние гонок (всё ж параллельное). Этот процесс сопровождается кучей стереотипного кода, в просторечии Boiler Plate, и в общем многократно где описан.

Ну, а мы, поскольку кодировали видео асинхронно через коллбэки, то делать шаг назад к синхронности, вроде как, нам не к лицу. Но проблема в том, что примеров асинхронного кода, почему-то намного меньше и основная мысль там такая: я написал, а оно не работает, хелп!.

То ли считается, что там всё тривиально (а оно по сравнению с синхронным вариантом действительно так), то ли правда, оно у всех не работает. Разбираться опять придётся самим.

Итак, сначала учреждаем и инициализируем экземпляр декодера. Это вообще не сложно, поскольку он будет использовать формат кодера и нам только надо установить разрешение выходного окна в пихелях (это мое личное изобретение пихель, так сказать, родним русский и английский). Пока ограничимся скромным 640 на 480. Также надо будет присобачить декодер к Surface этого самого окна, чтобы было куда показывать.

       try {            decoder = MediaCodec.createDecoderByType("video/avc");// H264 декодек        } catch (Exception e) {            Log.i(LOG_TAG, "а нету декодека");        }        int width = 480; // ширина видео        int height = 640; // высота видео        MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);        format.setInteger(MediaFormat.KEY_ROTATION,90);        SurfaceTexture texture = mImageViewDown.getSurfaceTexture();        texture.setDefaultBufferSize(480, 640);        mDecoderSurface = new Surface(texture);        decoder.configure(format, mDecoderSurface, null,0);        decoder.setOutputSurface(mDecoderSurface);        decoder.setCallback(new DecoderCallback());        decoder.start();        Log.i(LOG_TAG, "запустили декодер");

Далее обращаемся к коллбэкам. Нужных, как известно, два:

void onInputBufferAvailable(MediaCodec mc, int inputBufferId)void onOutputBufferAvailable(MediaCodec mc, int outputBufferId, )

То есть, при срабатывании первого коллбэка мы в него кладём данные, а при вызове второго вынимаем готовенькое и отправляем на Surface. Может показаться странным, что когда мы делали кодирование (то есть, наоборот из Surface в кодек), то почему-то InputBuffer мы не использовали, а сразу волшебным образом доставали байтовые данные из OutputBuffer. Это мне тоже казалось странным, пока я не прочитал:
When using an input Surface, there are no accessible input buffers, as buffers are automatically passed from the input surface to the codec. Calling dequeueInputBuffer will throw an IllegalStateException, and getInputBuffers() returns a bogus ByteBuffer[] array that MUST NOT be written into.
Короче, автоматически это делается. Ну, и сделали бы при декодировании также. Но нет, придётся самим.

Итак, в метод void (MediaCodec mc, int inputBufferId) я ничтоже сумняшеся прописал:

 private class DecoderCallback extends MediaCodec.Callback {        @Override        public void onInputBufferAvailable(MediaCodec codec, int index) {                  decoderInputBuffer = codec.getInputBuffer(index);                  decoderInputBuffer.clear();                             if(mNewFrame)                             {                               synchronized (outDataForEncoder)                               { b=outDataForEncoder;  }                             }                             decoderInputBuffer.put(b);                 codec.queueInputBuffer(index, 0, b.length,0, 0);              if(mNewFrame)              new Udp_recipient();        }

Ну, по аналогии, как мы делали в прошлый раз в кодере.

То есть, когда декодер вдруг ощущает, что ему срочно нужны данные, он вызывает этот коллбэк и кладёт наш прибывший udp-пакет (который уже доступен в виде байтового массива) в один из своих буферов под номером index. Там их вроде четыре. Естественно, ничего не заработало. Я ж забыл про onOutputBufferAvailable.

Туда тоже необходимо вставить две строчки:

    @Override        public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {            {                  {                      decoderOutputBuffer = codec.getOutputBuffer(index);                      codec.releaseOutputBuffer(index, true);                  }            }        }

Причем значение True/False отвечает за то, будет ли содержимое буфера рендерится на Surface или выкинется на помойку.

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



А в логах обнаружились какие-то таинственные:

buffer descriptor with invalid usage bits 0x2000
A resource failed to call release.


Но большой ясности они не прибавляли. Некоторое понимание пришло из прошлых наблюдений. Дело в том, что я сначала не использовал флаг NewFrame (просто руки ещё не дошли) и декодер мог обратится к одному и тому же пришедшему пакету несколько раз (данные-то обрабатываются гораздо быстрее, чем приходят, как уже говорилось). На экране была, понятное дело, совершенная каша, но onInputBufferAvailable работал и работал без перебоев!

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

Как только поправки были произведены, на Output Surface наконец-то появилось полученное через сеть видео.



Правда, как видно, на нём присутствуют некоторые недостатки. Посмотрев на них какое-то время, я стал в душе догадываться, что это все опять из-за onInputBufferAvailable. Ему снова что-то не нравилось. Оказалось, был не по вкусу байтовый массив. Я тогда про это не догадывался, мне лично не нравилась концепция флага NewFrame. Как-то она не сочеталась с реактивным программированием. Поэтому я решил полученные пакеты не просто класть в массив, а заворачивать этот массив в итоге в байтовый поток ByteArrayOutputStream. И пускай коллбэк, что ему надо, сам оттуда забирает, а конкретно каким образом, это его проблема.

Идея сработала блестяще и на экране я увидел это:

image

Согласитесь, прогресс существенный. Но опять чего-то не хватает. Или чего лишнее?

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

Решение было простым:

поток.reset();

И всё заработало как надо.



Теперь можно было насладиться законченным кодом, как обычно крайне минималистическим:

Листинг main_activity
package com.example.encoderdecoder;import androidx.annotation.RequiresApi;import androidx.appcompat.app.AppCompatActivity;import androidx.core.content.ContextCompat;import android.Manifest;import android.content.Context;import android.content.pm.ActivityInfo;import android.content.pm.PackageManager;import android.graphics.SurfaceTexture;import android.hardware.camera2.CameraAccessException;import android.hardware.camera2.CameraCaptureSession;import android.hardware.camera2.CameraDevice;import android.hardware.camera2.CameraManager;import android.hardware.camera2.CaptureRequest;import android.media.MediaCodec;import android.media.MediaCodecInfo;import android.media.MediaFormat;import android.os.Build;import android.os.Bundle;import android.os.Handler;import android.os.HandlerThread;import android.os.StrictMode;import android.util.Log;import android.view.Surface;import android.view.TextureView;import android.view.View;import android.widget.Button;import android.widget.Toast;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.net.DatagramPacket;import java.net.DatagramSocket;import java.net.InetAddress;import java.net.SocketException;import java.nio.ByteBuffer;import java.util.Arrays;public class MainActivity extends AppCompatActivity {    public static final String LOG_TAG = "myLogs";    public static Surface surface = null;    CameraService[] myCameras = null;    private CameraManager mCameraManager = null;    private final int CAMERA1 = 0;    private Button mButtonOpenCamera1 = null;    private Button mButtonTStopStreamVideo = null;    public static TextureView mImageViewUp = null;    public static TextureView mImageViewDown = null;    private HandlerThread mBackgroundThread;    private Handler mBackgroundHandler = null;    private MediaCodec encoder = null; // кодер    private MediaCodec decoder = null;    byte [] b;    Surface mEncoderSurface; // Surface как вход данных для кодера    Surface mDecoderSurface; // Surface как прием данных от кодера    ByteBuffer outPutByteBuffer;    ByteBuffer decoderInputBuffer;    ByteBuffer decoderOutputBuffer;    byte outDataForEncoder [];    DatagramSocket udpSocket;    DatagramSocket udpSocketIn;    String ip_address = "your target address";// сюда пишете IP адрес телефона куда шлете //видео, но можно и себе    InetAddress address;    int port = 40002;    ByteArrayOutputStream  out;    private void startBackgroundThread() {        mBackgroundThread = new HandlerThread("CameraBackground");        mBackgroundThread.start();        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());    }    private void stopBackgroundThread() {        mBackgroundThread.quitSafely();        try {            mBackgroundThread.join();            mBackgroundThread = null;            mBackgroundHandler = null;        } catch (InterruptedException e) {            e.printStackTrace();        }    }    @RequiresApi(api = Build.VERSION_CODES.M)    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();        StrictMode.setThreadPolicy(policy);        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);        setContentView(R.layout.activity_main);        Log.d(LOG_TAG, "Запрашиваем разрешение");        if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED                ||                (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)        ) {            requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);        }        mButtonOpenCamera1 = findViewById(R.id.button1);        mButtonTStopStreamVideo = findViewById(R.id.button3);        mImageViewUp = findViewById(R.id.textureView);        mImageViewDown = findViewById(R.id.textureView3);        mButtonOpenCamera1.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                setUpMediaCodec();// инициализируем Медиа Кодек                if (myCameras[CAMERA1] != null) {// открываем камеру                    if (!myCameras[CAMERA1].isOpen()) myCameras[CAMERA1].openCamera();                }            }        });        mButtonTStopStreamVideo.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                if (encoder != null) {                    Toast.makeText(MainActivity.this, " остановили стрим", Toast.LENGTH_SHORT).show();                    myCameras[CAMERA1].stopStreamingVideo();                }            }        });        try {            udpSocket = new DatagramSocket();            udpSocketIn = new DatagramSocket(port);// we changed it to DatagramChannell becouse UDP packets may be different in size            try {            }            catch (Exception e){                Log.i(LOG_TAG, "  создали udp канал");            }            new Udp_recipient();            Log.i(LOG_TAG, "  создали udp сокет");        } catch (                SocketException e) {            Log.i(LOG_TAG, " не создали udp сокет");        }        try {            address = InetAddress.getByName(ip_address);            Log.i(LOG_TAG, "  есть адрес");        } catch (Exception e) {        }        mCameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);        try {            // Получение списка камер с устройства            myCameras = new CameraService[mCameraManager.getCameraIdList().length];            for (String cameraID : mCameraManager.getCameraIdList()) {                Log.i(LOG_TAG, "cameraID: " + cameraID);                int id = Integer.parseInt(cameraID);                // создаем обработчик для камеры                myCameras[id] = new CameraService(mCameraManager, cameraID);            }        } catch (CameraAccessException e) {            Log.e(LOG_TAG, e.getMessage());            e.printStackTrace();        }    }    public class CameraService {        private String mCameraID;        private CameraDevice mCameraDevice = null;        private CameraCaptureSession mSession;        private CaptureRequest.Builder mPreviewBuilder;        public CameraService(CameraManager cameraManager, String cameraID) {            mCameraManager = cameraManager;            mCameraID = cameraID;        }        private CameraDevice.StateCallback mCameraCallback = new CameraDevice.StateCallback() {            @Override            public void onOpened(CameraDevice camera) {                mCameraDevice = camera;                Log.i(LOG_TAG, "Open camera  with id:" + mCameraDevice.getId());                startCameraPreviewSession();            }            @Override            public void onDisconnected(CameraDevice camera) {                mCameraDevice.close();                Log.i(LOG_TAG, "disconnect camera  with id:" + mCameraDevice.getId());                mCameraDevice = null;            }            @Override            public void onError(CameraDevice camera, int error) {                Log.i(LOG_TAG, "error! camera id:" + camera.getId() + " error:" + error);            }        };        private void startCameraPreviewSession() {            SurfaceTexture texture = mImageViewUp.getSurfaceTexture();            texture.setDefaultBufferSize(480, 640);            surface = new Surface(texture);            try {                mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW);                mPreviewBuilder.addTarget(surface);                mPreviewBuilder.addTarget(mEncoderSurface);                mCameraDevice.createCaptureSession(Arrays.asList(surface, mEncoderSurface),                        new CameraCaptureSession.StateCallback() {                            @Override                            public void onConfigured(CameraCaptureSession session) {                                mSession = session;                                try {                                    mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mBackgroundHandler);                                } catch (CameraAccessException e) {                                    e.printStackTrace();                                }                            }                            @Override                            public void onConfigureFailed(CameraCaptureSession session) {                            }                        }, mBackgroundHandler);            } catch (CameraAccessException e) {                e.printStackTrace();            }        }        public boolean isOpen() {            if (mCameraDevice == null) {                return false;            } else {                return true;            }        }        public void openCamera() {            try {                if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {                    mCameraManager.openCamera(mCameraID, mCameraCallback, mBackgroundHandler);                }            } catch (CameraAccessException e) {                Log.i(LOG_TAG, e.getMessage());            }        }        public void closeCamera() {            if (mCameraDevice != null) {                mCameraDevice.close();                mCameraDevice = null;            }        }        public void stopStreamingVideo() {            if (mCameraDevice != null & encoder != null) {                try {                    mSession.stopRepeating();                    mSession.abortCaptures();                } catch (CameraAccessException e) {                    e.printStackTrace();                }                encoder.stop();                encoder.release();                mEncoderSurface.release();                decoder.stop();                decoder.release();                closeCamera();            }        }    }    private void setUpMediaCodec() {        try {            encoder = MediaCodec.createEncoderByType("video/avc"); // H264 кодек        } catch (Exception e) {            Log.i(LOG_TAG, "а нету кодека");        }        {            int width = 480; // ширина видео            int height = 640; // высота видео            int colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; // формат ввода цвета            int videoBitrate = 2000000; // битрейт видео в bps (бит в секунду)            int videoFramePerSecond = 30; // FPS            int iframeInterval = 1; // I-Frame интервал в секундах            MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);            format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);            format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate);            format.setInteger(MediaFormat.KEY_FRAME_RATE, videoFramePerSecond);            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iframeInterval);            encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // конфигурируем кодек как кодер            mEncoderSurface = encoder.createInputSurface(); // получаем Surface кодера        }        encoder.setCallback(new EncoderCallback());        encoder.start(); // запускаем кодер        Log.i(LOG_TAG, "запустили кодек");        try {            decoder = MediaCodec.createDecoderByType("video/avc");// H264 декодек        } catch (Exception e) {            Log.i(LOG_TAG, "а нету декодека");        }        int width = 480; // ширина видео        int height = 640; // высота видео        MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);        format.setInteger(MediaFormat.KEY_ROTATION,90);        SurfaceTexture texture = mImageViewDown.getSurfaceTexture();        texture.setDefaultBufferSize(480, 640);        mDecoderSurface = new Surface(texture);        decoder.configure(format, mDecoderSurface, null,0);        decoder.setOutputSurface(mDecoderSurface);        decoder.setCallback(new DecoderCallback());        decoder.start();        Log.i(LOG_TAG, "запустили декодер");    }    //CALLBACK FOR DECODER    private class DecoderCallback extends MediaCodec.Callback {        @Override        public void onInputBufferAvailable(MediaCodec codec, int index) {                  decoderInputBuffer = codec.getInputBuffer(index);                  decoderInputBuffer.clear();                                    synchronized (out)                    {                            b =  out.toByteArray();                        out.reset();                          }                            decoderInputBuffer.put(b);                   codec.queueInputBuffer(index, 0, b.length,0, 0);        }        @Override        public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {            {                  {                      decoderOutputBuffer = codec.getOutputBuffer(index);                      codec.releaseOutputBuffer(index, true);                  }            }        }        @Override        public void onError(MediaCodec codec, MediaCodec.CodecException e) {            Log.i(LOG_TAG, "Error: " + e);        }        @Override        public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {            Log.i(LOG_TAG, "decoder output format changed: " + format);        }    }    private class EncoderCallback extends MediaCodec.Callback {        @Override        public void onInputBufferAvailable(MediaCodec codec, int index) {            Log.i(LOG_TAG, " входные буфера готовы" );            //        }        @Override        public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {            outPutByteBuffer = encoder.getOutputBuffer(index);            byte[] outDate = new byte[info.size];            outPutByteBuffer.get(outDate);            try {                DatagramPacket packet = new DatagramPacket(outDate, outDate.length, address, port);                udpSocket.send(packet);            } catch (IOException e) {                Log.i(LOG_TAG, " не отправился UDP пакет");            }            encoder.releaseOutputBuffer(index, false);        }        @Override        public void onError(MediaCodec codec, MediaCodec.CodecException e) {            Log.i(LOG_TAG, "Error: " + e);        }        @Override        public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {          //  Log.i(LOG_TAG, "encoder output format changed: " + format);        }    }    @Override    public void onPause() {        if (myCameras[CAMERA1].isOpen()) {            myCameras[CAMERA1].closeCamera();        }        stopBackgroundThread();        super.onPause();    }    @Override    public void onResume() {        super.onResume();        startBackgroundThread();    }    public class Udp_recipient extends Thread {        Udp_recipient()        {            out = new ByteArrayOutputStream(50000);            start();        }        public void run()        {            while (true)            {                try {                    byte buffer[] = new byte[50000];                    DatagramPacket p = new DatagramPacket(buffer, buffer.length);                    udpSocketIn.receive(p);                    byte bBuffer[] = p.getData();                    outDataForEncoder = new byte[p.getLength()];                     synchronized (outDataForEncoder)                     {                         for (int i = 0; i < outDataForEncoder.length; i++)                         {                             outDataForEncoder[i] = bBuffer[i];                         }                     }                     synchronized (out)                            {out.write(outDataForEncoder);}                } catch (Exception e) {                    Log.i(LOG_TAG, e + "  ");                }            }        }        }    }


Как видно из листинга, мы выкинули из кодера фрагмент, где готовый байтовый массив рубился на кусочки длиной не более килобайта из-за опасения, что они могут не влезть в MTU. Опасения, как уже говорилось, оказались напрасными и даже вредными. Дело в том, (как мне показалось ) что кодер лепит эти массивы уже как некие смысловые единицы и соответственно декодер таким же порядком кладёт их в свои входные буферы. А если у нас килобайт попадает в один буфер, а хвостик в другой? Теперь-то скрывать уже нечего, но на самом деле, видео у меня красиво не показывало даже тогда, когда я учредил ByteArrayOutputStream. Не показывало до тех пор, пока я не выкинул этот злосчастный фрагмент кода.

А вот, когда мы гнали видео поток на компьютер и декодировали его VLC плеером, такой проблемы не было. Скорее всего, не было потому, что плеер предварительно формировал кэш (он же плеер, он больше для просмотра кино, а не живого видео). А в кэше, все эти порубленные килобайты вновь красиво срастались.

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



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

Для полноты счастья оставалось только провести эксперименты с доступными разрешением и битрейтом. Я сначала хотел было поставить вполне приличные 1280 х 960, но для этого в кодеке H.264 нужен битрейт 5000-6000 Кбит / с. А при установке такой скорости мой декодер, к сожалению, со своей работой уже не справлялся. Пришлось ограничиться разрешением 640 х 480 (тем, что и так уже было) и подходящим для этого битрейтом 2000 Кбит / с. Потому что при приближении к скорости света к 3 000 000 бит/c, декодер начинает иметь бесконечную массу, валять дурака и отваливаться.

Тем не менее, теперь на руках у нас есть рабочий образцовый код на основании которого можно лепить видео телефон (в следующей статье) и аппаратуру для нейрологических исследований (прямо сейчас).

Сама программа особо не поменяется. Макет вообще трогать не будем. Нам всего лишь надо:

  1. Отключить первую Surface от камеры
  2. Подключить к ней декодер. Грубо говоря, скопировать окно.

Я думал, что задача окажется тривиальной, но как всегда ошибся. Казалось бы, надо просто создать вторую ссылку на экземпляр декодера и оно само всё скопируется во второе окно.
Действительно, если у вас к примеру два текстовых окна и вы хотите вывести в них значение переменной Int, то вы же не вычисляете это Int два раза. Вы просто пишете:

TextView1.setText(Int +  );TextView2.setText(Int +  );

Но видимо, поток данных в Surface не так прост как просто вывод текста. Если пробовать решить вопрос таким путем, то у вас будет выводится окно, которое было инициализировано последним. А первая инициализация исчезнет.

Чего я только не делал, даже textureView окна в макете пытался обозвать одинаково, ничего не помогало. На stockoverflow было на этот счёт мало чего (действительно, кому нафиг надо дублировать видео поток в два одинаковых окна на смартфоне), а в единственном обсуждении, что я нашёл, высказывалось туманное предположение, что один экземпляр Mediac Codec может связываться только с одной Surface.

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

Но, тем тем не менее, при всей своей дубовости метод работает.

Листинг кода main_activity
package com.example.twovideosurfaces;import androidx.annotation.RequiresApi;import androidx.appcompat.app.AppCompatActivity;import androidx.core.content.ContextCompat;import android.Manifest;import android.content.Context;import android.content.pm.ActivityInfo;import android.content.pm.PackageManager;import android.graphics.SurfaceTexture;import android.hardware.camera2.CameraAccessException;import android.hardware.camera2.CameraCaptureSession;import android.hardware.camera2.CameraDevice;import android.hardware.camera2.CameraManager;import android.hardware.camera2.CaptureRequest;import android.media.MediaCodec;import android.media.MediaCodecInfo;import android.media.MediaFormat;import android.os.Build;import android.os.Bundle;import android.os.Handler;import android.os.HandlerThread;import android.os.StrictMode;import android.util.Log;import android.view.Surface;import android.view.TextureView;import android.view.View;import android.widget.Button;import android.widget.Toast;import java.io.ByteArrayOutputStream;import java.io.IOException;import java.net.DatagramPacket;import java.net.DatagramSocket;import java.net.InetAddress;import java.net.SocketException;import java.nio.ByteBuffer;import java.util.Arrays;public class MainActivity extends AppCompatActivity  {    public static final String LOG_TAG = "myLogs";    CameraService[] myCameras = null;    private CameraManager mCameraManager = null;    private final int CAMERA1 = 0;    private Button mOn = null;    private Button mOff = null;    public static TextureView mImageViewUp = null;    public static TextureView mImageViewDown = null;    private HandlerThread mBackgroundThread;    private Handler mBackgroundHandler = null;    private MediaCodec encoder = null; // кодер    private MediaCodec decoder = null;    private MediaCodec decoder2 = null;    byte [] b;    byte [] b2;    Surface mEncoderSurface; // Surface как вход данных для кодера    Surface mDecoderSurface; // Surface как прием данных от кодера    Surface mDecoderSurface2; // Surface как прием данных от кодера    ByteBuffer outPutByteBuffer;    ByteBuffer decoderInputBuffer;    ByteBuffer decoderOutputBuffer;    ByteBuffer decoderInputBuffer2;    ByteBuffer decoderOutputBuffer2;    byte outDataForEncoder [];    static  boolean  mNewFrame=false;    DatagramSocket udpSocket;    DatagramSocket udpSocketIn;    String ip_address = "192.168.50.131";    InetAddress address;    int port = 40002;    ByteArrayOutputStream out =new ByteArrayOutputStream(50000);    ByteArrayOutputStream out2 = new ByteArrayOutputStream(50000);    private void startBackgroundThread() {        mBackgroundThread = new HandlerThread("CameraBackground");        mBackgroundThread.start();        mBackgroundHandler = new Handler(mBackgroundThread.getLooper());    }    private void stopBackgroundThread() {        mBackgroundThread.quitSafely();        try {            mBackgroundThread.join();            mBackgroundThread = null;            mBackgroundHandler = null;        } catch (InterruptedException e) {            e.printStackTrace();        }    }    @RequiresApi(api = Build.VERSION_CODES.M)    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();        StrictMode.setThreadPolicy(policy);        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);        setContentView(R.layout.activity_main);        Log.d(LOG_TAG, "Запрашиваем разрешение");        if (checkSelfPermission(Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED                ||                (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED)        ) {            requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);        }        mOn = findViewById(R.id.button1);        mOff = findViewById(R.id.button3);        mImageViewUp = findViewById(R.id.textureView);        mImageViewDown = findViewById(R.id.textureView3);        mOn.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                setUpMediaCodec();// инициализируем Медиа Кодек                if (myCameras[CAMERA1] != null) {// открываем камеру                    if (!myCameras[CAMERA1].isOpen()) myCameras[CAMERA1].openCamera();                }            }        });        mOff.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                if (encoder != null) {                    Toast.makeText(MainActivity.this, " остановили стрим", Toast.LENGTH_SHORT).show();                    myCameras[CAMERA1].stopStreamingVideo();                }            }        });        try {            udpSocket = new DatagramSocket();            udpSocketIn = new DatagramSocket(port);// we changed it to DatagramChannell becouse UDP packets may be different in size            try {            }            catch (Exception e){                Log.i(LOG_TAG, "  создали udp канал");            }            new Udp_recipient();            Log.i(LOG_TAG, "  создали udp сокет");        } catch (                SocketException e) {            Log.i(LOG_TAG, " не создали udp сокет");        }        try {            address = InetAddress.getByName(ip_address);            Log.i(LOG_TAG, "  есть адрес");        } catch (Exception e) {        }        mCameraManager = (CameraManager) getSystemService(Context.CAMERA_SERVICE);        try {            // Получение списка камер с устройства            myCameras = new CameraService[mCameraManager.getCameraIdList().length];            for (String cameraID : mCameraManager.getCameraIdList()) {                Log.i(LOG_TAG, "cameraID: " + cameraID);                int id = Integer.parseInt(cameraID);                // создаем обработчик для камеры                myCameras[id] = new CameraService(mCameraManager, cameraID);            }        } catch (CameraAccessException e) {            Log.e(LOG_TAG, e.getMessage());            e.printStackTrace();        }    }    public class CameraService {        private String mCameraID;        private CameraDevice mCameraDevice = null;        private CameraCaptureSession mSession;        private CaptureRequest.Builder mPreviewBuilder;        public CameraService(CameraManager cameraManager, String cameraID) {            mCameraManager = cameraManager;            mCameraID = cameraID;        }        private CameraDevice.StateCallback mCameraCallback = new CameraDevice.StateCallback() {            @Override            public void onOpened(CameraDevice camera) {                mCameraDevice = camera;                Log.i(LOG_TAG, "Open camera  with id:" + mCameraDevice.getId());                startCameraPreviewSession();            }            @Override            public void onDisconnected(CameraDevice camera) {                mCameraDevice.close();                Log.i(LOG_TAG, "disconnect camera  with id:" + mCameraDevice.getId());                mCameraDevice = null;            }            @Override            public void onError(CameraDevice camera, int error) {                Log.i(LOG_TAG, "error! camera id:" + camera.getId() + " error:" + error);            }        };        private void startCameraPreviewSession() {            try {                mPreviewBuilder = mCameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);                mPreviewBuilder.addTarget(mEncoderSurface);                mCameraDevice.createCaptureSession(Arrays.asList(mEncoderSurface),                        new CameraCaptureSession.StateCallback() {                            @Override                            public void onConfigured(CameraCaptureSession session) {                                mSession = session;                                try {                                    mSession.setRepeatingRequest(mPreviewBuilder.build(), null, mBackgroundHandler);                                } catch (CameraAccessException e) {                                    e.printStackTrace();                                }                            }                            @Override                            public void onConfigureFailed(CameraCaptureSession session) {                            }                        }, mBackgroundHandler);            } catch (CameraAccessException e) {                e.printStackTrace();            }        }        public boolean isOpen() {            if (mCameraDevice == null) {                return false;            } else {                return true;            }        }        public void openCamera() {            try {                if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) {                    mCameraManager.openCamera(mCameraID, mCameraCallback, mBackgroundHandler);                }            } catch (CameraAccessException e) {                Log.i(LOG_TAG, e.getMessage());            }        }        public void closeCamera() {            if (mCameraDevice != null) {                mCameraDevice.close();                mCameraDevice = null;            }        }        public void stopStreamingVideo() {            if (mCameraDevice != null & encoder != null) {                try {                    mSession.stopRepeating();                    mSession.abortCaptures();                } catch (CameraAccessException e) {                    e.printStackTrace();                }                encoder.stop();                encoder.release();                mEncoderSurface.release();                decoder.stop();                decoder.release();                decoder2.stop();                decoder2.release();                closeCamera();            }        }    }    private void setUpMediaCodec() {        try {            encoder = MediaCodec.createEncoderByType("video/avc"); // H264 кодек        } catch (Exception e) {            Log.i(LOG_TAG, "а нету кодека");        }        {            int width = 640; // ширина видео            int height = 480; // высота видео            int colorFormat = MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface; // формат ввода цвета            int videoBitrate = 2000000; // битрейт видео в bps (бит в секунду)            int videoFramePerSecond = 30; // FPS            int iframeInterval = 1; // I-Frame интервал в секундах            MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);            format.setInteger(MediaFormat.KEY_COLOR_FORMAT, colorFormat);            format.setInteger(MediaFormat.KEY_BIT_RATE, videoBitrate);            format.setInteger(MediaFormat.KEY_FRAME_RATE, videoFramePerSecond);            format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iframeInterval);            encoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); // конфигурируем кодек как кодер           mEncoderSurface = encoder.createInputSurface(); // получаем Surface кодера        }        encoder.setCallback(new EncoderCallback());        encoder.start(); // запускаем кодер        Log.i(LOG_TAG, "запустили кодек");        try {            decoder = MediaCodec.createDecoderByType("video/avc");// H264 декодек        } catch (Exception e) {            Log.i(LOG_TAG, "а нету декодека");        }        int width = 480; // ширина видео        int height = 640; // высота видео        MediaFormat format = MediaFormat.createVideoFormat("video/avc", width, height);        format.setInteger(MediaFormat.KEY_ROTATION,90);        SurfaceTexture texture= mImageViewUp.getSurfaceTexture();        mDecoderSurface = new Surface(texture);        decoder.configure(format, mDecoderSurface, null,0);        decoder.setOutputSurface(mDecoderSurface);        decoder.setCallback(new DecoderCallback());        decoder.start();        Log.i(LOG_TAG, "запустили декодер");        try {            decoder2 = MediaCodec.createDecoderByType("video/avc");// H264 декодек        } catch (Exception e) {            Log.i(LOG_TAG, "а нету декодека");        }        int width2 = 480; // ширина видео        int height2 = 640; // высота видео        MediaFormat format2 = MediaFormat.createVideoFormat("video/avc", width2, height2);        format2.setInteger(MediaFormat.KEY_ROTATION,90);        SurfaceTexture texture2= mImageViewDown.getSurfaceTexture();        mDecoderSurface2 = new Surface(texture2);        decoder2.configure(format2, mDecoderSurface2, null,0);        decoder2.setOutputSurface(mDecoderSurface2);        decoder2.setCallback(new DecoderCallback2());        decoder2.start();        Log.i(LOG_TAG, "запустили декодер");    }    private class DecoderCallback2 extends MediaCodec.Callback {        @Override        public void onInputBufferAvailable(MediaCodec codec, int index) {            decoderInputBuffer2 = codec.getInputBuffer(index);            decoderInputBuffer2.clear();            synchronized (out2)            {                b2 =  out2.toByteArray();                out2.reset();            }            decoderInputBuffer2.put(b2);            codec.queueInputBuffer(index, 0, b2.length,0, 0);            if (b2.length!=0)            {                //   Log.i(LOG_TAG, b.length + " декодер вход  "+index );            }        }        @Override        public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {            {                {                    decoderOutputBuffer2 = codec.getOutputBuffer(index);                    codec.releaseOutputBuffer(index, true);                }            }        }        @Override        public void onError(MediaCodec codec, MediaCodec.CodecException e) {            Log.i(LOG_TAG, "Error: " + e);        }        @Override        public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {            Log.i(LOG_TAG, "decoder output format changed: " + format);        }    }    //CALLBACK FOR DECODER    private class DecoderCallback extends MediaCodec.Callback {        @Override        public void onInputBufferAvailable(MediaCodec codec, int index) {            decoderInputBuffer = codec.getInputBuffer(index);            decoderInputBuffer.clear();            synchronized (out)            {                b =  out.toByteArray();                out.reset();            }            decoderInputBuffer.put(b);            codec.queueInputBuffer(index, 0, b.length,0, 0);                 if (b.length!=0)            {               //  Log.i(LOG_TAG, b.length + " декодер вход  "+index );            }        }        @Override        public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {            {                {                    decoderOutputBuffer = codec.getOutputBuffer(index);                    codec.releaseOutputBuffer(index, true);                }            }        }        @Override        public void onError(MediaCodec codec, MediaCodec.CodecException e) {            Log.i(LOG_TAG, "Error: " + e);        }        @Override        public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {            Log.i(LOG_TAG, "decoder output format changed: " + format);        }    }    private class EncoderCallback extends MediaCodec.Callback {        @Override        public void onInputBufferAvailable(MediaCodec codec, int index) {            Log.i(LOG_TAG, " входные буфера готовы" );        }        @Override        public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo info) {            outPutByteBuffer = encoder.getOutputBuffer(index);            byte[] outDate = new byte[info.size];            outPutByteBuffer.get(outDate);            try {                //  Log.i(LOG_TAG, " outDate.length : " + outDate.length);                DatagramPacket packet = new DatagramPacket(outDate, outDate.length, address, port);                udpSocket.send(packet);            } catch (IOException e) {                Log.i(LOG_TAG, " не отправился UDP пакет");            }            encoder.releaseOutputBuffer(index, false);        }        @Override        public void onError(MediaCodec codec, MediaCodec.CodecException e) {            Log.i(LOG_TAG, "Error: " + e);        }        @Override        public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {            //  Log.i(LOG_TAG, "encoder output format changed: " + format);        }    }    @Override    public void onPause() {        if (myCameras[CAMERA1].isOpen()) {            myCameras[CAMERA1].closeCamera();        }        stopBackgroundThread();        super.onPause();    }    @Override    public void onResume() {        super.onResume();        startBackgroundThread();    }    public class Udp_recipient extends Thread {        Udp_recipient() {            start();            //    Log.i(LOG_TAG, "запустили прием данных по udp");        }        public void run() {            while (true) {                try {                    byte buffer[] = new byte[50000];                    DatagramPacket p = new DatagramPacket(buffer, buffer.length);                    udpSocketIn.receive(p);                    byte bBuffer[] = p.getData();                    outDataForEncoder = new byte[p.getLength()];                    synchronized (outDataForEncoder) {                        for (int i = 0; i < outDataForEncoder.length; i++) {                            outDataForEncoder[i] = bBuffer[i];                        }                    }                    mNewFrame = true;                    synchronized (out)                    {out.write(outDataForEncoder);}                    synchronized (out2)                    {out2.write(outDataForEncoder);}                } catch (Exception e) {                    Log.i(LOG_TAG, e + "hggh ");                }            }        }    }}       



Теперь в таргет адресе первого смартофона:

String ip_address = " допустим 192.168.50.131";

указываем адрес его визави и наоборот. Запускаем приложения, надеваем VR-очки и вперёд, двигать науку нейробиологию!
Подробнее..

Первая попытка собрать лазерный проектор из жестких дисков

21.06.2020 02:15:26 | Автор: admin

Всем привет!
Сегодня публикую короткую статью о том, как я из старых жестких дисков собирал лазерный проектор. Который получился не очень интересным, а вот визуализатор звука, на удивление вышел потрясающим.
Если Вам интересны подробности, то смотрите видео или читайте статью под катом.

У меня в хламе давно валялись и занимали место, два неисправных жестких диска от известного бренда WD. И вот настало время воплотить свою старую идею в жизнь, попробовать собрать лазерный проектор из них. Так как считывающая головка HDD это есть ничто иное как гальванометр и она как раз идеально подойдет для перемещения зеркала в заданную позицию одной оси.
Вооружившись инструментом приступаю к выполнению задуманной цели.


Для этого я сначала разбираю жесткие диски.


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


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


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


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


Выпиливаю зеркало из алюминиевого диска, снятого с этого же HDD. Его размер получился 35 x 10 мм. Приклеиваю эти зеркала цианакрилатным клеем к гальванометру. При вклеивании учтите, что клей схватывается моментально. Лучше сначала установить зеркало в паз, а потом капнуть в место соединения каплю клея.


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


После того как собраны 2 гальванометра, я их соединяю на уголки от оконных рам, под углом 90. Для этого пришлось просверлить по 2 отверстия в алюминиевом основании и закрепить к ним эти уголки

Механическая часть проектора собрана, теперь подключаем его электромагниты к двухканальному усилителю мощности PAM8610. Мне быстрее хотелось проверить мое устройство в деле и я решил подать на вход усилителей музыку. И о чудо! Я увидел красивое зрелище, словно в одно мгновения я очутился в ночном клубе или на концерте своей любимой рок группы. Получилась крутая лазерная цветомузыка. Она рисует невероятные картинки в такт с музыкой, на которые можно смотреть не отрываясь. Что самое удивительное, то что получаемые изображения на столько разнообразные и не повторяются от трека к треку. Наибольшую разницу можно заметить в разных музыкальных жанрах. Посмотрев видео, Вы можете в этом наглядно убедиться.

Теперь пробую включить с бесплатным софтом в режиме проектора и тут меня ждало разочарование. Без постоянной составлявшей с выхода звуковой карты невозможно отобразить хороших изображений или анимацию. Так как через разделительные конденсаторы все углы округляются, не возможно удерживать гальванометр в одном положении и он сразу стремится вернуться в среднее положение. Также гальванометрам требуется обратная связь, для удержания нулевой точки. И еще нужно обеспечивать гашения луча, то есть отключать лазер на момент возврата в начальное положение. Для переделки потребуется USB звуковая карта, как минимум на 4 канала. Внешней звуковой карты у меня пока нет и я решил продолжать свои эксперименты после того как она ко мне приедет.

Редактор ild файлов
Проигрыватель ild файлов и анимации

На этом я временно приостанавливаю проект, до получения нужных компонентов.
Так как в лазерных проекторах я не специалист, то жду от Вас помощи в виде консультаций.
Спасибо, что дочитали мою статью. Если у Вас остались вопросы, я с удовольствием на них отвечу.
Подробнее..

Гены Ардуинщика

21.06.2020 16:16:20 | Автор: admin


В очередной раз, при обдумывании самоделки на Atmega, встал вопрос проектирования соединений. В моем случае 12 внешних коннекторов и 21 связь. Расположение, соединение, пересечения, программирование, ошибки, ошибки, ошибки.

При кажущейся простоте задачи поломать мозг придется день, а то и два. Без опыта месяц.
И лень взяла свое.

Как это делается


Обычный алгоритм в таком случае:

  • используем макетную плату (самодельную или Arduino) не меняя или добавляя разъемы и разводку
  • подгоняем под макетку внешние коннекторы, чтобы не было пересечений
  • пишем программный код для настройки контроллера и макросы-переменные.

А что если последовательность пинов в коннекторах уже жестко заданы?

Как например у драйвера L298N. Пересечения проводов или дорожек сильно усложнят проектирование, сборку и эксплуатацию.

Попробуем решить задачу с помощью генетического алгоритма. И для начала определимся с моделью.

Список разъемов


  • Шина i2c для компаса I2C(SDA, SCL)
  • UART для связи с внешним миром UART(RXD, TXD)
  • Ультразвуковой сенсор SONAR1(Echo, Trig)
  • Управление маршевыми двигателями DRIVE(ENA, IN1, IN2, IN3, IN4, ENB) с помощью L298N длинный коннектор как раз для шлейфа
  • Энкодер на колесе:
  • левый ENCODER_L(IN)
  • правый ENCODER_R(IN)
  • Сенсор-выключатель впереди робота:
  • левый SENS_L(IN)
  • правый SENS_R(IN)
  • Включение питания Мозга (OPI PC) CPU(EN)
  • Включение турбины VAC_CLEAN(EN)
  • Включение веника BROOM(EN)
  • Напряжение батареи и потребляемый ток PWR(LVL, CUR).

Модель


Получается ген длиной 21 хромосом, которые отображают пины микроконтроллера, а точнее их соединения с коннекторами.

VCC и GND пины коннекторов игнорируем, так как шины мы можем вынести за коннекторы.
Каждая ячейка может иметь значение от 1 до 32 (количество лап у микросхемы). Значения в гене не могут повторяться.

Не допускается пересечений проводников (соединения делаются последовательно, если следующий пин занят проскакиваем далее)

Количество вариантов соединений:

$32^{21}=$ 40564819207303340847894502572032.

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

Ускоряемся


Для уменьшения пространства поиска используем функции пина коннектора (ADC, INT, PWM, PCINT). Например, если пин может быть только ADC, то вести к нему линию PWM или дискретного входа бессмысленно.

Данный фильтр уменьшает количество вариантов до 8 748 869 014 201 881 088. Разница ощутима. Но миллиарды миллиардов вариантов это тоже много.

Так же ранее использовались ручные эмпирические правила:

  • начинаем процесс соединения с уникальных пинов (SDA, SCL, RXD, TXD)
  • после соединяем разъемы с большим количеством пинов
  • последними соединяем пины с более общим спектром функций.

Но волшебный Collections.shuffle задающий последовательность коннекторов для обработки, решает эту задачу успешно на автомате.

Решение


Запускаем алгоритм и получаем решение для Atmega328p TQFP32. У меня на бюджетном ноутбуке находит менее чем за минуту.

1 SONAR1.Echo
2 SONAR1.Trig
9 DRIVE.ENB
10 DRIVE.IN4
11 DRIVE.IN3
12 DRIVE.IN2
13 DRIVE.IN1
14 DRIVE.ENA
15 ENCODER_L.IN
16 VAC_CLEAN.EN
17 CPU.EN
22 PWR.LVL
23 PWR.CUR
24 SENS_R.IN
25 SENS_L.IN
26 ENCODER_R.IN
27 I2C.SDA
28 I2C.SCL
30 UART.RXD
31 UART.TXD
32 BROOM.EN

Алгоритм находит решение за пару тысяч эпох. Иногда не находит и за миллион. В таком случаем просто надо перезапустить программу, потому что инициализация происходит случайно.
Без фильтра по функциям пинов алгоритм так же находит решение. Правда за 74 минуты и 13 перезапусков алгоритма по миллиону эпох на каждый. Перед каждым запуском делаем shuffle последовательности соединений.

Остается только соединить проводами макетку или нарисовать дорожки на плате с микроконтроллером.

Детали


Чтобы описать всё, надо будет написать не одну статью. Я постарался комментировать непонятные и самые интересные моменты в java-коде.

Желающие углубиться в тему могут заглянуть в git-проекта.
Подробнее..

Ламповый фонокорректор

22.06.2020 08:06:00 | Автор: admin


Сборка фонокорректора.

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

Размещаю пост, в основном, чтобы фото процесса не потерялись и может кому-нибудь будет интересен такой корпус.

Схема:



Разведено все на двух печатных платах. Для противников печатных плат: можно сошлифовать дорожки, использовать плату с уже готовыми отверстиями, с шелкографией, и с пайкой на скрутках.
Трансформатор заказывал в тортранс по характеристикам 220/130v 0.03A/22v 0.5A. Остальные радиодетали купить не проблема.







Корпус из нержавеющей стали толщиной 3мм. Детали вырезаны лазерной резкой.







Потом все равно пришлось обращаться к профессиональным сварщикам для устранения дефектов







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



Отверстие в середине с резьбой для крепления трансформатора. Потом было заварено и отшлифовано.





Снаружи после отполировал в зеркало, было нелегко после пескоструя.





Передняя панель осталась с одной стороны матовая.



Также поэкспериментировал со стеклом для передней панели









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









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

Файлы для резки нужно немного откорректировать, сейчас понимаю, что можно сделать намного проще. Целесообразности в экране скорее всего нет, корпус следовало сделать немного длиннее, чтобы от ламп до трансформатора было 20см. Цветок наверно можно убрать. И нужно пересмотреть крепление для передней панели, пришлось для надежности сверлить отверстия.





Для меня это был первый и наверно последний фонокорректор. Первый опыт с витражным стеклом, сваркой и с полировкой нержавейки. Возможно, если появится свободное время, займусь ламповым усилителем в таком дизайне, но это не точно).
Результатом в целом доволен, но затрат не думаю что стоит. insta
И еще несколько фото.



















Подробнее..
Категории: Diy или сделай сам

Перевод Беспроводной телефон из консервных банок

22.06.2020 10:10:48 | Автор: admin

Новый подход к старой игрушке беспроводной телефон из консервных банок берёт прошлогоднюю технологию и впихивает её в современность!


image

Буквально вчера я вёл серьёзный телефонный разговор, как вдруг мой бананофон перестал работать! Я очень расстроился. Ну, всё я последний раз пропускаю звонок из-за этого дурацкого телефона! (Оглядываясь назад, стоит признать, что я в тот момент был, возможно, слишком сильно зол).



Пришло время обновлений. И вот он новый беспроводной телефон из консервной банки! Новый, улучшенный псевдотелефон, подходящий для всех моих коммуникационных нужд!





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

Инструменты и материалы



Для проекта вам потребуется довольно много электронных компонентов и парочка инструментов.





Инструменты:
  • Дрель.
  • Ножницы по металлу.
  • Пистолет для термоклея.
  • Круглогубцы.
  • Молоток с круглым бойком.


Материалы (всё в двух экземплярах):


Готовим банки


Перед подключением электроники нужно подготовить банки. Просверлим в них по два отверстия одно для антенны, второе для кнопки.



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

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

У меня получилось 5,5 мм.

ОК, надеваем защитные очки!


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

После этого можно приступать к отверстию для кнопки. С ним всё немного по-другому.

Я работаю с тем, что есть, поэтому решил попробовать снова сделать отверстие при помощи дрели и круглогубцев. Но гораздо удобнее было бы делать это при помощи сверла Форстнера. Вот, как я это сделал.

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

После этого я при помощи молотка и круглогубцев забил края внутрь и загнул их см. фото. Рекомендую использовать молоток с круглым бойком. Я использовал обычный, поскольку другого нет.









Теперь можно вкрутить антенну и кнопку. Остерегайтесь острых краёв металла!

Время термоклея




Пора вклеивать все компоненты. Сначала включите клеевой пистолет и дайте разогреться. Потом используйте клей, чтобы приклеить антенную плату к банке. Также советую покрыть клеем металлическую часть антенны, торчащую наружу, чтобы её не закоротило с банкой.



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

Приклейте Arduino Uno к донышку банки, а потом подсоедините батарейки. Это самая сложная часть рекомендую нанести клей на края, а потом разместить так, чтобы антенна смотрела вверх, а батарейки были в противоположной части банки. Батарейки будут естественным центром тяжести.



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

Подключаем электронику




Когда всё прочно приклеено, пора подключать провода. Соедините проводами компоненты по схеме. Ниже привожу список подключаемых контактов.

Антенная плата:
  • MI -> MISO
  • MO -> MOSI
  • SCK -> SCK
  • CE -> Pin 7
  • CSE -> Pin 8
  • GND -> GND
  • 5V -> 5V


Комментарий: NRF24L01 отличная штука, только уж очень чувствительна к питанию. Подключайте её только к 3,3 В если не используете дополнительную плату, как я. К 5 В подключайте только с дополнительной платой, иначе спалите антенну.

Аналоговый звуковой датчик:
  • Gravity Pins -> A0


Аудио усилитель:
  • + (вход динамика) -> 9 или 10 (левый или правый канал)
  • (вход динамика) -> GND
  • Gravity pins -> D0


Переключатель:
  • NO -> A1
  • COM -> GND


Краткое пояснение работы схемы.

Мы используем библиотеку RF24Audio, поэтому микрофон, динамик, выключатель и антенну нужно подключать строго определённым образом:
  • Сигнальный контакт микрофона всегда идёт на контакт A0.
  • Переключатель (приём/передача) на А1.
  • Аудио усилитель включается куда угодно, главное, чтобы у него было питание. Кабель для передачи аудио нужно подключать к контактами 9 и 10.
  • Контакты антенны CE и CSE подключаются только к контактам 7 и 8.




Закачиваем код


Благодаря библиотеке RF24Audio программа получается крайне простой. Буквально 10 строк кода. Взгляните:

    //Include Libraries    #include <RF24.h>    #include <SPI.h>    #include <RF24Audio.h>    RF24 radio(7,8);    // Радио использует контакты 7 (CE), 8 (CS).    RF24Audio rfAudio(radio,1); // Аудио использует радио, номер радио назначить 0.          void setup() {        rfAudio.begin();    // Инициализировать библиотеку.    }


Чтобы закачать код, нужно установить Arduino IDE, скачать данный код и открыть его. Убедитесь, что в меню инструменты программатор установлен на AVR ISP, а плата на Arduino UNO. Убедитесь, что вы выбрали правильный COM-порт.

Теперь соедините Arduino и компьютер USB-кабелем, и нажмите кнопку загрузить слева вверху. Код должен загрузиться и вы должны услышать негромкое жужжание.

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

Если всё так, то программа работает и всё подсоединено правильно.

Испытания банок




Для проверки нужно включить обе банки. Зажмите кнопку на одной из банок и скажите что-нибудь в микрофон. Слышите ли вы звук из другой банки? Попробуйте проделать это с другой банкой.

Если звук проходит, то у вас всё получилось! Если у вас проблемы с помехами или вы слышите жужжание, проверьте на наличие проблем с заземлением. Могу порекомендовать обмотать антенну изоляционной лентой.

После этого испытайте дальность работы если на пути сигнала ничего нет, он должен проходить на расстояние порядка километра!

Заключение






Поздравляю, вы добрались до конца проекта! Отличная работа!
Подробнее..

Как я делал себе АВР для генератора

22.06.2020 22:22:51 | Автор: admin


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

О заземлении


Прежде чем что-либо делать с электричеством, нужно позаботиться о наличии хорошего заземления в вашем доме. Просто так взять и подключить обычный бытовой бензиновый/дизельный/газовый генератор к электросети дома не получится. Нужно соблюдать меры предосторожности. Первая из них ваш генератор должен быть хорошо заземлен. Тогда у вас есть хорошие шансы не получить удар током, когда статика от вашего любимого свитера пробъёт изоляцию обмотки генератора. Вообще, к работающему генератору не стоит без нужды прикасаться.

Стоит помнить, что в сети не всегда 220В. Коммутация на линиях, грозовые разряды вдалеке, статические разряды дают такие наводки, что в сети нередки короткие импульсы в несколько киловольт. С этим борются установкой разрядников и УЗИП на вводе в дом, но это очень редкая практика в РФ. Так что пусть искра в землю уходит, и не через вас сделайте по всему дому хорошее заземление. Без этого делать что-либо дальше просто нельзя!

О генераторах


К слову, у многих бытовых бензиновых генераторов обмотки никак не соединены с землёй. И это вполне нормально, когда вы питаете от генератора один электроинструмент. Но когда вам надо подключить генератор к дому, нужно сделать нулевой провод (N) и провод фазы (L). Для этого один из выводов генератора заземляется и из этой точки заземления уже независимо нужно вести в дом два провода один будет нейтралью N, а второй защитным заземлением (PE). При выборе генератора нужно обратить внимание, можно ли заземлять его выход, порой это запрещено в инструкции к генератору, тогда такой генератор вам не подойдёт.

Часто в Сети можно увидеть схемы подключения генератора без заземления и разделения линий N и PE. Не делайте так, дольше проживёте. Такие схемы хорошо работают до первого неудачного стечения обстоятельств. В типичных блоках питания современных электронных приборов стоят конденсаторы с линий L, N на землю. Если N не заземлить у генератора, то за счёт этих конденсаторов на линии N будет, если повезёт, 110 вольт относительно земли. Кстати, многие газовые котлы в таком режиме вообще перестают работать. Про влияние статики без присутствия заземления я уже писал выше.

О схемах АВР


Есть несколько разных схем реализации АВР. Дальше я буду писать о наиболее безопасной с моей точки зрения схеме однофазного АВР. Я не советую экономно делать АВР на одном контакторе или же с коммутацией только одного фазного провода. Только вместе с нейтралью.



На приведенной схеме питание от сети и от генератора подаётся через вводы 1 и 2. Они защищены спаренными автоматами. Через дополнительные автоматы питаются схемы коммутации и индикации. Видно, что катушки реле взаимно блокируются электрически. За включение того или иного ввода отвечает для упрощения не показанный на схеме микроконтроллер, который замыкает цепи в точке коммутации ТК1 или ТК2.

Принципиальным моментом является наличие в АВР 2х схем блокировок взаимной механической блокировки коммутирующих вводы контакторов и взаимной электрической блокировки контакторов. Самодельщики ради экономии, бывает, в своих конструкциях пренебрегают этими блокировками, а зря. Схема без блокировок может проработать некоторое время, но в какой-то момент контакты пригорят, возвратные пружины ослабнут и случится КЗ между вводами. Во-первых, это грозит большим бабахом, если обе линии окажутся под напряжением, но это не самая большая проблема. Гораздо важнее, что ваш генератор неожиданно для ремонтирующих проводку электриков может выдать в общую сеть напряжение при неблагоприятном стечении обстоятельств ремонтирующие линию электрики могут погибнуть. Для вас это уже уголовная статья.

О контакторах


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

Чтобы сделать механическую блокировку, нужно выбрать контакторы, которые могут работать в паре. Обычно взаимная блокировка достигается установкой одинаковых контакторов рядом друг с другом и установкой дополнительной опции механического блокиратора. Он продаётся отдельно от контакторов и стоит копейки.

Взаимная электрическая блокировка возможна, если на контакторе есть дополнительные сигнальные контакты, работающие на размыкание. Иногда они сразу встроены в контактор, иногда их можно докупить и установить как опцию.

Ведущие производители контакторов имеют в своих линейках такое оборудование. Так что найти и купить комплект не представляет особого труда. Правда цены на брендовые контакторы на порядок выше наших/китайских. Поскольку количество циклов коммутации не ожидается большим, то выбор китайских контакторов вполне оправдан. К недостаткам можно отнести только то, что катушки контактора во время работы довольно сильно гудят.

Еще по поводу коммутируемой мощности. Контакты контактора должны выдерживать максимальную мощность, которую вам разрешено потреблять в доме. У меня это 10 кВт, поэтому контакторы я выбирал на допустимый ток через один контакт примерно в 50 ампер. Стоит отметить, что по какой-то причине коммутируемая мощность для типичного трехфазного контактора указывается в паспорте суммарная для всех трёх фаз, поэтому надо внимательно смотреть, какой допустимый ток именно через один контакт.

О схеме управления


Когда я занимался созданием АВР у меня было несколько особых требований к его работе:

  • У меня не так часто отключают электричество, поэтому я решил, что мне не нужен автозапуск генератора, а вот от автоматической остановки генератора я решил не отказываться: когда сеть восстанавливается, генератор сам затихает и сразу понятно, что теперь с питанием всё хорошо, да и бензин экономится
  • После старта генератора ему надо дать время прогреться и только после прогрева давать ему нагрузку. Т.е. мне нужен был таймер включения АВР после подачи напряжения от генератора
  • После восстановления напряжения в сети часто происходили повторные отключения через короткий промежуток времени, поэтому мне нужен был таймер, который бы выждал перед переходом с генератора на сеть некоторое время и не глушил сразу генератор
  • Генератору, говорят, полезно перед выключением немного поработать без нагрузки. И для этого мне тоже нужен был таймер

Таким образом вырисовывалась картина, что мне нужен контроллер с несколькими таймерами. В те времена я увлекался кодингом на AVR, поэтому решил сделать такой контроллер на Atmega 8a.

Хорошо бы, чтоб контроллер работал долго и надёжно. Кроме того, чтобы сделать полную гальваническую развязку и снабдить контроллер сторожевым таймером я ничего более не придумал. Ну и сделать схему и программу максимально простыми. Поскольку делалось всё для себя, то все настройки и калибровки решил оставить в коде весь UI свелся к одному светодиоду )

Основная задача контроллера мониторить напряжение на вводах и, при необходимости, переключать вводы. При этом приоритетным является ввод от деревенской сети.

Тут стоит отметить, что качество сети таково, что колебания от 150 в до 250 в вполне обычное явление. Поэтому понятие что есть хорошее питание от сети очень размыто. Через какое-то время я решил эту проблему, когда поставил на весь дом один мощный тиристорный стаблизатор напряжения на 11 кВт. Но, важно, стабилизатор можно ставить только до АВР, а не после! Включать стабилизатор для генератора категорически не рекомендуется. Есть опасность, что при определенной комбинации нагрузок, особенно всяких мощных насосов, система из генератора и стабилизатора станет неустойчивой и войдет в автоколебания.

После некоторых раздумий нарисовал такую схему в Eagle.



В схеме есть два идентичных трансформаторных источника питания, при наличии напряжения на любом из вводов схема обеспечена питанием. Между вводами возможно напряжение в 600в, поэтому изоляция трансформаторов должна быть хорошей. Питание берется после пакетников QF3 и QF4 соответственно.

У каждого источника есть резистивный делитель напряжения, защищенный от перенапряжения стабилитроном с него производится путём нехитрых расчётов измерение напряжение сети с помощью АЦП микроконтроллера.

Для коммутации катушек контакторов применяется стандартная схема из даташита для управления семисторами. 2 штуки ). Катушки это индуктивная нагрузка, поэтому цепи снаббера на выходе из резистора и конденсатора обязательны.

У меня был релейный модуль с али, который используется для останова генератора. На схеме он просто прямоугольник с тремя выводами.

Из особенностей еще в качестве генератора опорного напряжения использован TL431. В остальном всё включено стандартно для Atmega 8. Есть светодиоды для индикации наличия напряжения питания на вводах и один светодиод статуса устройства. Тактируется схема с помощью внешнего кварца на 16 МГц.

Eagle мне породил вот такую печатную плату. Никаких SMD, симисторы и стабилизатор с легкими радиаторами.



Два тороидальных трансформатора установлены прямо на плате. Плату изготовил традиционным радиолюбительским способом с помощью фоторезиста. После монтажа покрыл тремя слоями акрилового лака. Надеюсь не пробьет его высокое напряжение.

О программе управления


Код программы довольно длинный, извините.

Код программы
/* * ABP - программа управления блоком "Автоматического ввода резерва" * В блоке управления есть два ввода напряжения от сети и генератора и три выхода- * один выход для управления контактором включения сети, второй - контактором * включения генератора и третий - реле запуска стартера генератора. * В блоке управления есть выход RS232 для отладочной информации, порт SPI для  * программирования микроконтроллера и 3 светодиода. Два зеленых светодиода  * показывают наличие напряжения питания на входах сети и генератора. Красный  * светодиод показывает состояние контроллера количеством вспышек. * * для нормальной печати напряжений нужно линковать большие библиотеки printf * дополнительные опции в линкере -Wl,-u,vfprintf -lprintf_flt */ #ifndef F_CPU #  define F_CPU 16000000UL #endif#define BAUD 9600#include <stdio.h>#include <stdlib.h>#include <avr/io.h>#include <util/delay.h>#include <avr/interrupt.h>#include <avr/wdt.h>#include <avr/sleep.h>// переменные для сохранения состояния контроллера после запуска// используются только для отладкиuint8_t mcusr_mirror __attribute__ ((section (".noinit")));void get_mcusr(void) \__attribute__((naked)) \__attribute__((section(".init3")));void get_mcusr(void){   mcusr_mirror = MCUSR;   MCUSR = 0;   wdt_disable();}//настройка UART для отладочной печати в порт RS232void uart_init( void ){/* //настройка скорости обмена   UBRRH = 0;   UBRRL = 103; //9600 при кварце 16 МГц */    #include <util/setbaud.h>    UBRRH = UBRRH_VALUE;    UBRRL = UBRRL_VALUE;       #if USE_2X      UCSRA |= (1 << U2X);    #else      UCSRA &= ~(1 << U2X);   #endif   //8 бит данных, 1 стоп бит, без контроля четности   UCSRC = ( 1 << URSEL ) | ( 1 << UCSZ1 ) | ( 1 << UCSZ0 );   //разрешить прием и передачу данных   UCSRB = ( 1 << TXEN ) | ( 1 <<RXEN );}int uart_putc(  char c, FILE *file ){   //ждем окончания передачи предыдущего байта   while( ( UCSRA & ( 1 << UDRE ) ) == 0 );   UDR = c;   wdt_reset();   return 0;}FILE uart_stream = FDEV_SETUP_STREAM( uart_putc, NULL, _FDEV_SETUP_WRITE );// настройка счетчика 1 для счета секунд - главный таймер в программеvoid timer1_init( void ){   TCCR1A = 0; // регистр настройки таймера 1 - ничего интересного   /* 16000000 / 1024 = 15625 Гц, режим СТС со сбросом 15625 должен давать прерывания раз в 1 сек */   // режим CTC, ICP1 interrupt sense (falling)(not used) + prescale /1024 + без подавления шума (not used)    TCCR1B = (0 << WGM13) | (1 << WGM12) | (0 << ICES1) | ((1 << CS12) | (0 << CS11) | (1 << CS10)) | (0 << ICNC1);    OCR1A = 15625;        // прерывание    TIMSK |= (1 << OCIE1A);}// описание состояния контакторовtypedef enum _ABP_RLY_STATES {   RLY_OFF = 0, // контактор выключен   RLY_ON // контактор включен} ABP_RLY_STATES;// перечень используемых в блоке релеtypedef enum _ABP_RLY {   RLY_220N = 0,   RLY_220G,   RLY_GEN} ABP_RLY;volatile ABP_RLY_STATES contactors[RLY_GEN+1]; // расчетные состояния контакторов// описание состяния софтовых таймеровtypedef enum _ABP_TMR_STATES {   TMR_OFF = 0, // таймер выключен   TMR_ON // таймер включен} ABP_TMR_STATES;// структура описывающая софтовый таймерtypedef struct {   ABP_TMR_STATES state; // состояние включения таймера   unsigned char passed_secs; // сколько секунд прошло (ограничение до 255!!!)   unsigned char set_secs; // установка срабатывания таймера в секундах} TMR_INSTANCE;// перечень используемых софтовых таймеровtypedef enum _ABP_TMRS {   TMR_220N_ON = 0,  // задержка включения контактора после появления 220 от сети   TMR_220G_ON, // задержка включения контактора после появления 220 от генератора   TMR_220N_OFF, // задержка выключения контактора после пропадания 220 в сети   TMR_220G_OFF, // задержка выключения контактора после пропадания 220 от генератора    TMR_PRINT, // задержка отладочной печати в последовательный порт   TMR_GEN_OFF // задержка выключения реле стартера генератора после появления 220 от сети} ABP_TMRS;volatile TMR_INSTANCE abp_timers[TMR_GEN_OFF+1]; // таймеры по перечню ABP_TMRSvoid abp_timers_init( void ) {   // время срабатывания таймеров в секундах   abp_timers[TMR_220N_ON].set_secs = 10; // ожидание после включения сетевого напряжения   abp_timers[TMR_220G_ON].set_secs = 60; // ожидание для переключения на генератор для прогрева генератора   abp_timers[TMR_220N_OFF].set_secs = 5; // ожидание после пропадания сетевого напряжения   abp_timers[TMR_220G_OFF].set_secs = 5; // ожидание после пропадания напряжения генератора   abp_timers[TMR_GEN_OFF].set_secs = 60; // ожидание для охлаждения генератора перед остановом   abp_timers[TMR_PRINT].set_secs = 2; // задержка печати      unsigned char i;   for(i=TMR_220N_ON; i<=TMR_GEN_OFF; i++ ) {      abp_timers[i].state = TMR_OFF;      abp_timers[i].passed_secs = 0;   }   for(i=RLY_220N; i<=RLY_GEN; i++ ) {      contactors[i] = RLY_OFF;   }}// запуск таймера void abp_timer_start( ABP_TMRS tmr ) {   abp_timers[tmr].passed_secs = 0;   abp_timers[tmr].state = TMR_ON;    }// остановка таймера void abp_timer_stop( ABP_TMRS tmr ) {   abp_timers[tmr].state = TMR_OFF;   abp_timers[tmr].passed_secs = 0;}// проверка срабатывания таймераunsigned char abp_timer_check( ABP_TMRS tmr ) {   if (abp_timers[tmr].passed_secs >= abp_timers[tmr].set_secs) {      return 1;    } else {      return 0;   }}// прерывание для подсчета секунд в таймерахISR(TIMER1_COMPA_vect){   // сюда надо добавлять переменные счетчиков таймеров включения/выключения   unsigned char i;   for(i=TMR_220N_ON; i<=TMR_GEN_OFF; i++ ) {      if (abp_timers[i].state) {         abp_timers[i].passed_secs++;         }   }}//настройка COUNTER2 для управления светодиодом через переменную led_Statevoid counter2_init( void ){   ASSR = 0;  /* AS0 = 0 */  /* disable asynchronous mode */   while (ASSR); /*EMPTY*/   OCR2 = 223;             /* 70 Гц на выходе */   TCCR2 |= (1 << CS22) | (1 << CS21) | (1 << CS20);  /* prescale /1024 */      TCCR2 |= (1 << WGM21);                /* mode CTC */   TCCR2 &= ~(1 << WGM20);      TCCR2 &= ~(1 << COM21);              /* не выводить на OC2 */   TCCR2 &= ~(1 << COM20);      TIMSK |= (1 << OCIE2);          /* enable compare interrupt */}typedef enum _ABP_LED_STATES {   ABP_UNDEF = 0, // режим неопределен   ABP_1RELAY, // включено 1 реле   ABP_2RELAY  // включено 2 реле} ABP_LED_STATES;ABP_LED_STATES led_State = ABP_UNDEF;const unsigned char led_pattern[3][10] ={ { 1,0,1,0,1,0,1,0,1,0 }, // статус не определен  { 1,0,0,0,0,0,0,0,0,0 }, // включено 1 реле  { 1,0,1,0,0,0,0,0,0,0 } }; // включено 2 релеvolatile unsigned char timer2_count = 0;         volatile unsigned char led_cycle = 0; // от 0 до 9ISR(TIMER2_COMP_vect)  // должно вызываться примерно 70 раз в секунду{   if (++timer2_count > 6)       // типа примерно через 0.1 сек. нужно сменить режим светодиода   {      timer2_count = 0;           // сбрасываем счетчик      if (led_pattern[led_State][led_cycle]) {         PORTB &= ~(1 << PB0); // включаем              } else {         PORTB |=  (1 << PB0); // выключаем      }      if (++led_cycle >= 10)         led_cycle = 0;   }  }// количество семплов для усреднения значения датчиков напряжения#define SAMPLES 2500// используемое опорное напряжение TL431#define REFERENCEV 2.479// экспериментальные коэффициенты пересчета для делителей напряжения#define DIVIDER1 (12.3/2.13)#define DIVIDER2 (12.4/2.03)double realV1 = 0; // здесь итоговое зхначение измерения V0double realV2 = 0; // здесь итоговое зхначение измерения V1volatile int sampleCount = 0;volatile unsigned long tempVoltage1 = 0; // переменные для накопления суммыvolatile unsigned long tempVoltage2 = 0;volatile unsigned long sumVoltage1 = 0; // переменные для передачи суммы семплов в основной цикл volatile unsigned long sumVoltage2 = 0;void ADC_init() // ADC1,0{   // внешний ИОН 2,5В, 10 bit преобразование   ADMUX = (0 << REFS0) | (0 << REFS1) | (0 << ADLAR) |   (0 << MUX3) | (0 << MUX2) | (0 << MUX1) | (0 << MUX0); // ADC0   // включить, free running, с прерываниями   ADCSRA = (1 << ADEN) | (1 << ADFR) | (1 << ADIE) |   (1 << ADPS2) | (1 << ADPS1) | (1 << ADPS0); // делитель 128      ADCSRA |= (1 << ADSC);             // Start ADC Conversion}ISR(ADC_vect) // должен накапливать измерения по 2500 семплам по каждому каналу{   if ((ADMUX & (1 << MUX0))) { // если работаем с ADC1      if (sampleCount++) // пропускаем первое измерение         tempVoltage1 += ADC;      if (sampleCount >= SAMPLES) {         sampleCount = 0;         sumVoltage1 = tempVoltage1;         tempVoltage2 = 0;         tempVoltage1 = 0;         ADMUX &= ~(1 << MUX0); // переключаем на ADC0      }           } else { // если работаем с ADC0      if (sampleCount++) // пропускаем первое измерение         tempVoltage2 += ADC;      if (sampleCount >= SAMPLES) {         sampleCount = 0;         sumVoltage2 = tempVoltage2;         tempVoltage2 = 0;         tempVoltage1 = 0;         ADMUX |= (1 << MUX0); // переключаем на ADC1      }     }   ADCSRA |=(1 << ADIF);              // Acknowledge the ADC Interrupt Flag}// валидность напряжения на входах блока АВРtypedef enum _ABP_U_STATES {   U_INVALID = 0, // напряжение не в норме   U_VALID // напряжение в норме} ABP_U_STATES;ABP_U_STATES u220n, u220g; // расчетная валидность напряжения на входах// допустимый диапазон напряжений питания в В на выходе выпрямителей// напряжения меняются не только от изменения сетевого напряжения, но и // плавают под нагрузкой (реле стартера), поэтому диапазон широкий#define MAX_V 14.0#define MIN_V 7.5void ports_init() {   // настройка порта светодиода индикации   PORTB &= ~(1 << PB0);   DDRB  |=  (1 << PB0); // output   PORTB &= ~(1 << PB0); // включаем      // настройка порта Контактора 1   PORTD |=  (1 << PD4);   DDRD  |=  (1 << PD4); // output   PORTD |=  (1 << PD4); // высокий уровень - выключаем   // настройка порта Контактора 2   PORTD |=  (1 << PD3);   DDRD  |=  (1 << PD3); // output   PORTD |=  (1 << PD3); // высокий уровень - выключаем   // настройка порта Реле запуска генератора   PORTD &= ~(1 << PD2);   DDRD  |=  (1 << PD2); // output   PORTD &= ~(1 << PD2); // низкий уровень - выключаем}void validate220() {   // логика валидации напряжения питания сети и генератора   realV1 = DIVIDER1 * ((sumVoltage1 * REFERENCEV) / 1024) / SAMPLES;   realV2 = DIVIDER2 * ((sumVoltage2 * REFERENCEV) / 1024) / SAMPLES;      if( realV1 > MAX_V || realV1 < MIN_V ) { // проверка напряжения от генератора      u220g = U_INVALID;      } else {      u220g = U_VALID;   }   if( realV2 > MAX_V || realV2 < MIN_V ) { // проверка напряжения от сети      u220n = U_INVALID;      } else {      u220n = U_VALID;   }}void validate_contactors() {      // проверка валидности включения контактора сети 220 в      // на выходе RLY_220N = RLY_ON или RLY_OFF      if( u220n == U_VALID && contactors[RLY_220N] == RLY_OFF      && abp_timers[TMR_220N_ON].state == TMR_OFF) { // есть напряжение, реле пока выкл, и таймер не вкл         abp_timer_stop( TMR_220N_OFF ); // остановка таймера выключения 220         abp_timer_start( TMR_220N_ON ); // запуск таймера включения 220      }      if( u220n == U_VALID && contactors[RLY_220N] == RLY_OFF      && abp_timers[TMR_220N_ON].state == TMR_ON && abp_timer_check(TMR_220N_ON)) { // есть напряжение, реле пока выкл, таймер сработал         abp_timer_stop( TMR_220N_OFF ); // остановка таймера выключения 220         abp_timer_stop( TMR_220N_ON ); // остановка таймера включения 220         contactors[RLY_220N] = RLY_ON; // ставим флаг включения 220      }      if( u220n == U_VALID && contactors[RLY_220N] == RLY_ON      && abp_timers[TMR_220N_OFF].state == TMR_ON) { // есть напряжение, реле вкл, и таймер выкл включен         abp_timer_stop( TMR_220N_OFF ); // остановка таймера выключения 220      }      if( u220n == U_INVALID && contactors[RLY_220N] == RLY_ON      && abp_timers[TMR_220N_OFF].state == TMR_OFF) { // нет напряжение, реле пока выкл, и таймер не вкл         abp_timer_stop( TMR_220N_ON ); // остановка таймера включения 220         abp_timer_start( TMR_220N_OFF ); // запуск таймера выключения 220      }      if( u220n == U_INVALID && contactors[RLY_220N] == RLY_ON      && abp_timers[TMR_220N_OFF].state == TMR_ON && abp_timer_check(TMR_220N_OFF)) { // нет напряжения, реле пока вкл, таймер сработал         abp_timer_stop( TMR_220N_OFF ); // остановка таймера выключения 220         abp_timer_stop( TMR_220N_ON ); // остановка таймера включения 220         contactors[RLY_220N] = RLY_OFF; // ставим флаг выключения 220      }      if( u220n == U_INVALID && contactors[RLY_220N] == RLY_OFF      && abp_timers[TMR_220N_ON].state == TMR_ON) { // нет напряжения, реле выкл, и таймер вкл включен         abp_timer_stop( TMR_220N_ON ); // остановка таймера включения 220      }            // проверка валидности включения контактора генератора      // на выходе RLY_220G = RLY_ON или RLY_OFF      if( u220g == U_VALID && contactors[RLY_220G] == RLY_OFF      && abp_timers[TMR_220G_ON].state == TMR_OFF) { // есть напряжение, реле пока выкл, и таймер не вкл         abp_timer_stop( TMR_220G_OFF ); // остановка таймера выключения ген         abp_timer_start( TMR_220G_ON ); // запуск таймера включения ген      }      if( u220g == U_VALID && contactors[RLY_220G] == RLY_OFF      && abp_timers[TMR_220G_ON].state == TMR_ON && abp_timer_check(TMR_220G_ON)) { // есть напряжение, реле пока выкл, таймер сработал         abp_timer_stop( TMR_220G_OFF ); // остановка таймера выключения ген         abp_timer_stop( TMR_220G_ON ); // остановка таймера включения ген         contactors[RLY_220G] = RLY_ON; // ставим флаг включения ген      }      if( u220g == U_VALID && contactors[RLY_220G] == RLY_ON      && abp_timers[TMR_220G_OFF].state == TMR_ON) { // есть напряжение, реле вкл, и таймер выкл включен         abp_timer_stop( TMR_220G_OFF ); // остановка таймера выключения ген      }      if( u220g == U_INVALID && contactors[RLY_220G] == RLY_ON      && abp_timers[TMR_220G_OFF].state == TMR_OFF) { // нет напряжение, реле пока выкл, и таймер не вкл         abp_timer_stop( TMR_220G_ON ); // остановка таймера включения ген         abp_timer_start( TMR_220G_OFF ); // запуск таймера выключения ген      }      if( u220g == U_INVALID && contactors[RLY_220G] == RLY_ON      && abp_timers[TMR_220G_OFF].state == TMR_ON && abp_timer_check(TMR_220G_OFF)) { // нет напряжения, реле пока вкл, таймер сработал         abp_timer_stop( TMR_220G_OFF ); // остановка таймера выключения ген         abp_timer_stop( TMR_220G_ON ); // остановка таймера включения ген         contactors[RLY_220G] = RLY_OFF; // ставим флаг выключения ген      }      if( u220g == U_INVALID && contactors[RLY_220G] == RLY_OFF      && abp_timers[TMR_220G_ON].state == TMR_ON) { // нет напряжения, реле выкл, и таймер вкл включен         abp_timer_stop( TMR_220G_ON ); // остановка таймера включения ген      }            // запуск и останов генератора с таймером      if( contactors[RLY_220N] == RLY_OFF && contactors[RLY_GEN] == RLY_OFF) { // нет сети, стартуем ген         abp_timer_stop( TMR_GEN_OFF ); // остановка таймера останова генератора         contactors[RLY_GEN] = RLY_ON; // ставим флаг запуска генератора      }      if( contactors[RLY_220N] == RLY_ON && abp_timers[TMR_GEN_OFF].state == TMR_OFF       && u220g == U_VALID) { // есть 220 в сети и есть от генератора         abp_timer_start( TMR_GEN_OFF ); // запуск таймера останова генератора      }      if( contactors[RLY_220N] == RLY_ON && abp_timers[TMR_GEN_OFF].state == TMR_OFF      && u220g == U_INVALID) { // есть 220 в сети и нет от генератора         abp_timer_stop( TMR_GEN_OFF ); // остановка таймера останова генератора         contactors[RLY_GEN] = RLY_OFF; // ставим флаг останова генератора      }      if( contactors[RLY_220N] == RLY_ON && abp_timers[TMR_GEN_OFF].state == TMR_ON      &&  abp_timer_check(TMR_GEN_OFF) ) { // есть 220 в сети и истекло время таймера         abp_timer_stop( TMR_GEN_OFF ); // остановка таймера останова генератора         contactors[RLY_GEN] = RLY_OFF; // ставим флаг останова генератора      }}void switch_contactors() {   // логика переключения контакторов   if( contactors[RLY_220N] == RLY_ON ) {      PORTD |=  (1 << PD4); // высокий уровень - выключаем контактор 1 ген       _delay_ms(50); //даем возможность отключиться контактору генератора        PORTD &= ~(1 << PD3); // низкий уровень - включаем контактор 2 220      led_State = ABP_2RELAY;   } else {      if( contactors[RLY_220G] == RLY_ON ) {         PORTD |=  (1 << PD3); // высокий уровень - выключаем контактор 2 220         PORTD &= ~(1 << PD4); // низкий уровень - включаем контактор 1 ген         led_State = ABP_1RELAY;      } else {         PORTD |=  (1 << PD3); // высокий уровень - выключаем контактор 2 220         PORTD |=  (1 << PD4); // высокий уровень - выключаем контактор 1 ген         led_State = ABP_UNDEF;      }   }   if( contactors[RLY_GEN] != RLY_ON ) { // v2 реле работает на время работы генератора - не дает напряжения на стоппер      PORTD &= ~(1 << PD2); // низкий уровень - выключаем реле - возврат напряги на стоппер - стоп гены   } else {      PORTD |= (1 << PD2); // высокий уровень - включаем реле - держим режим работы гены   }}int main(void){   ports_init(); // настройка портов светодиода, реле и контакторов      uart_init(); //настройка uart   stdout =  &uart_stream;      counter2_init(); // настройка таймера мигания светодиода   ADC_init(); // настройка АЦП измерения 220   abp_timers_init(); // настройка таймеров задержек включения и выключения   timer1_init(); // настройка секундного таймера на аппаратном счетчике 1    set_sleep_mode(SLEEP_MODE_IDLE);  // разрешаем сон в режиме IDLE    sleep_enable();    wdt_enable(WDTO_2S); // Сторожевой таймер настроен на таймаут в 2 секунды   sei(); // запускаем работу прерываний      _delay_ms(1000); // ждем первых результатов ЦАП, отличных от 0   wdt_reset();   printf( "Start flag after reset = %u\r\n", mcusr_mirror );   abp_timer_start(TMR_PRINT);       while(1)    {      wdt_reset(); // сбрасываем сторожевой таймер            validate220(); // проверка качества 220 от сети и генератора         validate_contactors(); // валидация возможности включения контакторов с таймерами            switch_contactors(); // переключение контакторов по схеме            // отладочная печать раз в 2 секунды      if (abp_timer_check(TMR_PRINT)) {         printf( "V220 = %4.2f VG = %4.2f\r\n", realV2, realV1 );         printf( "valid = %u %u \r\n", u220n, u220g );         printf( "rly = %u %u %u\r\n", contactors[RLY_220N], contactors[RLY_220G], contactors[RLY_GEN] );         abp_timer_start(TMR_PRINT);      }              sleep_cpu(); // заснуть до следующего пррывания по таймерам    }}



Программа разработана с помощью бесплатного AVR Studio и использует стандартные библиотеки AVR.

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

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

Все циклы измерений сделаны на прерываниях и с использованием аппаратных таймеров. Счетчик секунд сделан на таймере 1. По прерыванию таймера 1 обновляются программные таймеры, отвечающие за задержки включения и отключения контакторов и реле генератора.
Второй таймер используется для создания эффекта мигания светодиода статуса. Предусмотрено три паттерна мигания. Значения из паттерна мигания берутся в прерывании таймера 2. По миганию можно судить о состоянии контроллера.

Два АЦП также работают по таймерам и усредняют по 2500 сэмплов измерений напряжения. Для перевода измерений в реальные вольты предусмотрены калибровочные константы. Их значения надо исправить в ходе настройки АВР.

Кроме того, есть еще ряд констант, которые нужно определить в ходе наладки.
abp_timers[TMR_220N_ON].set_secs = 10; // ожидание после включения сетевого напряженияabp_timers[TMR_220G_ON].set_secs = 60; // ожидание для переключения на генератор для прогрева генератораabp_timers[TMR_220N_OFF].set_secs = 5; // ожидание после пропадания сетевого напряженияabp_timers[TMR_220G_OFF].set_secs = 5; // ожидание после пропадания напряжения генератораabp_timers[TMR_GEN_OFF].set_secs = 60; // ожидание для охлаждения генератора перед остановом


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

Если кто-то надумает повторить АВР, то стоит подкорректировать значения настроек. Готовую прошивку не публикую, так как программу всё равно надо править в ходе настройки АВР.

Надо сказать, что мой АВР работает уже 4 года без проблем, так что схема можно считать проверенная как и код.
Подробнее..

Перевод Изготовление контроллера терминала IBM 3270

23.06.2020 10:15:03 | Автор: admin

IBM 3270 это терминал компьютерного мейнфрейма. Я давно восхищаюсь мейнфреймами от IBM, и особенно этими терминалами. У ранних моделей, 3278 и 3279, была уникальная эстетика, а их блочная система работы заметно отличается от той, которой пользовались вездесущие терминалы серии VT.



Терминал IBM 3278-2 и его пра-пра-правнук, ThinkPad X1, на котором запущен оес. Терминал подсоединён к современному z14 при помощи TN3270.

Не один я хотел попробовать подсоединить настоящий терминал от IBM к эмулятору Hercules [программный эмулятор мейнфреймов IBM (System/370, System/390, zSeries/System z) и совместимых (Amdahl)]. К сожалению, довольно трудно найти контроллер терминала IBM 3174 с интерфейсом Ethernet или Token Ring, версией софта с поддержкой TCP/IP и в рабочем состоянии. К тому же они огромные, шумные, и их сложно обслуживать, поскольку софт на них приходится загружать при помощи редких флоппи-дисков 5" объёмом 2,4 Мб если повезёт, бывают ещё варианты с жёстким диском на 20 Мб. Поэтому я решил сделать собственный контроллер, а в процессе узнать что-то новое.

Немного истории IBM 3270


Судя по записям компании IBM, информационная система 3270 была представлена в 1972 году, а культовый терминал 3278 через пять лет, в 1977. Цветные терминалы появились в 1979 с 3279. Сложновато не запутаться в моделях и годах!

3270 один из самых узнаваемых терминалов, работающих по блочной системе. Поток данных 3270 позволяет программисту описать целый экран, содержащий текст, защищённый от редактирования, доступные для редактирования поля и атрибуты его форматирования. Доступные для редактирования поля позволяют вводить или изменять данные. Вместо того, чтобы отправлять каждое сделанное вами нажатие клавиши сразу на хост, как это происходит у символьных терминалов, этот терминал хранит ввод у себя, пока вы не нажмёте особую клавишу к примеру, Enter. Только после этого весь ввод отправляется на хост блоком данных. Такая система улучшает воспринимаемую отзывчивость компьютера в случае медленной или ненадёжной связи. Также это снижает требования по мощности хоста, и позволяет подключать к нему гораздо больше блочных терминалов, чем символьных. В чём-то это весьма напоминает заполнение формы на HTML-странице.

Для ранних 3270 предлагалась всякая прикольная периферия, типа световых ручек для улучшения навигации и устройств для чтения магнитных карт к сожалению, на eBay я их не встречал.

Терминалы соединяются с контроллером коаксиальным кабелем по топологии звезда. При помощи балуна можно превратить её в более привычную схему подключения по витой паре, хотя, как мне кажется, IBM предпочла бы, чтобы вы использовали их систему подключения. Контроллер соединяется с мейнфреймом по разным интерфейсам, в зависимости от того, местный это контроллер или удалённый. Кроме того, подключение менялось со временем. Среди интерфейсов присутствовали:
  • Parallel Channel
  • ESCON Channel
  • X.21
  • V.35
  • Token Ring
  • Ethernet


В итоге контроллеры IBM 3174 и 3274 бывают самых разных форм и размеров от небольших настольных модулей, до средних устройств, предназначенных для установки в стойку и крупных напольных сооружений.


Контроллеры терминалов от IBM по часовой стрелке, начиная снизу слева: 3174-1L, 3174-23 (без передней крышки), 3174-61R и 3274-41D.

Другие компании, включая Memorex, производили совместимые терминалы и контроллеры. С распространением персональных компьютеров терминалы начали менять на ISA, PCMCIA и потом PCI-карточки, позволявшие подсоединять по тому же коаксиальному кабелю ПК с эмулятором. Позднее, с развитием LAN и TCP/IP эти подсоединения заменил телнет TN3270, которому не требовалось специальное железо или коаксиальный кабель.

Для клиентов, обзаведшихся сетью TCP/IP, но всё ещё пользующихся большим количеством физических терминалов, поздние варианты контроллеров 3174 предлагали возможность соединять их с мейнфреймом телнетом по TCP/IP. Для подсоединения к Hercules нам нужны именно такие контроллеры. Меня очень интересовал тот факт, что они также предлагали эмуляцию VT100, с помощью которой терминал 3270 можно было соединить с хостом на UNIX или VMS так, будто бы это был VT100. Для меня это было странным ведь терминал заточен под блочную передачу, поэтому эмуляция VT100 кажется невозможной.

Последним терминалом от IBM стал 3483, представленный в конце 1990-х. К тому времени терминалы напоминали уже тонких клиентов и использовали стандартные мониторы VGA и клавиатуры с интерфейсом PS/2.

Разбираемся в протоколе


Изначальный мой поиск навёл меня на описание кабельного подключения и потока данных для 3270. Всё это было хорошо описано, однако там ничего не сказано о протоколе передачи данных между терминалом и контроллером. Его детали я смог найти, только раскопав технические описания устаревших на сегодня чипов, использовавшихся для создания интерфейсов с ПК. Их производили CHIPS и National Semiconductor:
  • CHIPS 82C570
  • NS DP8340
  • NS DP8341
  • NS DP8344


Устройства соединяются друг с другом коаксиальным кабелем с характерным импедансом 93 . Кабель этот имеет тип RG-62 в отличие от сетей на Ethernet 10BASE2, использующих топологию шины и кабель с импедансом в 50 . Для уменьшения шума в длинных участках кабеля используется разностная передача сигналов, и максимальная рекомендованная длина отрезка между устройствами по спецификации составляет 1,5 км.

Данные идут с битрейтом в 2,3587 Мб/с в Манчестерском кодировании. Эта схема обеспечивает перепад напряжения с низкого уровня на верхний уровень (или наоборот) в рамках каждого бита, что гарантирует надёжную передачу данных до получателя. В каждом кадре содержится одно или несколько 10-битных слов. Каждое слово начинается с бита синхронизации и заканчивается битом чётности. Начало и конец кадра отмечают уникальные последовательности.


Вместо тысячи слов или, как в данном случае, 10 бит. Команда чтения состояния, отправленная с контроллера на терминал.

Все коммуникации инициирует контроллер. Он отправляет на терминал кадр, содержащий управляющее слово и опциональные слова с данными (что-то типа параметров). Терминал отвечает кадром, содержащим одно или несколько слов с данными.

Оказалось, что терминалов существует два типа CUT и DFT. CUT перекладывают обработку потока данных 3270 на управляющий модуль, и преобразуют их в базовые операции вроде движения курсора по адресу или записи символов в буфер дисплея (он же кадровый буфер). Большую часть функций, которые люди ассоциируют с терминалом 3270, выполняет контроллер то есть, логика терминала CUT проще, чем у сравнимого по функциональности VT1000. Терминалы DFT могут обрабатывать поток данных 3270 самостоятельно, и контролер просто передаёт поток на терминал.

Подробную документацию протокола я веду на GitHub.

Hello world


На eBay до сих пор можно найти передатчики DP8340 и приёмники DP8341 от National Semiconductor, поэтому я купил их и некоторые другие компоненты, перечисленные в техническом описании. Сначала я думал собрать интерфейс с нуля, однако эти интегральные схемы передатчика и приёмника в итоге обеспечили передачу данных.

Я также нашёл контроллер IBM 3174-23R для установки в стойку с интерфейсом Token Ring, и решил, что больше мне ничего и не надо. К сожалению, дискета с управляющей программой оказалась испорченной, и загрузить получалось только диск диагностики. Диагностическая программа показывала менюшки на терминале, чего хватило для перехвата части трафика между терминалом и контроллером для последующего анализа.

Я сделал простую схему приёмника по описанию DP8341 на макетной плате, подключил её к Arduino Mega, и при помощи т-коннектора подключился между контроллером и терминалом. Я был в восторге, впервые увидев поднятие напряжения на контакте DATA AVAILABLE и вызов моего обработчика прерываний. На то, чтобы надёжно считывать данные с приёмника, у меня ушло некоторое время. Вскоре я обнаружил, что памяти в 8 Кб на Arduino Mega мне уже не хватает, а поток данных шёл слишком быстро, чтобы в реальном времени скидывать их на ПК по последовательному порту.

Большая часть перехваченных мною сообщений была командами запросов с контроллера и подтверждениями, отправленными терминалом. Поскольку все коммуникации начинает контроллер, он постоянно должен опрашивать терминал на предмет нажатой клавиши. Отфильтровав все сообщения об отсутствии нажатия, я смог разметить скан-коды клавиш, которые возвращает терминал.

Я также перехватил счётчик адресов загрузки и команды на запись данных, связанные с буфером дисплея. Адрес извлечь было легко, однако сначала я не нашёл данных, записываемых в буфер дисплея, в ожидаемой мною кодировке EBCDIC (я ждал её, учитывая схему работы диагностического интерфейса на терминале). Также это была и не ASCII оказалось, что терминал использует свою кодировку символов. Как она называется, я не знаю, и отсылок к ней я не нашёл. Большую часть кодировки я смог разметить при помощи диагностического интерфейса. А когда я добавил на макетную плату передатчик DP8340, то смог полностью разметить все символы сначала, конечно же, выведя на экран hello world!

У терминала 3270 есть строка состояния, которая выводится в самом низу экрана. Ею, за исключением текущей позиции курсора, управляет контроллер. Сбивает с толку, что строка состояния начинается с адреса 0 в буфере экрана, хотя находится в самом низу а адрес 80 соответствует верхней левой ячейке в режиме 80 столбцов. Также кодировка символов в строке состояния отличается от остальных, и в неё входят много необычных символов.

В итоге я смог написать библиотеку для Python, pycoax, для сериализации и десериализации кадров 3270. Код на Python работает на ПК и общается с Arduino по последовательному порту (физически через USB).

Наконец, я разработал свою первую печатную плату при помощи KiCad и отправил на изготовление в JLCPCB. Спустя неделю и $15, я получил 5 шилдов для Arduino, готовых к сборке. Плата работает значительно лучше макетной, количество ошибок заметно уменьшилось. Да, и признаюсь, что мне пришлось заказывать плату дважды, поскольку в первый раз я совершил ошибку при разводке.


Arduino Mega и шилд-интерфейс для коаксиала

Создание oec


Моей целью было создать замену контроллеру IBM 3174, которую можно было бы использовать совместно с Hercules. Теперь, когда я понял, что терминал типа CUT не обрабатывают поток данных 3270 самостоятельно, я понял, что реализовать сначала эмуляцию VT100 будет гораздо проще, чем TN3270.
Я нашёл pyte, обеспечивающий эмуляцию VT100 в памяти, и поддерживающий подключение к процессу (типа командной оболочки или SSH). Мне нужно было только опрашивать терминал на предмет нажатий клавиш, передавать их процессу, обновлять эмулятор pyte, передавая ему выходные данные с процесса, а потом выводить эмулированный экран терминала на сам терминале. А кроме этого ещё нужно было следить, включен терминал или отключен, и отслеживать здоровье процесса.

Первые контроллеры, существовавшие в эпоху дорогой памяти, не удерживали копию экрана в своей памяти, как я делаю это сейчас. У терминала CUT есть команды на чтение из буфера дисплея и на поиск ячеек, соответствующих определённой последовательности типа атрибута, обозначающего начало поля ввода. Я обнаружил, что без подобных ограничений куда легче хранить копию экрана в памяти и обновлять терминал.

Вдохновившись pyte, я написал pytn3270, быблиотеку TN3270 на чистом Python, обеспечивающую похожую эмуляцию в памяти. Для интеграции с контроллером потребовалось сопоставить атрибуты полей байту атрибутов терминала 3270. Я мог бы использовать x3270, однако мне хотелось лучше разобраться в протоколе telnet и потоке данных 3270. Для протокола TN3270 я реализовал необходимый минимум но его достаточно для подключения к z/OS, а библиотека на чистом Python может оказаться полезной и вне этого проекта.

На сегодня oec ещё далёк от поддержки абсолютно всех функций контроллера IBM 3174, однако пользоваться им можно. Скачать его можно с GitHub.

Будущее


Во-первых, хочу добавить меню связи, чтобы можно было выбирать хоста (пока что мы умеем подключаться только к одному хосту) и поддержку нескольких логических терминалов. Потом добавлю поддержку EAB и TN3270E для этого мне нужно будет реализовать некоторые функции в pytn3270, которых пока не хватает.

Пока я работаю над интерфейсом на основе FPGA, чтобы не полагаться на устаревшие микросхемы от National Semiconductor и в процессе изучаю протокол и немного Verilog.
Подробнее..

Где электроника не сможет там Бог поможет. Молитвослов на атмеге328

24.06.2020 00:13:58 | Автор: admin
Идея данного проекта мне пришла почти год назад. Основная идея это коробочка, открыв которую, люди в возрасте могли легко и просто получить доступ к молитвам. Список вошедших молитв это топ самых высокочастотных запросов в ютубе со словом молитвы, кроме одной (думаю сами догадаетесь какой).

Первая версия моего молитвослова. В первой версии название молитв не совпадает с проигрываемыми молитвами.



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



Спасибо за внимание.
Подробнее..

Из чего состоит набор для разработчиков NB-IoT DevKit?

26.06.2020 12:10:59 | Автор: admin
Набор вышел в начале июня. Он поможет разобраться, в чем преимущества сети интернета вещей NB-IoT, и научит работать с ней. В комплект входит аппаратная часть, коннективити, то есть доступ к сети NB-IoT и доступ к IoT-платформам. Главная фича DevKit демонстрационная прошивка, которая позволяет на практике разобраться, как работает система. В этой статье детально рассмотрим DevKit и его возможности.



Кому это надо?


Когда мы начали разворачивать сеть NB-IoT (почитать больше о сети NB-IoT можно здесь), на нас со всех сторон посыпались различные вопросы. Крупные производители, которые много лет работают на рынке M2M устройств, стартапы, начинающие разработчики и просто любители интересовались режимами работы сети, протоколами передачи данных, даже управлением радиомодулем АТ-командами. Нас спрашивали, какие частоты (band) используются, как работает режим power save mode, как устройство и сеть согласуют соответствующие таймеры, как, используя протокол транспортного уровня UDP, добиться гарантированной доставки сообщения, как задать APN и выбрать определенный band (частотный диапазон). И множество других вопросов.

В ходе тестов мы также обратили внимание, что многие устройства не адаптированы для работы в сети NB-IoT и в результате работают некорректно. Например, использование протокола передачи данных с транспортным уровнем TCP приводит к высокому объему передаваемого трафика, а также к трудностям при работе в сложных радиоусловиях. Другая распространенная проблема использование радиомодуля в режиме по умолчанию, в котором модуль сам включает eDRX: это вводит пользователя в заблуждение, так как он не может принять данные в произвольный момент времени.

К нам стали обращаться ВУЗы, специализирующиеся на телекоммуникациях, которым интересно изучить на реальных устройствах технологии LPWAN, в том числе NB-IoT, и разработчики-любители, которые хотят поэксперементировать и создать собственное IoT-решение для умного дома или даже умного города.

Мы решили, что лучший способ ответить на все эти вопросы создать development kit, на котором можно будет максимально быстро научиться работать с сетью интернета вещей.

Из чего состоит набор?


Мы сделали решение, с которым можно сразу начинать работать, ничего не приобретая дополнительно. Как уже говорилось выше, в комплект входит аппаратная часть, коннективити, то есть доступ к сети NB-IoT, и доступ к IoT-платформам.

В состав аппаратной части входит радиомодуль Ublox SARA-R410, микроконтроллер STM32L152RE, кабели, антенны и программатор. На основной плате установлены акселерометр и температурный датчик. Плата расширения в форм-факторе Arduino-shield содержит GNSS модуль.

Модуль идентификации в сети входит в базовый набор и выполнен в виде SIM-чипа NB-IoT с безлимитным трафиком на год. Пользователю не нужно тратить время на подключение SIM-карты и выбирать тариф, кроме того, SIM-чип обеспечивает большую надежность: он не подвержен перепадам температур, влажности и вибрациям. Также вместе с комплектом пользователь получает готовую учетную запись на платформе IoT с настроенными виджетами и возможностью гибко настраивать виджеты самостоятельно дополнительно получать и оплачивать аккаунт не потребуется.

Также по запросу возможна активация функционала NIDD и возможность работы через узел SCEF (Service Capability Exposure Function) сети NB-IoT МТС. Сервис снимает с разработчика необходимость идентификации и аутентификации устройств и предоставляет возможность клиентским серверам приложений (Application Server) получать данные и управлять устройствами через единый API-интерфейс. В России ни один другой оператор интернета вещей не предоставляет такую возможность.

Возможности демонстрационной прошивки


Демонстрационная прошивка содержит функциональное меню, которое позволяет новичкам сразу приступить к работе. Достаточно лишь подключить плату и получить регистрационные данные. Прошивка реализует функциональное меню, доступ к которому мы получаем, подключившись через любую терминальную программу. Можно воспользоваться знакомой многим PuTTY. Мы также рекомендуем удобную и бесплатную утилиту от Ublox: m-center



Выглядит меню так:

*** Welcome to MTS NB-IoT Development Kit service menu ***
Firmware version: 2.3 beta 2, 28.05.2020

Current settings found in EEPROM:

Target IP: 195.34.49.22
Target port: 6683
Target URL: /api/v1/devkitNIDDtest03/telemetry
NIDD APN: devkit.nidd
Use NIDD for telemetry: 1
Board mode on startup: service menu
Telemetry interval
(in logger mode): 1000 ms
GNSS privacy mode: 0

Type in a function number from a list below and press enter.

Target server setup:
1 set the URL of the resource JSON data will be transmitted to
2 set the IP address
3 set the port
4 set an APN for NIDD access or turn NIDD mode ON or OFF
System functions:
5 force send telemetry packet
6 wait for incoming NIDD data during specified timeout
7 enter direct AT-command mode
8 enter true direct mode to access the RF module
CAUTION: to exit this mode you will have to reboot the board physically
9 show ICCID, IMEI, IMSI and MCU serial number
10 show network information
11 set telemetry transmission interval
12 set GNSS privacy mode
(hide actual location data when transmitting on server)
13 set firmware startup mode (setup or logger)
14 read on-board sensors and try to acquire GNSS data
15 reboot MCU
16 reboot RF module
17 factory setup & test
(do not use this unless you really know what you want)

Первые четыре пункта меню позволяют настроить параметры соединения с платформой (URI-path, IP, порт, параметры NIDD). Пятый пункт позволяет отправить пакет, содержащий данные с датчиков на плате.

Пример того, как выглядит результат отправки пакета по протоколу CoAP:

Using IP method.
Sending data to iotsandbox.mts.ru:6683/api/v1/devkitNIDDtest03/telemetry
Telemetry:
{'interface':'telemetry', 'ICCID':'89701012417915117807', 'Tamb_degC':24, 'aX':-14, 'aY':23, 'aZ':244, 'RSSI_dBm':-81, 'latitude':55.768848, 'longitude':37.715088, 'GNSS_data_valid':1}
(184 bytes)
Raw CoAP data (225 bytes):
5102000000B36170690276310D036465766B69744E4944447465737430330974656C656D65747279FF7B27696E74657266616365273A2774656C656D65747279272C20274943434944273A273839373031303132343137393135313137383037272C202754616D625F64656743273A32342C20276158273A2D31342C20276159273A32332C2027615A273A3234342C2027525353495F64426D273A2D38312C20276C61746974756465273A35352E3736383834382C20276C6F6E676974756465273A33372E3731353038382C2027474E53535F646174615F76616C6964273A317D
Server response code: 2.03, response time = 664 ms.
Server response dump:
5143067000

Переданные данные мы видим на платформе:



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

On-board sensors data:
temperature: 23

accelerometer: X = -3 Y = 4 Z = 249
Accelerometer signature value is correct.

Testing GNSS option
GNSS string:
$GNGLL,5546.12120,N,03742.89412,E,133220.00,A,D*75
Parsed data:
latitude = 55.768687
longitude = 37.714902

Можно прочитать идентификаторы радиомодуля и микроконтроллера, используя пункт 7 меню. В таком случае ответ будет:

Board identification data
SIM card ICCID: 89701012417915117807
AT+CCID

+CCID: 89701012417915117807

OK
RF module IMEI: 359215103260015
AT+CGSN

359215103260015

OK
IMSI:
AT+CIMI

250011791511780

OK
MCU serial number:
0x393533307473832200

Используя пункт 10 меню можно считать сетевые параметры NB-IoT, вот пример вывода:

RSSI = -75 dBm (valid range is -111 to -51 dBm)
SNR = 22 dB
RSRP = -83 dBm (valid range is -141 to -44 dBm)
Cell ID = 753621
EARFCN = 1711

Также можно перейти в режим прямого управления радиомодулем посредством АТ-команд, используя пункт меню 8. Эта функция переводит Development kit в режим отладочной платы радиомодуля. Ниже пример прямого управления радиомодулем с командами обмена данными между устройством и сервером в режиме NIDD:

Entering true direct mode.
From now on everything you type into a terminal will be transferred to the RF module as is
(and similarly in reverse direction).
NOTICE: No special commands supported here, for nothing is between you and the RF module.
YOU NEED TO REBOOT THE BOARD PHYSICALLY TO EXIT THIS MODE.

at+cereg?
+CEREG: 0,1
OK

AT+COPS?
+COPS: 0,0,MTS RUS MTS RUS,9
OK

at+cgdcont?
+CGDCONT: 1,NONIP,devkit.nidd,0.0.0.0,0,0,0,0
OK

AT+CRTDCP=1
OK

AT+CSODCP=1,16,"{'test':'hello'}"
OK

+CRTDCP:1,5,Hi hi

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

Для продвинутых пользователей открыты исходные коды прошивки, можно оценить логику работы устройства на уровне кода, внести изменения и экспериментировать. Со стороны облачной платформы доступны инструменты для обработки данных: виджеты, панели управления, инструменты Интеграции, позволяющие принять данные в любом формате в протоколах UDP, TCP и Non-IP. Инструмент Конвертер данных позволяют привести получаемый поток данных к виду понятному платформе.

DevKit и Ардуино


Несмотря на критическое отношение части профессионалов к Ардуино, которые указывают на неоптимальный код и на то, что библиотеки написаны не профессиональными разработчиками, мы изначально хотели сделать DevKit совместимым с Ардуино. Arduino IDE пользуется популярностью среди разработчиковлюбителей, а также может применяться в учебных программах ВУЗами.

DevKit полностью совместим с Ардуино R3 по своему духу, архитектуре, и разьемам. Для устройства были разработаны все сопутствующие файлы поддержки, так что оно устанавливается в Arduino IDE штатным образом.



В состав пакета для Arduino IDE включены необходимые инструменты и библиотеки, а также ряд примеров с исходными текстами. Программная совместимость с другими платами Arduino на основе STM32 позволяет использовать написанные для них библиотеки, а также загружать рабочие скетчи практически без изменений. Для программирования DevKit из Arduino IDE необходимо установить ПО STMCubeProgrammer, которое можно скачать с сайта компании STM. Наши разработчики продолжают наполнять пакет для Arduino новыми библиотеками и примерами.

В качестве простейшего примера работы с Arduino IDE рассмотрим загрузку примера Blink (мигание светодиодом). На изображении ниже можно увидеть результат загрузки и компиляции данного скетча из меню Файл Примеры 01.Basics





Поддерживаемые интегрированные среды разработки


Разработка демонстрационной прошивки производилась в интегрированной среде Em::Bitz. Программный код примера рассчитан на компиляцию с помощью GCC. Для разработки пользовательского кода пригодны любые IDE, поддерживающие семейство микроконтроллеров STM32L15x. Например, Arduino IDE, IAR Embedded Workbench, Keil uVision MDK ARM и другие.

Расширение функциональности


Функциональность можно расширять как с помощью шилдов, так и подключая любые дискретные датчики.



Установка GNSS-шилда превращает DevKit в GPS/GLONASS трекер.



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



Можно подключить специальный шилд и добавить функциональность Ethernet и карту памяти.



Подключая датчики скорости и направления ветра получаем NB-IoT метеостанцию



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



В следующем материале покажем на практике, как собрать на основе NB-IoT DevKit устройство интернета вещей. Заказать NB-IoT Development Kit можно уже сейчас на нашем сайте.

Авторы:
старший менеджер группы планирования сети IoT Виталий Бачук
ведущий эксперт направления МТС Виктор Лучанский
руководитель группы планирования сети IoT Андрей Плавич
Подробнее..

ESP32 development board с кастомным дизайном

28.06.2020 02:16:08 | Автор: admin
Наконец то у меня добрались руки до сборки минимального аналога Espressif Systems плат на ESP32 микроконтроллере.

На плате расположен микрофон, усилитель для этого микрофона на одном транзисторе. Усилитель для динамика на трех транзисторах с выходным каскадом работающим в режиме AB. OLED дисплей 128x64 SSD1306



Espressif Audio Development Framework предоставляет богатую функциональную SDK которая работает все стабильнее с каждым релизом. Не без своих косяков конечно. Есть у нее и минусы. Например некоторые библиотеки предоставляются без исходных кодов. В линейке плат не было версии с использованием внутреннего ЦАП и АЦП для обработки звука. Конечно для чего-то серьезного такой подход не подойдет. И разрядность встроенного АЦП 12 бит и ЦАП 8 бит накладывает некоторые ограничения. Да и шумят эти встроенные модули не слабо. Но хотелось попробовать сделать если уж не VoIP телефон, то пример Google Translator и TTS Text-To-Speech постараться запустить. Как это у меня получилось читайте под катом


Плата проектировалась и разводилась в Eagle Autodesk.



ESP32 Module Amplifiers



OLED Display



Поднимать плату будем традиционно с питания. Распаиваем разъем питания, стабилизатор напряжения питания и конденсатор на выходе это стабилизатора. Замеряем напряжение питания. На входе 5V после стабилизатора должно быть 3.3V

Плата задумывалась работать в переносном варианте с питанием от аккумулятора. Для этого предусмотрен чип IC2 отвечающий за заряд батареи TP4056 и схема барьера на полевом транзисторе Q3 и диоде Шоттки D3. Стабилизатор питания сделан на HT7833. Он обеспечивает малое падение напряжения и достаточный для питания схемы ток

Цепь зарядки нас пока не интересует, поэтому кинем перемычку с разъема питания прямо на вход HT7833. Померяем что на выходе и приступим к распайке самого модуля ESP32



Надо распаять цепь Reset и кнопку PROG которую надо удерживать для перевода контроллера в режим программирования. Пины для подключения UART адаптера RX-TX нужны для соединения платы с компьютером

Теперь можно подключать плату к компьютеру через USB-to-UART адаптер и запустить какой нибудь пример esp-idf. Например сканирования сети



Далее распаиваем усилитель для динамика и микрофона. Про использование усилителей для микрофона и динамика можно прочитать в статье Микрофонный усилитель и УНЧ для ЦАП и АЦП микроконтроллера

Настраиваем резистором R15 половину напряжения на эмиттерах T3 и T4 в схеме усилителя динамика. У меня подошел резистор 33К. R17 можно поставить поменьше 1K 4.7K если усиления будет недостаточно. Он ограничивает усиление и выступает в качестве делителя напряжения

Для усилителя микрофона резистором R10 подбираем на коллекторе транзистора T2 напряжение в районе 0.6V...1.2V. Чем больше смещение при слабом сигнале, тем больше потери на разрядности АЦП. Так как у нас один каскад усилителя на одном транзисторе, амплитуда сигнала будет не большая. Я поставил 100К и получил 0.85V. Полная амплитуда сигнала full-scale voltage будет 1.7V что можно компенсировать аттенюатором на входе АЦП взяв например ADC_ATTEN_DB_2_5

Напомню про градации аттенюатора на входе АЦП ESP32

  • 0 dB attenuation (ADC_ATTEN_DB_0) gives full-scale voltage 1.1 V
  • 2.5 dB attenuation (ADC_ATTEN_DB_2_5) gives full-scale voltage 1.5 V
  • 6 dB attenuation (ADC_ATTEN_DB_6) gives full-scale voltage 2.2 V
  • 11 dB attenuation (ADC_ATTEN_DB_11) gives full-scale voltage 3.9 V


Теперь уже можно запустить что-то со звуком. Что касается примеров рассчитанных на внешний ЦАП тут все просто, в настройках конфигурации добавляем I2S_MODE_DAC_BUILT_IN

i2s_config_t i2s_config = {        .mode = I2S_MODE_MASTER | I2S_MODE_TX | I2S_MODE_DAC_BUILT_IN,...


Или при использовании esp-adf audio development framework

struct i2s_stream_cfg_t i2s_cfg = I2S_STREAM_INTERNAL_DAC_CFG_DEFAULT();


В частности это изменение для примера pipeline_bt_sink позволяет получить на этой плате BlueTooth динамик

Есть пример который выдает синусоидальный сигнал dac-cosine

Замена использования внешнего АЦП на внутренний так же возможна, но это будет несколько сложнее. Я это делаю примерно так
#define I2S_STREAM_CFG_1() {                                              \    .type = AUDIO_STREAM_WRITER,                                                \    .task_prio = I2S_STREAM_TASK_PRIO,                                          \    .task_core = I2S_STREAM_TASK_CORE,                                          \    .task_stack = I2S_STREAM_TASK_STACK,                                        \    .out_rb_size = I2S_STREAM_RINGBUFFER_SIZE,                                  \    .i2s_config = {                                                             \        .mode = I2S_MODE_MASTER  | I2S_MODE_RX | I2S_MODE_TX | I2S_MODE_ADC_BUILT_IN | I2S_MODE_DAC_BUILT_IN,                    \        .sample_rate = SAMPLE_RATE,                                                   \        .bits_per_sample = I2S_BITS_PER_SAMPLE,                                                  \        .channel_format = I2S_CHANNEL_FMT,                           \        .communication_format = I2S_COMM_FORMAT_I2S_MSB,                            \        .dma_buf_count = 4,                                                     \        .dma_buf_len = 512,                                                     \        .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1,                               \    },                                                                          \    .i2s_port = I2S_NUM_0 \}


И заменяю дефолтный конфиг esp-adf framework на свой

i2s_stream_cfg_t i2s_cfg = I2S_STREAM_CFG_1();i2s_cfg.type = AUDIO_STREAM_READER;


Продолжение сборки и запуск примеров в видео:



Используя эти подходы по замене внешнего ЦАП и АЦП на встроенный мне удалось запустить такие примеры как:
pipeline_http_mp3
pipeline_bt_sink
google_translate_device



И многие другие

На плате предусмотрена разводка под джойстик 4+1 кнопки, так что можно реализовать мини консоль Arduboy

Запустить VoIP пока не удалось. Но думаю у меня возникли проблема с настройками протокола. Так что пока в процессе. Но опять же, библиотека SIP в примере закрытая. Есть пример на Github sip_call с другой и открытой реализацией SIP библиотеки

В заключении хотел отметить, что я вывел на плате некоторые неиспользованные порты GPIO для подключения периферии. Возможность подключения внешнего ЦАП и АЦП все еще остается, что надо проверить

Подробнее..

Из песочницы Пропеллеролет конвертоплан для мониторинга протяженных объектов

28.06.2020 20:20:26 | Автор: admin


Однажды в институте зав кафедры рассказывал нам о нашей будущей профессии:
Наша профессия очень важна и ответственна. Сейчас вы работаете над обликом будущего флагмана Российской авиации. Вы сыпете идеями, пытаетесь предугадать задачи, которые он будет решать, берете на себя ответственность и принимаете решения. А через 15 лет, когда самолет наконец построят, всем будут раздавать почести, но про вас уже никто не вспомнит.
Такая агитация мне не понравилась, ждать 15 лет результата я не готов.

Авиация высокоразвитая отрасль, но высокие требования по надежности и распланированное будущее не позволяют внедрять новые технологии быстро. А беспилотникам же наоборот, никаких особых требований пока нет, вот они и растут как грибы после дождя, заполняя собой ниши авиа мониторинга, рекламы, помощи сельскому хозяйству, видеосъемки и подобные.
Так я и решил, пускай они там как-нибудь без меня строят свои истребители, а я, пока молод и горяч, пойду в беспилотники. Благо что авиамодельное прошлое имеется.

Какие есть пути развития авиации? На мой взгляд, авиация может развиваться двумя путями:

  • Использование инновационных технологий для комплектующих (новые эффективные силовые установки с малым весом, авионика и прочее).
  • Изменение аэродинамической схемы.

Мне больше импонирует изменение аэродинамической схемы самолета. Этим путем появились такие аэродинамические схемы как утка и летающее крыло. Но инженерам этого было мало, им хотелось, чтобы аэродинамика изменялась прямо в полете. Так появились самолеты с изменяемой геометрией крыла (Су-24, Ту-22М, Ту-160, F-111 и им подобные), самолеты с управляемым вектором тяги (Миг-29ОВТ, Миг-35, Су-30, 37, 57, F-22 и др) и, конечно же, конвертопланы (например, всем известный Bell V-22 Osprey).

Это все, конечно, хорошо, но не совсем мне по душе. Идея с изменяемой геометрией крыла интересна, но надо, чтоб если складывались крылья, то так, чтоб совсем, а не жалкие несколько градусов. А вот идея с поворотными силовыми установками мне импонирует. Так и родилась идея сделать свой конвертоплан. Схематично нашу идею можно описать следующим образом:


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

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

Какие преимущества у данной аэродинамической схемы?

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

Закончим введение и перейдем к сути. Для реализации проекта нам необходимо:

  • Изготовить прототип.
  • Создать систему стабилизации за счет управляемых векторов тяги.
  • Реализовать алгоритмы переходного процесса во время смены режимов полета.
  • Провести тестовые полеты, отладить систему.
  • Коммерческий полет.

Прототип 1


Мы приступили к созданию первого прототипа. У нас не было никакого особого оборудования, только руки и базовые знания. Начали с того, что начертили модель в 3D.





Построенный летательный аппарат выглядел следующим образом:





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

Написали простенький код для управления моторами и запустили этого монстра сначала на стенде, а потом и в воздухе. Мы с ней съездили на несколько конференций, где получали неоднозначные отзывы. При изготовлении первого прототипа в расчеты мы не углублялись, и единственная лтх, которую мы можем предоставить, это его габариты. Круизная скорость рассчитывалась на 100 км/час, но как-то не сложилось с дальними полетами и записью хоть каких-то характеристик.
габариты в сложенном состоянии 650мм x 400мм x 600мм
размах 1100мм
длина 650мм
диаметр пропеллеров 11 дюймов
снаряженная масса 3 кг

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

  • Определиться с конструкцией механизма наклона моторов.
  • Проверить возможность реализации такой модели.
  • Сформулировали облик модели.
  • Получили наглядную модель всех механизмов.

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

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

Прототип 2



Встретились как-то красотка, сантехник, турист, айтишник и школьник. И сантехник говорит, Диплом близко, надо строить самолет.

Мы собрались снова и начали делать второй прототип. Теперь в нашем распоряжении были: гараж, оборудованный по последнему слову техники, токарный станок, ЧПУ, 3D принтер, а также опыт и знания, полученные в институте. Сразу определили целевое назначение беспилотника, он будет мониторить нефтепроводы. Под эту задачу подобрали оптимальные ЛТХ (это отдельная история). Рассчитали и начертили все агрегаты и узлы. Рассчитали систему спасения и сшили парашют. Разработали систему раскрывания и складывания крыльев. Изготовили прототип со следующими ЛТХ:
Технические характеристики
Режим самолетный вертолетный
Длительность полета до 2,5 часов до 20 минут
Максимальная протяженность маршрута 100 км 10 км
Скорость полета 70-150 км/ч 0-50 км/ч
Максимальная взлетная масса 20 кг
Макс. масса полезной нагрузки 2 кг
Габаритные размеры Размах 3200мм,
длина 1600мм
1500мм х 900мм х 1600мм
ГМаксимальная высота полета 2500 м 1200 м
Двигатель электрический
Взлет / посадка аварийная система посадки на парашюте Вертикально в автоматическом режиме с использованием док станции
Время взлета/посадки - 5 мин

Летные характеристики
Режим самолетный вертолетный
Крейсерская скорость 100 км/ч 37 км/ч
Скороподъемность 1,3 м/с 2,5 м/с
Скорость полета 70-150 км/ч 0-50 км/ч
Скорость необходимая для трансформации 45 км/ч -
Скорость сваливания 42 км/ч (1,5кг полезной нагрузки) -
Аэродинамическое качество 11,6 0











С построенным прототипом мы принимали участие в МАКС-2019, поучавствовали в конкурсе от Вертолетов России и продолжаем заниматься его доработками в свободное от отдыха время.

Выводы


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

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

P.S


В данном проекте при постановки целей и задач я руководствуюсь требованиями и условиями предъявляемыми к ДПЛА в задачах мониторинга, применение всех идей подходов и созданных прототипов в других отраслях может быть рассмотрено только после достижения поставленных сейчас целей. Такой подход считаю рациональным в сложившихся обстоятельствах с ограниченными материальными, трудовыми и временными ресурсами. Подобными суждениями я пользовался довольно часто в процессе работы над этим проектом, потому что с нетерпением хочу увидеть в действии то, чем занимаюсь.
Подробнее..

Солнечная электростанция на балконе. Личный опыт

29.06.2020 00:11:43 | Автор: admin
Привет Хабр.

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


Фото (с) smartflower.com

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

Для тех, кому интересно как это работает, продолжение под катом.

Зачем это нужно?


Уверен, что вопрос зачем будет самым первым в комментариях, поэтому отвечу на него сразу. С экономической точки зрения, солнечная батарея на балконе невыгодна, масштабы генерации увы, не те, в идеале, нужна крыша собственного дома. Но с технической и инженерной точки зрения это достаточно интересно в плане изучения новых и современных технологий. Плюс не стоит забывать, что современные квартиры потребляют все больше энергии, особенно с учетом растущей популярности различных смарт-устройств, подключенных постоянно роутеров, умных лампочек, умных розеток, умных котокормушек и прочего. Компенсировать эти затраты солнечной энергией в принципе, не так уж плохо и вполне экологично. Ну и наконец, посмотреть на электросчетчик и увидеть на экране текущее потребление -100Вт просто приятно.

Общая информация


Как известно, существуют два основных принципа работы домашних солнечных электростанций.

  1. Накапливание энергии в аккумуляторах.
  2. Отдача энергии в электросеть.

Первый способ подразумевает ситуацию, когда нужно действительно автономное электроснабжение, т.е. электричества либо нет совсем, либо оно подается нерегулярно. В таком случае энергия солнечных панелей сначала запасается в аккумуляторах, затем через инвертор создается обычное напряжение 220В. Плюс наличия аккумуляторов в том, что система может работать при отсутствии внешнего электричества. Увы, минусов тут гораздо больше, чем плюсов. Аккумуляторные батареи дорогие и их ресурс службы ограничен 2-3 годами для свинцовых аккумуляторов. Нужно думать, как переключать потребителей между основной и солнечной сетью. Аккумулятор может быть глубоко разряжен или перезаряжен, и то и другое для них плохо. Если аккумулятор полностью заряжен, то панели работают впустую, и энергия пропадает зря. Плюс нужно думать про балансировку ячеек аккумулятора, если их несколько, и так далее. Правда, в последние годы наметился некий прогресс, в частности, с Tesla Powerwall там все работает из коробки, используются литиевые батареи и гарантия 10 лет, однако при цене в 6500$, окупаемость такой штуки под вопросом.

Второй способ, и он же наиболее эффективный это напрямую отдавать электроэнергию от солнечных панелей в электросеть. В этом случае панели подключаются к специальному grid-tie инвертору, который не только преобразует постоянный ток от панелей в 220В, но и синхронизирует фазу с электросетью. Отданная в розетку электроэнергия потребляется внутри квартиры/дома, избытки уходят в городскую сеть, таким образом мы не только добываем электроэнергию себе, но и помогаем соседям/городу немного разгрузить общие электросети. В случае современного законодательства и наличия электросчетчика, умеющего считать импорт и экспорт, мы даже можем получить немного денег, но не во всех странах это пока работает.

В моем случае автономка была неактуальна, загромождать квартиру аккумуляторами резона не было, так что выбор был очевиден. Кстати, минус у grid-tie инвертора один в случае пропадания напряжения электросети он отключается, таким образом, даже имея целую крышу солнечных панелей на 3-4КВт, можно оказаться без электричества если оно вдруг пропало. Но в моем случае отключения настолько редки, что ими можно пренебречь, на крайний случай, сейчас огромный выбор довольно эффективных DC-DC конвертеров, которыми можно запитать и ноутбук, и смартфон, и LED-лампы, так что при желании это вполне можно доделать.

Итак, общая идея того что делать, ясна, let's get started.

Схема соединений будет крайне простой, берем солнечные панели, подключаем к инвертору, включаем его в розетку:

Рассмотрим все компоненты системы последовательно.

1. Солнечные панели


Первый актуальный вопрос это выбор панелей. Доводилось читать мнения экспертов, что солнечные панели отличаются по КПД, и надо брать наиболее эффективные. С этим трудно поспорить, однако, как показал поиск, разница составляет 2-3%. Судя по статье Most Efficient Solar Panels 2020 топ-10 панелей по эффективности выглядит так:



Однако в наличии таких панелей просто не было, а там где были, разница в цене была бы гораздо больше чем заявленные 2-3% разницы в КПД. В общем, на этот пункт я просто забил и выбрал те, что были в наличии на местном Амазоне и имели лучшие отзывы.

Остается выбрать мощность. Тут все просто, чем больше панель, тем она в пересчете на ватт дешевле, оптимум получился в районе 160Вт:



В принципе, есть более крупные панели на 320 или 360Вт, но они довольно громоздкие и тяжелые, с более дорогой доставкой, и для балкона уже великоваты. В общем, 160 Вт оказалось оптимальным значением. Размер такой панели составляет 150x70см, а вес 12.5кг.

К солнечным панелям также был куплен крепеж с регулируемым углом наклона:



Практически, две 160Вт панели нормально помещаются на балконе, можно даже было бы поставить третью, но тогда балкон был бы занят полностью, и выходить туда уже было бы неудобно:



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

2. Grid-tie инвертор


Выбор инверторов для таких микромощностей не так уж велик, из основных моделей можно отметить такой:



Это довольно простой китайский инвертор ценой 80-100Евро, есть модели под разное входное напряжение, 11-30В и 22-60В. Если есть возможность использовать более высокое напряжение и соединить две панели последовательно, то лучше второй вариант, но если солнечная панель одна, то остается первый.

У этого инвертора есть минус он периодически шумит, т.к. внутри есть кулер. Включается он только днем при мощности более 100Вт, ночью солнца нет, но это все же стоит иметь в виду если инвертор стоит в жилой комнате.

Другой вариант, это так называемый микро-инвертор, который закрепляется прямо на солнечной панели:



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

Сбор данных


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

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



Он может выводить основные параметры (мощность, напряжение, ток, сумма киловатт-часов), однако никаких сетевых функций, как и возможности сохранения данных, он не имеет.

Сумма киловатт-часов полезна, если речь идет об утюге или холодильнике, однако для солнечных панелей актуально видеть выработку в течении дня. Поиск показал, что наилучший функционал обеспечивает смарт-розетка TP-Link Kasa HS110 ценой порядка 25Евро она умеет не только показывать данные о мощности, но и под неё существует Python API, позволяющий получать текущие данные. Важно не перепутать с моделью HS100, измерения мощности в ней нет. Кстати, как бонус, софт от TP-Link имеет собственное облако, и видеть значения генерации можно онлайн из любой точки мира:



К сожалению, ни на одной из умных розеток нет своего LCD-экрана (я давно знал, что все маркетинговые и дизайн-решения принимаются алиенами, которые в данном случае, считают что удобнее взять смартфон и сделать 10 тапов чтобы посмотреть мощность, чем просто взглянуть на LCD-экран). В итоге, получился такой паровозик первая не-умная розетка показывает значения генерации на экране, вторая умная но зато без экрана, обеспечивает коннект по WiFi. Честь и хвала современным маркетологам (а может так и задумано, я ведь потратил в итоге деньги на 2 устройства вместо одного).



Однако, встроенного ведения логов в приложении TP-Link нет, пришлось дописать это самостоятельно, для этого использовалась библиотека https://github.com/python-kasa/python-kasa. Разумеется, это можно было бы автоматизировать с помощью OpenHAB или Home Assistant, но держать целый сервер на выделенном устройстве для того, что можно сделать из 20 строк кода, мне показалось избыточным.

Код записи лога весьма прост:

from kasa import Discover, SmartPlug, SmartDeviceimport datetime, logging, time, asynciolog_format = "solarlog-%Y-%m.csv"def get_power_from_meter() -> float:    try:        logging.debug("Connecting the smart plug...")        devices = asyncio.run(Discover.discover())        for addr, dev in devices.items():            if dev.is_plug:                asyncio.run(dev.update())                if dev.has_emeter:                    logging.debug("Smart Plug found: %s", addr)                    emeter_status = asyncio.run(dev.get_emeter_realtime())                    power = emeter_status['power']                    return float(power)        logging.debug("Smart Plug was not found")    except Exception as e:        logging.error("get_power_from_meter exception: %s", e)    return -1.0def write_log(power: float):    log_name = datetime.datetime.now().strftime(log_format)    with open(log_name, "a") as logfile:        logfile.write(f'{datetime.datetime.now().isoformat()},{power}\n')if __name__ == "__main__":    logging.basicConfig(level=logging.DEBUG, format='[%(asctime)-15s] %(message)s')    logging.debug("App started")    # Read meter and save to the log    try:        while True:            power = get_power_from_meter()            logging.debug("Power reading: %f W", power)            write_log(power)            time.sleep(60.0)    except KeyboardInterrupt:        pass    logging.debug("App done")

При работе программы будут создаваться csv-файлы лога с шагом примерно в минуту и разбивкой по месяцам. Я запустил сбор лога на своем роутере с dd-wrt, для чего достаточно команды nohup python3 /opt/solar.py >/dev/null 2>&1 &. При желании можно добавить скрипт в автозагрузку, чтобы не вводить команду каждый раз при включении роутера. Была также идея добавить в программу свой веб-сервер для доступа к логу, но на практике стандартного WinSCP оказалось вполне достаточно, чтобы раз в несколько дней скачать новый лог.

Результаты


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



В моем случае балкон ориентирован на запад, утром панели в тени, и полноценная выработка начинается со второй половины дня. Хотя уже в 9 утра в электросеть отдается до 25Вт, что в целом неплохо. Как можно видеть из графика, пиковая мощность составила порядка 175Вт, также хорошо видны провалы на графике из-за набегающих иногда туч. Заканчивается генерация после 21 час летом световой день длинный, зимой он будет, разумеется, короче.
За весь этот день было выработано 0.73КВт*ч электроэнергии:



Если бы туч не было совсем, наверно можно было бы рассчитывать на прирост 20-30%, т.е. суммарно получится ровно 1кВт*ч/день. Панели кстати, работают и в пасмурную погоду, но выработка при этом разумеется, меньше, и лишь при совсем темных грозовых тучах, может упасть до нуля.

Из негативного, можно отметить, что КПД получился не такой высокий, как хотелось бы. Увы, производители пишут на панелях максимальное значение мощности, полученное под прямым углом падения солнечных лучей и кристально чистом воздухе на Луне в Гималаях. В реале Солнце постоянно движется по небу, и оптимальный угол падения будет длиться не более 1-2 часов в день. Ничего страшного в этом разумеется нет, просто нужно учитывать, что к примеру, реальных 100Вт со 100-ваттной солнечной панели практически никогда вырабатываться не будет.

Экспорт энергии в электросеть


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

Здесь есть варианты:

  • Старый дисковый счетчик при реверсе напряжения скорее всего не будет крутить диск (в нем есть специальный стопор), т.е. выработанное нами электричество будет отдаваться в сеть бесплатно, показания на счетчике изменяться не будут.
  • Старый дисковый счетчик без стопора будет крутить диск в обратную сторону, т.е. показания счетчика будут уменьшаться, что разумеется, выгодно владельцу солнечных панелей. Но такие счетчики сейчас не выпускаются и стали музейной редкостью.
  • Цифровой счетчик, не умеющий считать экспорт электроэнергии, будет считать её по модулю независимо от направления, т.е. за каждый отдаваемый городу киловатт владелец солнечных панелей будет платить как за потребленный.
  • Современный цифровой счетчик, умеющий считать как экспорт, так и импорт электроэнергии, будет показывать отдельные значения по всем параметрам. Всего на таком счетчике 4 сменяющих друг друга варианта показаний: импорт дневной, импорт ночной, экспорт дневной, экспорт ночной.

Разумеется, дело не только лишь в счетчике, а в возможности всей инфраструктуры и бюрократической системы принимать и обрабатывать такие платежи. Скажу честно, как обстоят дела в России, я не знаю. В 2019 году был принят закон о возможности микрогенерации с мощностью до 15КВт (http://personeltest.ru/aways/habr.com/ru/post/479836/), но работает ли это реально, и можно ли просто придти в магазин и купить новый счетчик с возможностью учета экспорта, сказать сложно. Тем более, не уверен, что в российских платежках за электроэнергию уже появился такой пункт как генерация. Можно лишь сказать, что в Европе это вполне работает, счетчики во многих домах и квартирах уже заменены муниципалитетом на новые. Для примера, в Голландии для регистрации солнечной электростанции достаточно уведомить государство, заполнив форму на сайте:



Разумеется, для балконной станции мощностью 100-200Вт это не так критично, большинство электроэнергии скорее всего и так будет потребляться внутри квартиры холодильником и прочими устройствами. Так что даже если у кого-то нет современного электросчетчика, проще рассматривать это лишь как благотворительный вклад в экологию даже если подарить городу несколько КВт*ч в месяц и заплатить за них, ну скажем, 50 рублей, вряд ли владелец от этого обеднеет. Проше считать, что эти деньги пойдут на развитие электросетей Конечно, если панелей реально много, то целесообразно ставить специальный grid-tie инвертор с так называемым лимитером датчиком тока, который ставится сразу после электросчетчика и ограничивает выработку инвертора, чтобы наружу ничего не отдавалось.

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



Кстати, вопрос о том, сколько можно заработать на генерируемом и продаваемом государству электричестве, экономически весьма непростой. В Германии например, вначале были введены специальные льготные тарифы для отдаваемой электроэнергии, что дополнительно мотивировало владельцев ставить солнечные панели. Но потом льготы стали отменять, и сейчас, это вроде бы уже не так выгодно. Аналогичный закон про зеленый тариф с реально высокими ценами за генерацию был принят в Украине, и это привело к значительному росту числа солнечных станций, но долго ли он продержится, неизвестно. Понятно, что в Украине хотят получить максимальную энергонезависимость от соседей, поэтому цены покупки электричества пока высоки. В России наоборот, генерируемое электричество будет покупаться дешевле чем потребляемое энергоресурсов в РФ и так в избытке, и желания у правительства мотивировать людей на установку солнечных панелей нет. Но в любом случае, речь идет об излишках то электричество, которое потребляется внутри дома, является прямой и чистой экономией, уменьшая платежи по обычному тарифу, а лишь излишки продаются государству. По идее, цель инсталляции солнечных панелей для домовладельца уменьшить до нуля потребление электричества от внешней сети, а возможность продажи излишков городу это лишь приятный бонус, но не самоцель.

Кстати, если говорить об окупаемости в настоящее время, то судя по немецкому онлайн-калькулятору, окупаемость для панелей на крыше площадью 31м2 составляет для Германии порядка 9 лет:



Заключение


Получение солнечной энергии это достаточно интересный хобби-проект, в плане приобщения к чему-то новому. Ведь как известно, лучший способ изучить новую технологию это попробовать её самостоятельно. Можно сколько угодно читать чужие статьи, но увидеть собственными глазами результаты работы, влияние угла наклона панелей, придумать защиту от ветра, сбор статистики, и так далее оно гораздо более интересно и дает гораздо больше опыта и понимания разных тонкостей.

В целом, результатами работы я вполне доволен. Стоимость проекта составила около 500Евро, что в плане затрат на хобби не астрономическая сумма, которая вполне сопоставима со средним игровым смартфоном или фотоаппаратом. 2 панели обеспечивают выработку от 10 до 180Ватт в зависимости от погоды и времени суток, что неплохо компенсирует работу разных домашних устройств, при этом даже если текущего потребления нет, излишки не пропадают а уходят в городскую электросеть.

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

Моя USB визитка

01.07.2020 22:06:22 | Автор: admin
Всем привет!

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



Цель проекта


Сделать максимально дешёвую версию USB Flash визитки, на которой бы содержалось моё резюме, исходный код и ещё несколько файлов. При этом использовать доступные комплектующие, которые можно заказать на LCSC и JLCPCB.

Что мне нужно было для этого сделать?


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

Что в итоге делает моя визитка?


Как можно догадаться, моя визитка имитирует маленький USB Flash накопитель, на котором содержится резюме на двух языках, исходный код, прошивка, гербер файлы, схемотехника и печатная плата, а также моё фото.

Основные узлы проекта


Микроконтроллер STM32F070F6 это был самый дешевый вариант контроллера с USB device на борту из тех что я нашёл;
Winbond W25Q32 последовательная флэш память на 4 Мб;
Светодиоды 0805 белый и синий цвета, для красоты;
LP3992 понижающий преобразователь напряжения на 3.3В, питание микросхем;
USBLC6-2 защита USB разъема от статики.

Мой подход к разработке пет-проектов


Есть определенный путь, которого я придерживаюсь программируя микроконтроллеры:
1) Не использовать SPL, HAL и другие библиотеки для программирования периферии. Этого правила я придерживаюсь и на основной работе;
2) Не использовать ОС. Я считаю что мои домашние проекты не настолько сложны, чтобы использовать этот полезный инструмент;
3) Не использовать Кучу. Касается только домашних проектов (нет необходимости);
4) Не использовать динамическое выделение памяти. Касается только домашних проектов (нет необходимости).

Проектирование


Компоненты
Для начала мне было необходимо подобрать компоненты для моей задумки т.к. помимо рассыпухи у меня ничего не было. Для этого я решил использовать сервис LCSC, как дешёвый и доступный вариант для домашних разработок. Как оказалось, по многим параметрам я вошёл тютелька в тютельку. Например, использовал все ножки микроконтроллера, уложился в оперативную память в 6кБ, из которых 4кБ у меня занял буфер для передачи данных с USB на SPI Flash, использовал ноги для USB которых физически нету на данном контроллере и т.п.

По цене в итоге вышло следующее:
Микроконтроллер STM32F070F6 -0.64$
Последовательная флэш память Winbond W25Q32 0.35$
Понижающий преобразователь LP3992-33B5F 0.04$
Защита от статики USBLC6-2SC6 -0.08$
Кварц 0.15$

Резисторы и конденсаторы использовал в форм факторе 0603, светодиоды 0805. Всё это у меня уже было, поэтому подсчитать их стоимость не представляется возможным. Однако можно с уверенностью сказать, что по цене всё уместилось в 1.5$ с запасом. Это не выглядит дешёвым по сравнению с проектом Хиллиарда, но и цены у меня не оптовые.

Печатная плата и схемотехника
Схему и печатную плату проектировал в Altium designer, скрины прилагаю.





Все компоненты обвязаны согласно даташитам. Микросхема флэш памяти запускается с контроллера через МДП транзистор.
Тут проблемой был USB разъем. Согласно спецификации USB он имеет толщину 2мм. Плату такой толщины заказывать дорого, да и для визитки не очень практично. Выход из ситуации я подсмотрел у одного человека, который делал музыкальную визитку. Он использовал плату 1мм, и делал на ней дополнительный разъем, который отрывается и запаивается поверх основного. Я поступил также.



Пайка
Пайка компонентов в основном проблем не вызывала. Всё запаял феном и паяльной пастой. Единственной проблемой для меня стало запаивание микроконтроллера. От паяльной пасты получалось слишком много клякс, паяльником такие ножки запаять тяжело. В итоге я просто промазал припоем все пады, а затем феном впаял микроконтроллер.

Программирование
Тут мне особо сказать нечего, программировал всё на низком уровне, на языке C++.Исходный код прикрепляю. Обращение к регистрам делал по статьям lamerok. USB реализовано с помощью средств микроконтроллера. Микросхема флэш памяти работает по SPI через DMA на самой высокой скорости. Диоды мигают по очереди по прерыванию таймера на 250мС.





Заключение


На этом думаю всё, основные моменты я описал. Если нужно что-то раскрыть более подробно (программирование USB, Flash или схемотехнику), то пишите, обязательно сделаю.

Всех благ!
Подробнее..

Запускаем камеру от телефона, или что делать, когда ничего не получается?

02.07.2020 02:23:39 | Автор: admin


Лет восемь назад работал я в одном а в прочем, не важно где. Делали мы там всякие разные интересные вещи. В том числе занимались системами технического зрения для роботов. Роботы были немного маленькие. И если привод для них маленький сделать не было для нас особой проблемой, то вот сделать миниатюрную цифровую камеру, которая не была бы размером с половину робота, было трудно (когда же мы наконец похороним PAL в таких разработках и везде будет цифра?). Если вам любопытно узнать, чем же всё закончилось, прошу под кат!


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

Изначально у нас был микропроцессор PXA300, в который втыкался SoC, от тогда еще Aptina, типа MT9D131 (JPEG сразу на параллельном выходе), но всё это потребляло много, а смысла в таком монстре было мало.
Проблема звучала просто робот привязан к компьютеру проводом на 100 мегабит. Оператор должен видеть цветную картинку хотя бы 640х480 в целых 15 FPS. Ставить в него камеру, которая гонит параллельный поток в XScale, в котором потом происходит тупое складывание картинки в буфер и передача кадра по сети, показалось слишком расточительным (ну серьезно, целый линукс на борту только чтобы перекладывать байты из одного интерфейса в другой?). Особенно учитывая целевое разрешение камеры. Нужно было решение проще. Плюс существенные ограничения по габаритам не позволяли поставить даже самый маленький объектив типа М12, он был просто конских размеров вместе с держателем.


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

реальный модуль,

кроватки на любой вкус.


Также удалось узнать, что есть такая штука как SMIA. Довольно быстро я нашел всю утекшую в сеть документацию на стандарт и стал его изучать. Оказалось, что есть три типоразмера модулей:


Name Length Width Height Option 1 Height Option 2
SMIA65 6.5 mm 6.5 mm 4.6 mm 5.8 mm
SMIA85 8.5 mm 8.5 mm 6.1 mm 7.1 mm
SMIA95 9.5 mm 9.5 mm 7.6 mm 8.6 mm

и по конструктиву нам подходит самый маленький SMIA65.
Поиск нормальных брендовых камер ни к чему адекватному не привел. Искать такую дичь у китайцев я еще не умел, а нормальные европейцы типа ST хоть и заявляли о том, что производят подобные модули (и даже документацию на которые можно было скачать без регистрации и смс), продавать свободно заинтересованной публике не желали. Обращение в наше представительство ST не помогло, и пришлось продолжать копать.


Слава ремонтникам мобильников! Схемы, платы, фотки! У них есть всё. После изучения документации и сопоставления доступных для покупки телефонов и запчастей выбор пал на камеру от Nokia 5250.
Сферическая нокия в вакууме.


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


Ох, какой же наивный я был. Это сейчас, пройдя весь путь от и до, я понимаю, что можно было существенно сократить время разработки, заказывая нормальные платы на нормальном производстве (правда, с оплатой тогда потенциально были некоторые проблемы, а ручки чесались сделать здесь и сейчас). Когда там появился JLCPCB или PCBWay? А тогда только ЛУТ на фольге, хлорное железо и два дня на всё про всё.
Signal Integrity? Вы делали гигабитные дифпары на двустороннем миллиметровом текстолите из платана лутом? А я делал. Даже импеданс считал.

0.5\0.2.


Ничо, работало (к сожалению, доступа к осциллографу, которым можно было бы посмотреть глазок, у меня не было). Сделал несколько тестовых плат. Намучился с пайкой. Экспериментировал с паяльной маской, даже сделал несколько плат. Одну успешно запорол, пропаяв её при помощи флюса ТАГС (ха-ха, какая ирония. Сейчас на сайте чипдипа написано про остаточное сопротивление. Эх, эту бы надпись тогда). На той плате ничего нормально не работало, даже источники питания. Между дорожками, которые ну никак не могли быть связаны, всегда присутствовало от десятков до сотен килоом. Причем плату после пайки мыл в семи водах. Поначалу я думал, что что-то успешно спалил. Начал отпаивать источники питания и всякую обвязку. Замыкания не проходили. Кончилось всё тем, что отпаял с платы вообще всё. Замыкания остались. Сделал вывод, что дело во флюсе, который затёк под маску и не вымылся вообще ничем. После этого я перестал делать платы с маской сам и паять их с флюсом ТАГС.


Но камера это еще полбеды. Надо же было чем-то принимать последовательный сигнал и разворачивать его в параллельный, чтобы запихнуть в STM32F217 (BlackFin? Не, не слышали. К моменту описываемых событий я прочно состоял в секте стм-оводов и конфигурил клоки через только появившийся экселевский файлик (кубоводы, привет!)). Да-да, четырехсотых стм-ок тогда еще не было (хорошо, хорошо, они только появились), и я успешно использовал кит от стартеркита на двухсотой серии. Опять же в результате длительного поиска и изучения рынка оказалось, что пути ровно два. Либо брать лэттисовскую плисину, для которой был апноут по преобразованию CSI в PCAM (так и не нашел его из 2012 года у себя в архиве, но точно помню, что был такой), либо покупать STSMIA832 (забавно, ST выпилили свою доку на преобразователь с сайта. У них теперь можно только апноутом разжиться.) и учиться паять BGA. Поскольку пайка была мне ближе и знакомство с потрохами плисов не входило в планы, да и сроки откровенно поджимали, я обзавелся несколькими микросхемами и купил готовые переходники с TFBGA25 в DIP (и да, макет был собран на беспаечной макетке и даже как-то работал).
Собственно, схема десериализатора совершенно банальна (не переживайте, ГОСТом тут и не пахнет, с тех пор я научился рисовать схемы посимпатичнее).

Схема, да.


Видите красные площадки? Это площадки подключения переходника из TFBGA25 в DIP.

Плата.


Но вернемся к камере. Чем прекрасна SMIA? Да тем, что поначалу казалось, что всё просто.

Красота же?


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

Такие подробности да в каждый документ бы!


Пока я занимался всем этим, SMIA_Functional_specification_1.0.pdf стал моей настольной брошюркой, зачитанной до дыр.
Но, чу, колченогое подобие драйвера (ах, какое громкое слово для заголовочного файла с несколькими функциями записи-чтения регистров камеры) написано, а в память микроконтроллера почему-то никакие байтики не падают. Хотя модуль через I2C успешно читается и записывается. О, сколько раз я думал, что неправильно сконфигурировал клоки или пропустил какую-то команду. Результата не было. Тогда я подумал, что хватит биться головой об стенку, надо делать что-то адекватное тому тупику, в который я угодил. Откуда я взял камеру? Правильно, из телефона. Телефон умеет показывать видео и делать фотографии с модуля? Умеет. Значит нужно тело на опыты. Драматичная нет история покупки донора для опытов лежит здесь.
Как бы там ни было, самым сложным было подпаяться к клоку и данным с модуля.

Ножки у кроватки под стенками, понимаешь.


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


Вы тоже заметили строки с вопросиками, которыми я пометил запись непонятно чего непонятно куда (пакеты 45-50)? Эти странные регистры, не описанные в, казалось, незыблемой документации (кто ж знал, что в Manufacturer Specific Registers [0x3000 0x3FFF] тоже надо что-то писать). Ну а дальше всё было очевидно. Пишем это непонятно что в непонятно куда и вуа-ля!


first_picture_ever.png


К сожалению не помню, есть ли тут дебайеризация (хм, J = demosaic(I,'grbg'); подсказывает, что есть) и если есть, то всё ли сделано правильно. Как и на втором кадре:

Камера лежала на боку.


Если повернуть голову набок, то можно прочитать 95% чего-то, рассмотреть рёбра какого-то радиатора, лежавшего на столе, и даже попытаться прочитать пароли на листочках с магнитной доски.
Почему всё такое зеленое? А пёс его знает. Этим вопросом уже занимался нормальный адекватный программист (Миша, привет!), который прикручивал эту систему к LwIP и проклинал меня за маленький объем SRAM и малую скорость 217-го (120МГц всего), который еле успевал перекладывать байты. По его словам, надо было просто правильно выставить усиление цветов каналов.


А дальше было дело техники. Нарисовать нормальную схему:

Ещё одна схема.


И сделать нормальную плату, которая показана на КДПВ.

Плата, да.


В производство, собственно, ушел вариант транслятора на отдельной платке (пришлось дорожки по 75мкм делать), чтобы не удорожать производство материнки, куда это всё напаивалось.


Времени было на всё угрохано больше года. Сейчас это кажется диким и ужасно непродуктивным. Наверняка, есть много людей, проделавших бы подобное за пару месяцев. Но я был один, в коллективе никто таким не занимался, интернеты упорно молчали, совета просить было не у кого. Но опыт есть опыт. Я ни сколько не жалею о потраченном времени. Полученные знания бесценны! Предполагаю, подобным образом можно раскурить многие другие модули, которые всё еще ставятся в телефоны, ведь, по сути, никакой особой магии в них нет. Знай себе записывай правильные байтики в правильные регистры да трассировку корректную делай.


А вы как думаете?

Подробнее..

Перевод Делаем звёздное небо на потолке при помощи оптоволокна и Arduino

02.07.2020 10:04:35 | Автор: admin




Хотите увидеть кусочек галактики у себя на потолке? Как это сделать рассказано ниже.

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

Немного о самом проекте. Я старался делать всё своими руками по-максимуму, что дало мне полную творческую свободу. В итоге у меня есть созвездия северного полушария, контроль над звёздными скоплениями при помощи пульта д/у (яркость и цвет), реакция на музыку, контроль подсветки, и, что самое важное возможность изменить всё, что угодно.

В качестве платформы для всего этого я выбрал Arduino, поскольку знаком с его программированием. За реагирование на музыку отвечал чип MSQ7EQ в интернете полно его описаний. Для связи я использовал завалявшийся у меня NRF24L01. Для управления большим количеством светодиодов хорошо подошёл контроллер сервоприводов PCA9685. Если вам хочется сделать что-то попроще и подешевле, вы можете поискать на Amazon готовые наборы, но если вам интересно делать всё самому, как мне тогда вам потребуются следующие навыки:
Знакомство с программированием Arduino.
Опыт разработки электрических схем и пайки.
Работа с переменным током.

Многие спрашивают у меня стоимость всего проекта. Довольно сложно дать конкретную цифру, поскольку я потратил много материалов, и всё зависит от того, какую часть вы решите делать самостоятельно, от размера проекта и т.п., однако, я думаю, что он может уложиться в вилку от нескольких сотен до тысячи долларов. Я работал над ним по выходным, и на всё про всё у меня ушло порядка года.

Шаг 1: планирование






Для начала нужно решить, покупать электронную часть или делать самому. Для изготовления схем требуется разбираться в Arduino и основах электроники, и кроме того, есть шанс где-то накосячить. На Amazon и в других магазинах можно найти множество наборов по фразе Fiber Optic Star Ceiling Kit, так что вариантов тут масса. Но если вам нужна полная творческая свобода и контроль, тогда лучше всё делать самому.

Определившись с электроникой, стоит подумать о структуре потолка, размере звёздной карты и количестве звёзд. Я выбрал вариант с обычным потолком из гипсокартона. У меня низкий потолок, и было достаточно сложно устанавливать оптоволокно, поэтому я остановился на относительно небольшом количестве звёзд, 1200 шт, однако результат всё равно получился потрясающим.

Теперь по выбору звёздного рисунка. Я живу в северном полушарии, поэтому выбрал ту часть неба, что здесь видна. Множество приложений демонстрирует рисунки созвездий я использовал Celestia (как в этой инструкции по изготовлению собственного звёздного неба). Естественно, никто не заставляет делать рисунок звёздного неба реалистичным и в верном масштабе тут у вас полная творческая свобода, а в интернете можно найти кучу идей.

Шаг 2: материалы


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

Материалы для самого потолка я перечислять не буду, тут всё зависит от используемой системы и других факторов. Я использовал потолок от Knauf. То же касается и инструментов большая их часть понадобится вам только для установки потолка. Для установки самих звёзд и электроники требуется не так уж много см. список. Много чего я купил в местных магазинах, а остальное заказал на AliExpress так дешевле, а качество обычно приемлемое.

Для звёзд и электроники потребуются:
Блок питания для светодиодных полос, мощность зависит от длины. В интернете можно найти ресурсы, помогающие подобрать БП для светодиодов. В моём случае у меня был импульсный БП на 12 В, 30 А, 350 Вт для ленты длиной 15 м. При этом ленты требуют по 14,4 Вт/м, поэтому у меня был хороший запас.
БП для светодиодов на 3 Вт. Зависит от количества светодиодов, а в моём случае это был БП на 5 В, 7 А, 35 Вт для 15 светодиодов и Arduino. Если вы выберете стандартные RGB светодиоды 5 мм, тогда можно взять БП попроще, однако звёзды будут уже не такими яркими.
RGB светодиоды на 3 В с общим анодом и радиатором (или обычные 5 мм светодиоды, если вам не нужна большая яркость). Один светодиод управляет одним скоплением звёзд, поэтому количество зависит от того, сколькими звёздами нужно управлять отдельно.
Светодиодная ленты 12 В.
Оптоволокно. Леска не подойдёт. Количество зависит от количества звёзд, размера потолка, местоположения управляющей схемы. Я для усиления эффекта использовал волокна разной толщины.
Платы PCA9685. С одной платы можно управлять 5-ю RGB светодиодами.
Arduino Uno/Mega 2.
NRF24L01 2.
USB-кабель для питания Arduino.
Логические транзисторы IRL540N. Количество зависит от количества полосок светодиодов. 1 шт на один цвет одной полоски. Ограничение длины полоски 5 м. Если нужно больше, используйте дополнительные полоски. Также существуют варианты соединения полосок в одну длинную смотрите в гугле.
Транзисторы 2N2222 (или другие n-p-n). На каждые 3 Вт одного цвета светодиодов нужно по транзистору. В моём случае это было 153.
Резисторы. 2 Вт 10 Ом / 2 Вт 6,8 Ом / 2 Вт 6,8 Ом для R, G и B на каждый светодиод 3 Вт соответственно. 5 притягивающих резисторов на 10 кОм, каждый по 0,25 Вт.
Конденсаторы на 10 мкФ для развязки NRF24L01.
Алюминиевая пластина для фиксации и охлаждения 3 Вт светодиодов.
Платы для схем.
Макетные платы для тестирования.
Шурупчики, фанера, клейкая лента и всё такое, что есть в любой мастерской.
Куча проводов разной толщины. Для ШИМ-сигналов можно использовать тонкие провода для прототипирования, но для светодиодных полосок и 3 Вт светодиодов толщину проводов нужно считать в зависимости от расстояния от схемы до светодиодов.

Для пульта д/у и спектрального анализатора:
MSGEQ7 1
Резисторы 1 470 / 1 180k / 1 33k .
Конденсаторы:1 33 пФ / 1 0.01 мкФ / 1 0.1 мкФ.
Термопаста для процессоров.
ИК пульт д/у и фотодиод для приёмника
Куча тонких проводов.
Небольшая макетная плата. Я использовал Proto Shield.
Небольшой корпус для Arduino. Я сделал корпус при помощи лазерной резки.
Другие компоненты, относящиеся к основной схеме. Их количество указано в списке компонентов основной схемы.

Инструменты для установки и пайки:
Прозрачный клей, не растворяющий оптоволокно. Я использовал простой канцелярский.
Оборудование для пайки.
Мультиметр не будет лишним.
Отвёртка.
Пассатижи.
Шило или что-то похожее для проделывания отверстий в потолке. Толщина совпадает с толщиной оптоволокна.

Шаг 3: установка потолка




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

Специально для обслуживания электроники я сделал лючок в самой малозаметной части потолка.

На этом шаге делается шпатлёвка и грунтовка, а покраска уже после установки оптоволокна.

Шаг 4: установка оптоволокна














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

Соображения:
Рекомендую приклеивать оптоволокно в отверстиях, чтобы оно не выпадало. Клей должен быть прозрачным и не реагировать с оптоволокном. Я использовал простой канцелярский.
Сверлить ничего не надо, отверстия можно проделать простым шилом, совпадающим по диаметру с оптоволокном.
Для разметки точного местоположения звёзд я использовал только рулетку. Не на 100% точно, но нормально. Для печати карты звёздного неба потолок был великоват.

Шаг 5: окончание отделки потолка покраска



Мы красили прямо по оптоволокну, поэтому когда оно не светится, его не видно. Всё выглядит как обычный потолок. После двух слоёв краски яркость оптоволокна осталась почти такой же.

Шаг 6: пробная схема










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

Моя тестовая версия это одна-две платы PCA9685, NRF24L01 и блоки питания, соединённые с Arduino. Всё можно делать на макетных платах. То же касается и схемы пульта д/у натыкали всё на макетку, и проверили, что всё работает. Я бы также посоветовал припаять несколько 3 Вт светодиодов для проверки.

Шаг 7: код для Arduino




Библиотеки и другие полезные ссылки я собрал в разделе полезная информация. Объяснения по работе кода содержатся в комментариях к нему.

Я писал этот код, используя различные ресурсы, некоторые из которых я перечислил в разделе полезная информация. Однако поскольку я закончил проект уже больше года назад, к тому времени, когда я решил написать эту статью, я уже не смог найти некоторые из них, а некоторые сохранённые ссылки уже не работали.

В коде содержится довольно сложная функция для мигания светодиодами. Для улучшения внешнего вида я использовал обучающий материал, где описано, как сделать дышащее мигание: sean.voisen.org/blog/2011/10/breathing-led-with-arduino

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

Код приёмника

Код передатчика

Шаг 8: подключение проводов и светодиодных полосок



















Время финального подключения! Если всё проверено и всё работает, подключение должно пройти без особых проблем просто пайка кучи одинаковых комплектующих. Для удобства обслуживания всей схемы я сделал коробку из фанеры по размеру технического лючка поэтому при необходимости я могу просто вынуть всю схему из потолка. Оптоволокно я провёл по пластиковым сантехническим трубам, размер которых примерно совпадает с 3 Вт светодиодами, а потом просверлил отверстия того же диаметра в фанере и вставил их туда. Таким образом я легко могу отсоединять оптоволокно от светодиодов по необходимости.

Светодиодные полоски предлагаю крепить на алюминиевых пластинах для охлаждения, поскольку они сильно греются.

Шаг 9: отладка и тонкая подстройка




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

Когда я понижал яркость светодиодов до минимума, полоски могли перестать работать или начать мигать. Потратив огромное количество времени на исследования и отладку, я обнаружил, что проблема была в медленном переключении IRL540, а решение в простом понижении частоты ШИМ до 50 Гц. Проблема почти решилась, и мигание осталось только на самых низких величинах однако это не имеет значения, поскольку я их не использую. Проблема вернулась, когда я решил снять ролик об этом потолке, поскольку такую небольшую частоту хорошо видно на камерах это всё равно, что снимать телевизор. Для решения этой проблемы я собрал на макетной плате небольшую схему, использовав транзисторы 2N2222 вместо IRL540, просто для съёмки видео.

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

Шаг 10: полезная информация и ссылки


MSGEQ7


www.sparkfun.com/datasheets/Components/General/MSGEQ7.pdf

www.baldengineer.com/msgeq7-simple-spectrum-analyzer.html

rheingoldheavy.com/msgeq7-arduino-tutorial-01-getting-started

www.instructables.com/id/How-to-build-your-own-LED-Color-Organ-Arduino-MSGE

Nrf24L01


arduinoinfo.mywikis.net/wiki/Nrf24L01-2.4GHz-HowTo

PCA9685


learn.adafruit.com/16-channel-pwm-servo-driver/overview

github.com/adafruit/Adafruit-PWM-Servo-Driver-Library

ИК пульт д/у


github.com/z3t0/Arduino-IRremote

Шаг 11: идеи для развития


Было бы классно разработать мобильное приложение для управления потолком, возможно, используя OpenHAB на Raspberry Pi, поскольку PCA9685 управлять через RPi довольно легко.

А если использовать OpenHab или его альтернативу, то звёздный потолок легко можно соединить с системой умного дома.
Подробнее..

SonarTermen v2.0

02.07.2020 12:23:15 | Автор: admin


Если ваш маленький ребенок проявляет бурный интерес к музыкальным игрушкам, сделайте ему нечто подобное.
У меня ушло порядка 3х дней. Имея все необходимое + мои наработки вы справитесь за пару часов.

Терменвокс с ультразвуковым сенсором уже давно бродит по просторам интернета. Есть даже вариация с оптическим сенсором.
У всех у них есть недостатки: при высокой чувствительности к изменению положения руки сложно попасть в ноту хуже чем на скрипке, при квантовании пространства возникают срывы, хлюпания итп.
Я решил эти проблемы + появился интересный эффект, задающий ритм.

В качестве мозга я использовал китайскую Arduino Nano. Корпус коробка от DVD дисков (а куда ж ее еще?). Все собрано как прототип.

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


Хочу обратить ваше внимание на качество проводов к сонару HC-SR04. Они должны иметь хороший контакт, иначе постоянно сбоит.
Мне пришлось допилить напильником соединительные провода. В идеале должна быть пайка.


Выключатель держится за счет собственных упоров. Главное аккуратно вырезать отверстие по размеру его корпуса.


Добавил немного пористого полиэтилена для того, чтобы батарея не болталась.


Схема устройства


Игрушка воспроизводит до-мажорную гамму первой октавы (+до второй октавы) с интервалами 9 см на ноту. Интервал можно регулировать.
//Минимальное расстояние от сонара. Начало ноты До. Сантиметры.#define MIN_SM 1//Расстояние между нотами. Сантиметры.#define SM_PER_NOTE 9//Переключатель Мажор/Минор#define GAMMA_PIN PC2

Есть возможность переключения на до-минор.
В моей поделке не реализован аппаратный переключатель лада. Вы легко можете его сделать, соединяя PC2 с землей.

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



Игрушка хорошо работает от одного элемента 18650 (4,2В max вместо 5В).

Проект в Proteus и исходный код (+BONUS) в AtmelStudio для повторения и дальнейшей доработки вы можете скачать с моего github.
Несомненным положительным эффектом игрушки будет тот факт, что ребенок не просто нажимает на кнопку и слушает композицию, а двигает руками, телом, творит, развивает координацию и слух.

Как знать, может у вас растет будущий Паганини.
Подробнее..

Ракета от Амперки, часть 1 Теория ракетных двигателей. Карамельное топливо

04.07.2020 12:05:52 | Автор: admin

Вступление


Всем привет! Мы команда ютуб-канала Амперки, в студии и пилим видео по проектам и железкам. Однако, в какой-то момент все изменилось.

Под катом история постройки нашей ракеты.


Шла весна 2020 года и карантин самоизоляция не щадила никого. В том числе и нас, отлученных от студии, дабы не подвергались опасности заражения заморской бациллой. Вот в этот-то период и начали активизироваться в голове старые идеи сделать то, что давно хотелось, но что было отложено в долгий ящик когда время будет. Наконец, то_самое_время пришло, и из того самого ящика была извлечена мысль о постройке собственной ракеты, еще и подстёгнутая недавним успешным пуском в эксплуатацию батута от SpaceX.

Так как сделать такой серьезный проект за один заход не получится, разделим его для удобства на составные части (список будет пополняться по мере работы):
  1. Часть 1. Теория ракетных двигателей. Карамельное топливо

Ракетостроение, в целом, наука комплексная, сложная и многогранная. Релевантного опыта у нас не было, не кончали мы институтов по этому направлению, но есть руки, голова, желание а это уже многое, так что, как говаривал Юрий Алексеевич, поехали.

Теория ТТРД


Что такое реактивное движение, (для тех, кто, вдруг, не в курсе) много говорить не будем: если в двух словах, то это движение за счет отброса массы в противоположную сторону от направления движения. Про всякие экзотические конструкции двигателей типа ядерных, ионных и иже с ними говорить не будем одна не предназначены для работы в атмосфере, другие слишком сложны и не воспроизводимы в любительских условиях и т.д., поэтому остановимся на простых, но доступных простому обывателю конструкциях, которые при желании можно повторить практически в домашних условиях, а именно химических. В таких двигателях реактивная струя получается за счет химической реакции топлива и окислителя (в некоторых случаях роль окислителя может играть атмосферный кислород).

Итак, химические двигатели (ХРД), по агрегатному состоянию топлива классифицируются на жидкостные (ЖРД) и твердотопливные (ТТРД), так что выбирать будем из них. ЖРД весьма удобны, так как позволяют управлять тягой, однако требуют применения в своей конструкции сложных систем форсунок в камере сгорания и не менее сложных систем подачи топлива. Одно только проектирование ЖРД, даже самого примитивного, займет у нас месяцы, а, следовательно, это не наш вариант. Альтернативой могут стать ТТРД за счет простоты своей конструкции и значительно меньшими требованиями к топливу. Да, у нас не выйдет точно дозировать тягу. Точнее, мы ее совсем не сможем дозировать. Однако, есть некоторые аспекты, на которых мы можем сыграть, об этом и пойдет речь дальше.

Виды смесевого топлива


Самым первым, и, соответственно, примитивным топливом для ракет был порох: сначала дымный, а затем и бездымный. Китайцы, придумав эту горючую смесь, быстро догадались, что она не только может делать бух и много света, а еще и толкать снаряд, постепенно сгорая внутри него. Толку от него, конечно, мало, годится только для фейерверков, да и удельный импульс оставляет желать лучшего. Эволюцией бездымного пороха стали гомогенные (однокомпонентные) составы на основе нитроцеллюлозы. Они достаточно неприхотливы в хранении и эксплуатации, а также достаточно экологичны, однако имеют все тот же недостаток в виде слабого удельного импульса.

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

Расчет двигателя


Важнейшая характеристика твердого топлива это скорость его горения, зачастую это значение константа для определенного состава топлива. Горение распространяется по поверхности. Если просто поджечь конец цилиндрической топливной шашки, то мы получим торцевое горение, которое даст длительное равномерное прогорание, однако, получить при этом достаточную тягу для подъема ракеты в воздух не выйдет. Для повышения эффективности нужно сделать в топливе канал, по которому будет распространяться горение, повысив тем самым его площадь. Также нужно учитывать, что по мере выгорания профиль канала будет меняться, следовательно, будет меняться эффективная площадь. Можно, конечно, долго экспериментировать с различными профилями, однако, это все уже сделано до нас и упаковано в удобный программный инструментарий.

В программу можно внести все необходимые параметры и получить графики тяги, которую будет развивать ракета. В графе Grain configuration под знаком вопроса есть описательный мануал по различным профилям канала.



Опытным путем, применяя различные конфигурации канала мы нашли оптимальные параметры для нашей ракеты. Для получения таких же показателей нужно ввести такие значения:

Форму канала мы выбрали Moon burner. Умный Meteor c учетом введенных данных построил нам вот такой график:

Из этой диаграммы понимаем, что двигатель со старта получит хороший пинок и будет развивать весьма неплохую тягу на протяжении всего времени работы. По расчетам программы пиковое значение тяги получилось без малого 312 Н при пиковом давлении в 24.5 бар. Средние значения оказались около 265 Н и 19.5 бар соответственно.
Еще одним неоспоримым плюсом программы является возможность прямого экспорта рассчитанных значений в другую не менее полезную для нас программу OpenRocket, при помощи которой мы будем рассчитывать стабильность ракеты, оперение, балансировку и другие важные показатели, но это будет уже в следующей серии.
Однако, не топливом единым жив начинающий ракетостроитель. Не менее важное значение имеет сопло. По этому принципу РД делятся на сопловые и бессопловые. Последние, технически, имеют дозвуковое сопло, являющееся, по сути, просто отверстием или конусом в нижней части двигателя. Дозвуковым оно называется по той причине, что истекающие через него газы не могут достигать, а уж тем более, превосходить скорость звука, сколько бы не наращивалось давление в камере сгорания, об этом нам говорит гидродинамика. А против физики, как известно, не попрёшь. Тем не менее, такие сопла за счет своей простоты применяются в малых любительских ракетах, а также в фейерверках. Но мы же делаем ракету, значит, дозвуковые сопла не наш путь.
Альтернативным решением является сверхзвуковое сопло или, как его еще называют по имени изобретателя, сопло Лаваля. В упрощенном варианте представляет собой два усеченных конуса, сопряженных узкими концами. Место сопряжения называется критической точкой.

Принцип его действия напоминает принцип, на котором работает холодильник: газы, проходя узкое горлышко и попадая в бОльший объем резко охлаждаются, за счет чего уменьшается их объем, что приводит увеличению скорости их истечения. В результате, за счет перепада диаметра выпускного отверстия мы получаем на выходе струю газа, движущегося со сверхзвуковой скоростью. Таким образом, применив сопло Лаваля мы значительно повышаем КПД ракеты.
К слову, Meteor проводит расчеты, подразумевая, что на двигателе установлено как раз сверхзвуковое сопло, расчет и изготовление которого также оставим на следующий выпуск.
Итак, характеристики, параметры и габариты двигателя у нас есть, можно приступать к варке топлива.

Изготовление топливных шашек


Первым топливом у нас будет карамельное, готовить будем из сорбита и калиевой селитры. Сорбит можно купить в аптеке, он используется как сахарозаменитель. Калиевую селитру можно найти в садово-огородном отделе, но там она довольно грязная, поэтому купили ч/чда в Русхиме.
Простейший способ измельчить компоненты до состояния мелкодисперсного порошка и смешать, но тогда топливо остается сыпучим и не будет держать форму. Решено сплавить компоненты вместе. Придется делать нагреватель с контролем температуры и песчаная баня, для которого нам понадобятся:

Из плиты выбрасываем ее родной регулятор и ставим в разрез твердотельное реле, управлять которым будем через Ардуино, к которой подключим дисплей и потенциометр, чтобы видеть текущую температуру и иметь возможность ее настройки. В форме для выпекания проделываем отверстие и вставляем термопару. Заполняем форму примерно наполовину песком солью (песка под рукой не оказалось, зато рядом был продуктовый магазин, на качество это не повлияет). Это нужно для создания среды с большой тепловой инерцией. Кстати, соль лучше брать экстра, так как более крупная при нагреве начинает раскалываться и стрелять в разные стороны, устраивая Сталинград. В центре солевой бани устанавливаем выпарительную чашу, предварительно положив под ее дно щуп термопары. Контролировать процесс будем через первый попавшийся релейный регулятор для Ардуино. Проверяем пирометром разность температур между показаниями термопары температуры чаши, вносим соответствующие коррективы.
Meteor заботливо подсчитал массу топлива, которая составила 838г, возьмем с запасом, еще пригодится. Решено было сделать топливный заряд из нескольких шашек для простоты их изготовления. Потом можно будет их просто склеить между собой и вставить в корпус двигателя.
Возьмем по массе 65% калиевой селитры и 35% сорбита, аккуратно засыпаем в чашу и добавляем немного воды. Это и нервы успокоит, и избавит от необходимости измельчать компоненты в пыль, так как в воде они и без того хорошо растворятся и смешаются. Ставим на огонь, выставляем температуру и ждем, постоянно помешивая. Постепенно полученная каша расплавится и станет похожа на овсянку. Надо дождаться выпаривания всей лишней воды (это можно будет понять по прекратившемуся выходу кипящих пузырьков).
Дальше надо действовать решительно: в заранее подготовленную водопроводную ПВХ-трубу, зафиксированную в держателе с внутренним креплением под круглую ось будем запрессовывать топливо. После извлечения оси у нас как раз останется канал запала по всей длине шашки. Запрессовывать удобно при помощи держателя для дрели, такой очень удачно нашелся в студии. Важно запрессовать топливо таким образом, чтобы внутри шашки не оказалось пузырей и полостей, иначе это потом негативно скажется на горении.
Трубу с топливом откладываем и оставляем до остывания. Затем ее можно будет распилить и достать шашку. Мы сделали несколько штук, одну из них сожжем в целях эксперимента.

В следующем выпуске займемся корпусом двигателя, соплом и испытательным стендом.
А пока мы его готовим, рекомендую почитать следующую книжку про проектирование ЗУРов. Из нее была почерпнута бОльшая часть информации.
Всю серия целиком:
Подробнее..

Категории

Последние комментарии

© 2006-2020, personeltest.ru