...

суббота, 20 июня 2015 г.

[Перевод] 2Mb веб-страницы — кого винить?

Я надеялся, что это было временно. Я надеялся, что 2015 год будет годом производительности. Я ошибался. Средний вес веб-страницы возрос на 7.5% за пять месяцев, превысив 2Mb. Для этого же потребуется три 3.5-дюймовые дискеты двойной плотности!

Согласно отчёту на HTTP Archive за 15 мая 2015, статистика, собранная на почти половине миллиона веб-страниц, такова:

технология конец 2014 май 2015 увеличение
HTML 59Kb 56Kb -5%
CSS 57Kb 63Kb +11%
JavaScript 295Kb 329Kb +12%
изображения 1,243Kb 1,310Kb +5%
Flash 76Kb 90Kb +18%
другое 223Kb 251Kb +13%
всего 1,953Kb 2,099Kb +7.5%

Наибольший рост наблюдается среди CSS, JavaScript, других файлов (в основном шрифты) и, неожиданно, у Flash. Среднее число запросов на страницу:
  • 100 файлов в общем (было 95)
  • 7 файлов со стилями (было 6)
  • 20 JavaScript файлов (было 18)
  • 3 файла со шрифтами (было 2)

Изображения остаются наибольшей проблемой, насчитывая 56 запросов и составляя 62% от общего веса страницы.

И, в конце-концов, помните, что это среднестатистические данные. Многие сайты весят значительно больше.


Немного театрально, но всё-таки есть хоть кто-то, кто сочтёт 2Mb приемлемыми? Это сайты публичного просмотра — никаких action-игр или тяжеловесных приложений. На некоторых может применяться клиентский фреймворк, который может придать больший вид 'отдельной' странице, но таких сайтов должно быть меньшинство.

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

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

Мои клиенты хотят проектировать ПО и разрабатывать представления (views), поскольку хотят реализовать своё собственное видение. У них есть новаторские идеи, на которых можно заработать миллионы — как только каждая из 1001-й «жизненно важной» фичи будет реализована в коде. И не важно, насколько велик проект, клиент всегда хочет большего. Они:

  1. ошибочно полагают, что большее кол-во функциональности привлечёт больше клиентов;
  2. заставляют своего разработчика лучше оправдывать денежные затраты;
  3. ничего лучшего придумать не могут.

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

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

Но мы уступаем требованиям клиентов.

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

Мы продолжаем отдавать предпочтение функциональным возможностям вместо производительности. Добавить какую-нибудь ерунду несложно и это делает клиента счастливым. Но пользователям не нравится взаимодействовать с web; они жаждут родных мобильных приложений и Мгновенных Статей Facebook. Более того, разработчики знают что это не правильно: Веб против родных приложений: давайте признаем поражение.


Трудно спорить с клиентом, который предлагает плату за ещё один набор бестолковых фич. Клиенты больше фокусируются на своих нуждах, нежели на нуждах своих пользователей. Большее количество рекламы на странице даёт больше доходов. Демонстрация навязчивых pop-up даёт большее количество регистраций. Лучше презентовать 20 продуктов чем 10. Эти методы работают до определённого момента, но как только вы перейдёте черту приемлемости, пользователи покинут сайт. Что инстинктивно делают клиенты при падении доходов? Они добавляют больше всякой всячины.

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

Исторически Microsoft разрабатывал ПО при помощи комитета. Множество людей высказывали множество мнений по поводу функциональных возможностей. Плюсы: ПО Microsoft предоставляет каждую мыслимую возможность и весьма хорошо конфигурируемо. Минусы: люди используют лишь часть этой мощи, к тому же это может стать слишком сложно — например, семнадцать способов выключения в Vsita или малопонятное диалоговое окно с опциями Интернета.

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

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

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


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

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

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

Настало время отдать приоритет производительности.

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Программирование для начинающих. Моё знакомство с Processing

Доброго времени суток, уважаемые.

Цифровые электронные самоделки часто взаимодействуют с компьютером. Передают данные, либо управляются с него. В свете этого всегда был интерес к программированию.
Мой прошлый опыт в этой области связан с интерпретатором бейсика ZX Spectrum, Qbasic’ом в рамках хобби юности и Си-86 в рамках студенчества. Были попытки освоить ECMAScript в рамках интереса к языку VRML. Встал вопрос, что выбрать сейчас?

Прошу под кат.
Чтение статей о бурно развивающихся языках вызывало грустные мысли. Все эти Го, Дельфи, Перлы, PHP, Java. Про питонов, так я вообще до последнего времени был уверен, что это вид змей. В общем, обилие и разнообразие. В котором мне, как начинающему, трудно понять, кто есть кто, и для каких задач. Чем, например, отличается Java от JavaScript?

Мои бесплодные размышления продолжались бы и дальше. Но прочитал пост ресурса Изокод о их цикле статей «Полуостров Бинария». О языке Processing. Заинтересовался. В рамках интереса к Ардуино несколько раз встречал упоминания о нём в туториалах. Были и сомнения. Ведь для него требуется Ява машина. С этим творением корпорации Oracle сталкивался при установке одной из любимых игр сына Minecraft. И с ним были проблемы. Домашний комп старенький, на атлоне 1,3. Оперативной памяти пол гига (сейчас уже полтора). И крайнюю версию этой игры запустить невозможно. Ибо требуется крайняя версия Явы, которая на этом железе уже не работает.

Сомнения плана: вот не будет работать Ява, по разным причинам. От железа до санкций. И что потом делать? Или, почему мой браузер Файерфокс верещит об обнаружении опасности, и прозрачно намекает «Братан, в общем, я тебя предупредил…»

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

Итак. Установил среду. Ява уже стояла. Запустил. Всё знакомо по Arduino IDE. Набрал один пример из статьи. Отрисовка кучи жёлтеньких кружочков с рандомными координатами. Хм… Синтаксис до боли знаком… Да это же Си! Запустил. Поразила скорость выполнения программы. И это на моём дохленьком Samsung NC-110, с гигабайтом ОЗУ и жалким подобием графической подсистемы. Любопытство русского мужика из анекдота про японскую бензопилу потянуло на эксперименты. Увеличил в 10 раз количество точек в цикле. Вжик, сказал мой скромный трудяга. Хм… Увеличил в 100 раз… Вввжик, повторил он. Увеличил в 1000 раз. Тут уже мой нетбук ощутимо просел по скорости исполнения кода. Но тем не менее я остался доволен. Стал грузить примеры. Те, что с графикой, конечно притормаживали. Попробовал скомпилировать один из примеров в ЕХЕшник. И тут выяснилось, что скомпилированный в EXE скетч не запускается. Выскакивало два окна.

В одном «C:\ProgramData\Oracle\Java\javapatch\javaw.exe Неизвестная ошибка».

Второе с заголовком «Processing error», с текстом «Error calling ShellExecuteEx()».

Дальше были классические действия начинающего программиста. Фары протёр, колёса попинал. Обшарил сайт processing.org, попытался найти чат программистов. Но там оказались не программисты, а юные озаботики. Отчаявшись найти ответ, стал размышлять над извечным русским вопросом. И вот так бывает! Правильно сформулированный вопрос содержит половину ответа. И свою проблему я решил. Я ж искал не там! В Program Files. А требует не Program Files, а ProgramData! Папка оказалась с атрибутом хидден. Залез. Там нашлись и требуемые подпапки, но! В папке javapath лежат не сами файлы, а ярлыки на них. Не смог отредактировать свойства, в частности указать рабочую папку. Полез в папку Явы. C:\Program Files\Java\jre1.8.0_25\bin\ Там нашёл требуемую программу, и создал с неё новый ярлык в C:\ProgramData\Oracle\Java\javapatch\ Это не помогло. Тогда просто скопировал её, и другие, на которые были ярлыки, в эту папку. И всё заработало!

Читая с сыном первые главы, разобрались с отрисовкой кружочка, его закраской желаемым цветом, задействование мыши, координат курсора и кнопок. Сыну очень понравилось рисовать мышкой. Он и до того баловался в программах раскрасках, фотошопе. А тут. Рисование в написанной нами программе! Требовал от мамы посмотреть. И у меня родилась идея. А почему бы не попробовать написать графический редактор а-ля паинтбраш?

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

На данный момент их уже почти 300. Но не потому, что программа стала супер навороченной. А потому, что стараюсь уйти от быдло-стиля написания. После, в этой мешанине даже самому невозможно разобраться. Что сделано? Сделаны панели выбора цвета и размера кисти. И к ним кнопочки вызова. Количество образцов цвета теперь 8. По 4 на левую и правую кнопку мышки. К каждому образцу своя кисточка. Пока лишь диаметр пятна. В планах масштабирование пятна по X и Y, расстояние между мазками. Левой кнопкой выбирается цвет для изменения, правой — активные для лев. прав. кнопок при рисовании. Не закончено сохранение изображения в формате BMP. Честные 16+ миллионов оттенка цвета. Не хватает числовой информации. Например, о размере кисти, о координатах курсора в поле рисования. Это тоже в планах. Ещё в планах пипетка для определения цвета, штамп для копирования, размытие — его сильно не хватает при рисовании. Различные формы пятна от кисточки, различные алгоритмы закрашивания. Основное — равномерный тон, без многократного наложения мазка в одной точке, даже при прозрачности цвета. Не хватает векторов для рисования прямых. А ещё безумно не хватает Undo. Сделана более чёткая логика работы. Теперь цвет не сбивается, если вылетаешь из поля рисования на панель выбора цвета. Как и не сбивается размер кисточки. Но и есть ошибки. Пока не исправленные. Но и не проявляющиеся явно. Функции в этом языке, как и в Си. Функции это подпрограммы. Очень удобно. Написал функцию, получающую параметры при вызове. Она выполнила предписанное, вернула результат. Причём предписанное предписано не жёстко, а может выбираться в зависимости от значений параметров при вызове. Или не вернула ничего, но изменила значения глобальных переменных. Или перерисовала что-то на экране. Гуру говорят, что глобальные переменные зло. Но мне пока без них никуда. Изначально моя программа была монолитной. И самой сложной частью была проверка условий, где курсор, что нажато. Когда это было 30 строк кода — всё было наглядно и понятно. Но с увеличением количества «фич» стало появляться всё больше и больше повторяющихся кусков кода. И я решил использовать функции. И тут всё сломалось. Стройная монолитная программа взорвалась на пару десятков отдельных кусков — функций. Очень хотелось поддерживать работоспособность программы постоянно. Пугала возможность допустить ошибку, и уже не разобраться, что, откуда, куда, зачем? Но постепенно всё нормализовалось. И даже более того. Оптимизировалось! Теперь одна и та же функция как изменяет цвет, так и параметры кисти. И, наверняка, она же будет применена для других панелей, если появятся. Другая универсальная функция перерисовывает положения ползунков на скроллбарах. Третья отображает 8 цветов, выбранный для изменения цвет, активные цвета для кнопок мыши. В процессе тестирования стали выявляться ограничения. Разрешение создаваемых изображений 72 пикселя на дюйм. И при малейшем увеличении это сразу видно. Но ведь редактор позиционировался а-ля ПаинтБраш, а не а-ля фотошоп. Стоит отметить, что я прочитал всего 3 главы книги «Бинария». И, считаю, что обладая столь ограниченными знаниями, результат всё же неплохой. Листинг программы будет прикреплён в конце статьи. Алгоритм ясен из комментов. Я пишу обычно по ночам. Когда мой ненаглядный главный бета — тестер спит. Не отвлекает внимание на свои интересы. А их немало. Вижу смысл сказать вот что:

1) Всё, буквально всё, надо комментировать! Не только, что какая функция делает, но буквально каждую строчку! Иначе следующей ночью смотришь на собственный код, как баран на новые ворота. Понимаешь, что хочешь, но не понимаешь, в каком месте, что нужно изменить, чтобы реализовать. Код работает. Но ты не понимаешь, как! Надо бекапить. Отладил что-то в программе. Программа работает. Надо сделать копию — бэкап. Чтобы, если всё пойдёт совсем плохо, иметь возможность вернуться. Это особенно важно при внесении больших изменений. Изменении логики работы, добавлении больших функций.

2) С желаниями надо быть последовательным. Идеи приходят табунами. Начинаешь их обдумывать — теряешь мысль, что и для чего делаешь сейчас.

3) Нужно планировать. Программа должна начинаться не с ядра, которое работает. А с задач, которые она будет решать. После эскиз интерфейса, после блок — схемы алгоритмов. Как основной программы — в общих чертах, так и подпрограмм — в деталях. Так проще разобраться, что, куда, откуда, зачем. Иначе — неизбежное переписывание до 60% кода! Когда уже реализованное кардинально изменяется под новую структуру программы, под придуманные универсальные функции.

4) Четвёртое, но на самом деле главное. Надо писать! Надо пробовать, ошибаться, разбираться, исправлять. Никогда не получится прочитать сотню книг по теме, а потом сесть, и написать программу «от и до». Но это, конечно, моё личное мнение. Мнение дилетанта. И это четвёртое не противоречит третьему. Даже имея чёткий план будущей программы, написать сразу и без ошибок не получится.
Что сказать ещё? Очень смешанные чувства. С одной стороны растущая уверенность в собственных силах, с другой осознание, что программа стремительно растёт. С одной — желание и дальше совершенствовать своё «дитя». С другой — осознание, что оно вряд ли когда либо превзойдёт возможностями монстра Фотошопа. А значит, важен не результат, а лишь процесс. Закрепление в голове изученного. Дабы позже быть способным объяснить сыну. Помочь в изучении языка. Осознание, что надо читать следующие главы «Бинарии». Что на данный момент написанное — мой предел. Но далеко не предел этого языка.

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

Пара рисунков главного бета — тестера, сделанных в процессе работы над программой.

А ещё скрин-дамп работы его программы. Рисующей девять пасхальных яичек. Каждый раз новыми оттенками цвета. Очень простой программы. Меньше десятка строк. Но назначение каждой команды и переменной в ней он сможет объяснить сам!

Листинг программы:

boolean ris = false; // лог. рисуем или нет
boolean sel_col = false; // лог. выбрана панель цветов или нет
boolean sel_kist = false; // лог. выбрана панель кистей или нет
int num_color = 0; // Изменяемый цвет. 8 цветов. Выбран первый
int color_active_left = 0; int color_active_right = 1; // выбранные для кнопок мыши цвета
int num_panel; // для какой панели вызывается функция изменения значений

// 4 массива по 8 элементов. RGB наборы 8 цветов
int [] colr = new int [8]; int [] colg = new int [8];
int [] colb = new int [8]; int [] proz = new int [8];

// 4 массива по 8 элементов. Размеры кистей, деформации по X и Y, расстояние между следами
int [] r = new int [8]; int [] scale_x = new int [8];
int [] scale_y = new int [8]; int [] distanse = new int [8]; 

// диаметр кисти, время задержки повторного рисования, выбр набора от лев и прав кнопки мыши
int zader = 1000000000; int nabor;

void ris_main_menu () // главное меню
   {
     fill ( 20,  20,  20);  rect ( 0,   0,  65, 600); // поле панели инструметов
     // кнопка выбора панели цветов
     fill (20, 20, 20); stroke (200, 200, 200); rect (3, 5, 15, 15); noStroke ();
     fill (255,   0,   0); rect ( 5,   7,   3,  11);
     fill (  0, 255,   0); rect ( 8,   7,   3,  11);
     fill (  0,   0, 255); rect (11,   7,   3,  11);
     for (int a= 3; a < 14; a++) 
    {// градиентная полоска прозрачности в кнопке выбора панели цвета
     int b = a * 17;
     stroke (b, b, b); line (15, 20 - a, 16, 20 - a); 
    }
    // кнопка выбора панели кисточек
    fill (20, 20, 20); rect (22, 5, 15, 15); fill (200, 200, 200); ellipse (29, 12, 8, 8); 
    fill (20, 20, 20); rect (41, 5, 15, 15); fill (200, 200, 200); rect (44, 8, 9, 9);
    // кнопка сохранения изображения на диске
    fill (50, 50, 50); rect (47, 11, 3, 3); stroke (50, 50, 50); line (48, 9, 51, 9);
   }
   
void disp_sel_col () //индикация выбранных цветов для прав и лев кнопок мышки 2 столбца по 4 цвета
{
 cls_sel_col (); // очистка области выбранных цветов
 noStroke (); fill (0, 0, 0); // нет обводки, чёрная заливка
for (int i = 65; i <= 119; i = i + 18) // рисуем в цикле 8 прямоугольников под цвета по 2 за итерацию
{
 rect (5,  i, 21, 16); rect (35,  i, 21, 16);
}
 for (int i = 0; i < 8; i+=2) // заливаем в цикле 4 нечётных прямоугольника цветами из массивов
 {
 if (num_color == i) // если цвет выбран для редактирования - обводим белым прямоугольником
   {stroke (200, 200, 200); }
   else {noStroke ();} // иначе - не обводим цвет 
 fill (colr [i], colg [i], colb [i], proz [i]); rect ( 5, 65 + i * 9, 21, 16);
// если цвет выбран активным для левой кнопки мыши - ставим рядом красную точечку 
 if (i == color_active_left) {noStroke (); fill (240, 0, 0); ellipse (30, 73 + i * 9, 5, 5);}
 }
for (int i = 1; i < 8; i+=2) // заливаем в цикле 4 чётных прямоугольника цветами из массивов
 {
  if (num_color  == i)
    {stroke (200, 200, 200); }
   else {noStroke ();}
  fill (colr [i], colg [i], colb [i], proz [i]); rect (35, 56 + i * 9, 21, 16);
// если цвет выбран активным для правой кнопки мыши - ставим рядом красную точечку 
 if (i == color_active_right) {noStroke (); fill (240, 0, 0); ellipse (60, 64 + i * 9, 5, 5);}
 }
 // если выбрана панель цветов - обновляем положение ползунков вместе с выбранным цветом
 if (sel_col == true)
  {ris_polz (colr [num_color], colg [num_color], colb [num_color], proz [num_color]);}
 // если выбрана палель кистей - обновляем положение ползунов вместе с выбранным цветом
 if (sel_kist == true)
  {ris_polz (r [num_color], scale_x [num_color], scale_y [num_color], distanse [num_color]);} 
}

void ris_col_bar () // панель выбора цветов
    {
    cls_pan_instr ();
    ris_polz (colr [num_color], colg [num_color], colb [num_color], proz [num_color]);
    for (int i=255; i>=0; i--) // рисуем в цикле 4 скроллбара для цветов и прозрачности
    {
     stroke (i, 0, 0); line (5,  400-i, 10, 400-i);
     stroke (0, i, 0); line (20, 400-i, 25, 400-i);
     stroke (0, 0, i); line (35, 400-i, 40, 400-i);
     stroke (i, i, i); line (50, 400-i, 55, 400-i);
    }
   }
   
void ris_sel_kist () // панель выбора кистей
   {
    cls_pan_instr ();
    ris_polz (r [num_color], scale_x [num_color], scale_y [num_color], distanse [num_color]);
    for (int i=255; i>=0; i--) // рисуем в цикле 4 скроллбара для параметров кисти
    {
     stroke (200, 200, 200);
     line (5,  400-i, 10, 400-i);
     line (20, 400-i, 25, 400-i);
     line (35, 400-i, 40, 400-i);
     line (50, 400-i, 55, 400-i);
    }
    disp_sel_col ();
   }
   
void cls_pan_instr () // очистка панели инструментов
{noStroke (); fill (20, 20, 20); rect (0, 145, 65, 450);}

void cls_polz () // очистка областей ползунов
{
 noStroke (); fill (20, 20, 20); 
 for (int i = 11; i < 57; i = i + 15)
 {rect (i, 140, 9, 265);} 
 }

void cls_sel_col () // очистка области отображения 8 цветов
{
  noStroke (); fill (20, 20, 20); rect (2, 63, 63, 75);
}
     
void ris_polz (int r, int g, int b, int p) // положение ползунов
 {
  cls_polz ();
  noStroke (); fill (200, 200, 200); 
  triangle (12, 400 - r, 16, 396 - r, 16, 404 - r);  
  triangle (27, 400 - g, 31, 396 - g, 31, 404 - g);
  triangle (42, 400 - b, 46, 396 - b, 46, 404 - b);
  triangle (57, 400 - p, 61, 396 - p, 61, 404 - p);
 }

void sel_num_col (int x, int y)  
// выбор одного из восьми цветов. Получает X и Y
// левой - выбор для изменения цвета, правой - установка активным. 
// Левый столбец - левая кнопка, правый столбец - правая кнопка
{
  for (int i = 0; i < 7; i++)
  {
    if (x >= 5 && x <= 26 && y >= 65 + i * 9 && y <= 81 + i * 8) 
      {
       if (mouseButton == LEFT) {num_color = i;}
    else {
          if (mouseButton == RIGHT) {color_active_left = i;}
          }} 
     if (x >= 35 && x <= 56 && y >= 65 + i * 9 && y <= 81 + i * 8)
       {
        if (mouseButton == LEFT) {num_color = i + 1;}
    else {
          if (mouseButton == RIGHT) {color_active_right = i + 1;}
          }}   
 }
 disp_sel_col ();
}

void select_value (int x, int y, int num_panel) // изменение выбранного цвета на скроллбарах 
    {
 // создаём массив на 4 элемента, считываем текущие значения для редактируемого цвета
 // если выбрана панель цвета - цвета из массивов, если панель кистей - размер кисти, 
 // деформации по X и Y, расстояние между мазками
     if (sel_col == true | sel_kist == true)
       {
         int [] a = new int [4];
         if (num_panel == 0)
           {
            a [0] = colr [num_color]; a [1] = colg [num_color]; a [2] = colb [num_color]; a [3] = proz [num_color];
           }
        else 
        {
          if (num_panel == 1)
           {
            a [0] = r [num_color]; a [1] = scale_x [num_color]; a [2] = scale_y [num_color]; a [3] = distanse [num_color];
           }
        }
        
         // проверяем в цикле положение всех четырёх ползунков, запоминаем в массиве новые вычисленные значения
         for (int i = 0; i < 4; i++)
          {
           if (x >= 5 + i * 15  && x <= 15 + i * 15 && y >= 145 && y <= 400)
           {a [i] = 400 - y;}
          }
  // переписываем значения из временного массива в глобальные массивы
     if (num_panel == 0)
       {
        colr [num_color] = a [0]; colg [num_color] = a [1]; colb [num_color] = a [2]; proz [num_color] = a [3];
       }
     else 
        {
          if (num_panel == 1)
           {
            r [num_color] = a[0]; scale_x [num_color] = a[1]; scale_y [num_color] = a[2]; distanse [num_color] = a[3];
           }
        }           
         ris_polz (a [0], a [1], a [2], a [3]); // перерисовываем ползуны
         disp_sel_col (); // отображаем выбранные цвета
   }

}



     void setup() 
    {// инициализируем нулём RGB всех цветов, непрозрачные
     for (int i = 0; i < 8; i++)
      {
       colr [i] = (i + 5) * 10; colg [i] = (i + 5) * 12; colb [i] = (i + 5) * 16; proz [i] = 255;
      }
     
     size(1000, 550); // размер окна
     background (0,0,0); // цвет фона
     frameRate (20);
     ris_main_menu ();  // рисуем главное меню (три кнопки)
     disp_sel_col (); // отображаем образцы цветов   
    }



void draw() 
    {
     if (mousePressed && ris == false) // если какая нить кнопка мышки нажата
        {// курсор на кнопке выбора панели цветов     
      if (mouseX >= 3 && mouseX <= 15 && mouseY >= 5 && mouseY <= 15)
// рисуем панель цветов, лог. панель изменения цвета отображена, панель выбора кисти скрыта      
        {ris_col_bar (); sel_col = true; sel_kist = false;} 
      else { // иначе курсор на кнопке выбора кисти
            if (mouseX >= 22 && mouseX <= 37 && mouseY >= 5 && mouseY <= 15)
            // рисуем панель кистей, лог. панель кистей отображена
            // выбор цветов заблокирован - лог. панель выбора цвета скрыта
               {ris_sel_kist (); sel_kist = true; sel_col = false;} 
               
      else { // иначе курсор в области выбранных цветов
            if (mouseX >= 5 && mouseX <= 56 && mouseY >= 65 && mouseY <= 135)
               {sel_num_col (mouseX, mouseY);}
               
 // иначе курсор в области скроллбаров. В зависимости какая панель активна, вызывается либо
 // функция изменения выбранного цвета, либо функция изменения кисти
      else {
            if (sel_col == true && mouseX >= 5 && mouseX <= 55 && mouseY >= 145 && mouseY <= 400)
              {num_panel = 0;}
               
            if (sel_kist == true && mouseX >= 5 && mouseX <= 55 && mouseY >= 145 && mouseY <= 400)
              {num_panel = 1;}
              select_value (mouseX, mouseY, num_panel);
            
    //  else { // курсор на кнопке сохранения изображения
    //        if (mouseX >= 70 && mouseX <= 120 && mouseY >= 25 && mouseY <= 75)
    //        {save ("KARTINKA.bmp");}
           }}} // закрываем все "else"
     } // закрываем "если нажата какая нить кнопка мыши"
     
     // если нажата какая нить кнопка мыши И положение курсора по X за областью панели
     // инструментов - ширина панели + половина диаметра кисточки
     
     // здесь косяк!!! Ограничение выбирается для одного набора, и не меняется при смене цвета / кисти.
     // В итоге кисть может затереть всю панель инструментов
     
     
    if (mousePressed && mouseX >= 65 + r [nabor] / 2) 
     {
      ris = true; // лог. рисуем. Выход на выбор цвета или размера кисти не меняется
      // пока не отпустим кнопку мышки
      if (mouseButton == LEFT) {nabor = color_active_left;} // если нажата левая - выбранный для левой
      else {
            if (mouseButton == RIGHT) {nabor = color_active_right;} // если нажата правая - выбранный для правой
           }
    // нет обводки следа кисти, выбор цвета из массива в зависимости от нажатой кнопки мыши
      noStroke (); fill (colr [nabor], colg [nabor], colb [nabor], proz [nabor]);
      ellipse (mouseX, mouseY, r [nabor], r [nabor]); // рисуем окружность
     // здесь должна быть задержка повторного рисования. Пятно от кисти слишком быстро 
     // закрашивается до непрозрачного
     
     }
      else { // иначе если кнопка отпущена - все лог. переменные в "ложно"
            if(!mousePressed){ ris = false;}} 
         
}

Упомяну найденные мною книги по этому языку на русском:
«Учимся программировать вместе с Processing» — авторы: Casey Reas/Кейси Риз, Ben Fry/Бен Фрай — 6,89 Мб
«Processing 2: креативное программирование» — автор: Ян Вантомм — 15,8 Мб

Готов выложить их, но не знаю, куда лучше

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Bindings QCustomPlot для Python

Добрый день, хаброжители!

Введение


В свободное от работы время увлекся написанием приложений на PyQt5. И свой давний проект по ведению домашней бухгалтерии MyWallet решил в конце мая переписать с плюсов на Python, так как в предыдущей версии были допущены ряд архитектурных ошибок, которые на хотелось исправлять. Поэтому собрав PyQt5 из исходников под Fedora 21, где-то за две недели реализовал весь функционал, который был ранее. И теперь встает вопрос в визуализации данных по расходам/доходам помесячно. Так как имел опыт визуализации данных с помощью QCustomPlot , хотел визуализацию сделать с помощью этой либы. Но к огорчению, не нашел биндов.

Сборка


После просмотра исходников PyQt5 было выяснено, что генерация биндов реализована с помощью SIP). SIP принимает на вход что-то вроде урезанного заголовка методов класса (естественно, со своими так называемыми аннотациями), а на выходе генерирует C++ код для создания готового модуля python.

Итак, для сборки модуля QCustomPlot для Python нам понадобится:

  1. Qt 5.x.
  2. SIP наиболее свежей версии.
  3. PyQt 5.x.
  4. Собранная в виде динамически подключаемой библиотеки qcustomplot, собранной под Qt 5.x.
  5. Файл специального вида с описанием интерфейса классов библиотеки.

Покопавшись по github'у в поисках готового файла интерфейса для этой либы, наткнулся на репозиторий qcustomplot-python, владелец которого собрал бинды, правда для PyQt4. Действуя по аналогии, получаем файл интерфейса либы qcustomplot.sip.

В этом же репозитории можно найти и configure.py, который, как известно, необходим для сборки и установки модулей Python. Данный файл пришлось адаптировать к новой версии PyQt.

Ну, а далее стандартно:

$ python3 configure.py build
$ make
$ sudo make install 

Удостоверимся, что у нас все получилось, запуститим IPy:

$ python3
>>> import qcustomplot
>>> dir(qcustomplot)
['QCP', 'QCPAbstractItem', 'QCPAbstractLegendItem', 'QCPAbstractPlottable', 'QCPAxis', 'QCPAxisRect', 'QCPBarData', 'QCPBars', 'QCPBarsGroup', 'QCPColorGradient', 'QCPColorMap', 'QCPColorMapData', 'QCPColorScale', 'QCPColorScaleAxisRectPrivate', 'QCPCurve', 'QCPCurveData', 'QCPData', 'QCPFinancial', 'QCPFinancialData', 'QCPGraph', 'QCPGrid', 'QCPItemAnchor', 'QCPItemBracket', 'QCPItemCurve', 'QCPItemEllipse', 'QCPItemLine', 'QCPItemPixmap', 'QCPItemPosition', 'QCPItemRect', 'QCPItemStraightLine', 'QCPItemText', 'QCPItemTracer', 'QCPLayer', 'QCPLayerable', 'QCPLayout', 'QCPLayoutElement', 'QCPLayoutGrid', 'QCPLayoutInset', 'QCPLegend', 'QCPLineEnding', 'QCPMarginGroup', 'QCPPainter', 'QCPPlotTitle', 'QCPPlottableLegendItem', 'QCPRange', 'QCPScatterStyle', 'QCPStatisticalBox', 'QCustomPlot', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__']
>>> 

Ну, а чтобы совсем было красиво, привожу код одного из примеров BarsDemo:

Код примера
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtGui import QColor, QPen
from qcustomplot import QCustomPlot, QCPBars, QCP

if __name__ == '__main__':

    app = QApplication(sys.argv)

    w = QCustomPlot()
    regen = QCPBars(w.xAxis, w.yAxis)
    nuclear = QCPBars(w.xAxis, w.yAxis)
    fossil = QCPBars(w.xAxis, w.yAxis)

    w.addPlottable(regen)
    w.addPlottable(nuclear)
    w.addPlottable(fossil)

    pen = QPen()
    pen.setWidthF(1.2)
    fossil.setName('Fossil fuels')
    pen.setColor(QColor(255, 131, 0))
    fossil.setPen(pen)
    fossil.setBrush(QColor(255, 131, 0, 50))
    nuclear.setName('Nuclear')
    pen.setColor(QColor(1, 92, 192))
    nuclear.setPen(pen)
    nuclear.setBrush(QColor(1, 92, 191, 50))
    regen.setName('Regenerative')
    pen.setColor(QColor(150, 222, 0))
    regen.setPen(pen)
    regen.setBrush(QColor(150, 222, 0, 70))
    nuclear.moveAbove(fossil)
    regen.moveAbove(nuclear)

    ticks = [1, 2, 3, 4, 5, 6, 7]
    labels = ['USA', 'Japan', 'Germany', 'France', 'UK', 'Italy', 'Canada']
    w.xAxis.setAutoTicks(False)
    w.xAxis.setAutoTickLabels(False)
    w.xAxis.setTickVector(ticks)
    w.xAxis.setTickVectorLabels(labels)
    w.xAxis.setTickLabelRotation(60)
    w.xAxis.setSubTickCount(0)
    w.xAxis.grid().setVisible(True)
    w.xAxis.setRange(0, 8)

    w.yAxis.setRange(0, 12.1)
    w.yAxis.setPadding(5)
    w.yAxis.setLabel('Power Consumption in\nKilowatts per Capita (2007)')
    w.yAxis.grid().setSubGridVisible(True)

    grid_pen = QPen()
    grid_pen.setStyle(Qt.SolidLine)
    grid_pen.setColor(QColor(0, 0, 0, 25))
    w.yAxis.grid().setSubGridPen(grid_pen)

    fossil_data = [0.86 * 10.5, 0.83 * 5.5, 0.84 * 5.5, 0.52 * 5.8, 0.89 * 5.2, 0.90 * 4.2, 0.67 * 11.2]
    nuclear_data = [0.08 * 10.5, 0.12 * 5.5, 0.12 * 5.5, 0.40 * 5.8, 0.09 * 5.2, 0.00 * 4.2, 0.07 * 11.2]
    regen_data = [0.06 * 10.5, 0.05 * 5.5, 0.04 * 5.5, 0.06 * 5.8, 0.02 * 5.2, 0.07 * 4.2, 0.25 * 11.2]
    fossil.setData(ticks, fossil_data)
    nuclear.setData(ticks, nuclear_data)
    regen.setData(ticks, regen_data)

    w.legend.setVisible(True)
    w.axisRect().insetLayout().setInsetAlignment(0, Qt.AlignTop|Qt.AlignHCenter)
    w.legend.setBrush(QColor(255, 255, 255, 200))
    legendPen = QPen()
    legendPen.setColor(QColor(130, 130, 130, 200))
    w.legend.setBorderPen(legendPen)
    w.setInteractions(QCP.iRangeDrag or QCP.iRangeZoom)

    w.show()

    sys.exit(app.exec())



Вот, что получилось:
Результат
image

P.S.


Ссылка на репозиторий с исходниками: QCustomPlot-PyQt5. В репозитории в каталоге RPMS находятся SRPM и RPM для Fedora21 (PyQt5, qcustomplot 1.3.1 и python3-qcustomplot).

Все комментарии и пожелания приветствуются. Надеюсь, этот модуль вам пригодится. Спасибо за внимание!

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Основы Rust – Глава 2. Использование переменных и типов

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

В этой главе будут рассмотрены основные строительные материалы Rust программ: переменные и их типы. Мы обсудим такие вопросы, как переменные с базовыми типами, явное указание типа и область видимости переменных. Так же, мы обсудим один из краеугольных камней в стратегии безопасности Rust — неизменяемость.

Комментарии


В идеале программа должны быть самодокументируемой, используя описательные имена переменных и легкий для чтения код, но всегда есть случаи, в которых необходимо указать комментарии с описанием работы программы или алгоритма. Rust имеет следующие правила по написанию комментариев:
  • Строчные комментарии (//): Абсолютно все, что идет после //, является комментарием и не будет компилироваться
  • Блок или многострочные комментарии (/* */): Все, что находится между начальным /* и конечным */ символами не будет компилироваться

Однако, в Rust желательно использовать только однострочные комментарии, даже для нескольких строк:
fn main() {
        // здесь происходит выполнение приложения.
        // Тут мы указываем отобразить сообщение с приветствием:
        println!("Добро пожаловать в игру!");
}


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

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

/// Начало выполнения игры
fn main() {
}


За сборку комментариев в документацию отвечает инструмент rustdoc.

Глобальные константы


Часто приложению требуется несколько неизменяемых значений (констант). Они не меняются во время работы программы. Предположим, мы хотим написать игру, под названием «Атака монстров», в которой будет параметр уровня здоровья, имя игры и максимальный уровень здоровья (100) – это константы. Мы хотим иметь возможность обращаться к этим константам из любого участка кода, для этого мы определяем их в начале файла, иными словами, указываем в глобальной области видимости. Константы объявляются ключевым словом static:
static MAX_HEALTH: i32 = 100;
static GAME_NAME: &'static str = "Атака Монстров";

fn main() {
}


Имена констант необходимо указывать верхнем регистре, а для разделения слов использовать символ подчеркивания. Также нужно указать константам тип. MAX_HEALTH является 32-битным целым числом (i32), а GAME_NAME – строкой (str). По такому же принципу объявляется тип у переменной, разница лишь в том, что в переменной его можно иногда не указывать.

Не забивайте пока голову насчет &’static. Поскольку Rust является низкоуровневым языком, многие вещи в нем требуют уточнения. Символ & служит ссылкой на что-то (в ней содержится адрес на значение в памяти), в нашем случае он содержит ссылку на строку. Однако, если мы напишем только &str и скомпилируем, то мы получим ошибку:

static GAME_NAME: &str = "Атака Монстров";

2:22 error: missing lifetime specifier [E0106]


2:22 означает, что у нас ошибка во 2 строке и 22 символе. Также мы должны добавить спецификатор времени жизни ‘static к аннотации типа, в результате мы имеем &’static str. В Rust время жизни объекта очень важный момент, поскольку от него зависит на сколько долго объект задержится в памяти. Когда время жизни подходит к концу, компилятор Rust избавляется от объекта и освобождает память, которую объект занимал. Время жизни у ‘static самое долгое, такой объект остается жить в программе на протяжение всей ее работы и доступен во всех местах кода.

Но не смотря на то, что мы добавили и спецификатор и ссылку, компилятор все равно выдает нам предупреждение:

static item is never used: `MAX_HEALTH`, #[warn(dead_code)]


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

Совет


Пройдет какое-то время, прежде чем начинающий разработчик начнет считать Rust компилятор своим другом, а не раздражающей машиной, которая постоянно выплевывает ошибки и предупреждения. К пример, если выйдет подобная ошибка, программа не запустится:
error: aborting due to previous errors


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

Помимо статических значений мы также можем использовать простые неизменяемые значения. Константы всегда нужно объявлять с указанием типа, например: const PI: f32 = 3.14.

Печать с помощью интерполяции строк


Очевидный способ использования переменных- это выводить их значения:
static MAX_HEALTH: i32 = 100;
static GAME_NAME: &'static str = "Атака Монстров";
 
fn main() {
  const PI: f32 = 3.14;
  println!("Игра, в которую вы играете, называется {}.", GAME_NAME);
  println!("У вас {} единиц жизней", MAX_HEALTH);
}


После запуска программа выдаст следующее:
Игра, в которую вы играете, называется Атака Монстров.
У вас 100 единиц жизней


Константа PI имеется в стандартной библиотеке, чтобы ей воспользоваться установите этот оператор в начале кода:
use std::f32::consts;


А работать с ней можно вот так:
println!("{}", consts::PI);


Первый аргумент внутри println! – это литеральная строка, содержащая плейсхолдер (placeholder) {}. Значение константы после запятой преобразуется в строку и вставляется заместо {}. Можно указать несколько плейсхолдеров, пронумеровав их по порядку:
println!("В игре '{0}', у вас будет {1} % очков жизни, да, вы прочли правильно: {1} очков!", GAME_NAME, MAX_HEALTH);


Программа выдаст следующее:
В игре 'Атака Монстров', у вас будет 100 % очков жизни, да, вы прочли правильно: 100 очков!


Плейсхолдер может также содержать один или несколько параметров, передаваемых по имени:
println!("У вас {points} % жизни", points=70);


Программа выдаст:
У вас 70 % жизни


Внутри {} можно указать тип форматирования:
println!("Значение MAX_HEALTH - {:x}, это шестнадцатеричный формат", MAX_HEALTH); // 64
println!("Значение MAX_HEALTH - {:b}, это бинарный формат", MAX_HEALTH);  // 1100100
println!("Значение pi - {:e}, это формат с плавающей запятой", consts::PI); // 3.141593e0


Следующие типы форматирования используются для объектов с определенным типом:
  • o для восьмиричного
  • x для шестнадцатеричного числа в нижнем регистре
  • X для шестнадцатеричного числа в верхнем регистре
  • p для указателей
  • b для бинарных
  • e для экспоненциальной нотации в нижнем регистре
  • E для экспоненциальной нотации в верхнем регистре
  • ? для отладки

Макрос format! имеет те же параметры и работает также, как println!, только он возвращает строку, а не выводит текст.

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

Значения и примитивные типы


У наших констант есть значения. Значение бывает различных типов: 70 – целое число, 3.14 – число с плавающей запятой, Z и q – символьный тип (они являются символами) в формате unicode, каждый символ занимает по 4 байта в памяти. “Godzilla” имеет тип строку $str (по умолчанию кодировка UTF-8), true и false – булевый тип, они являются Булевыми значениями. Целые числа можно написать в различных форматах:
  • В шестнадцатеричном формате с 0x (число 70 будет 0x46)
  • В восьмеричном формате с 0o (число 70 будет 0o106)
  • В бинарном формате с 0b (0b1000110)

Символы подчеркивания можно использовать для читабельности, например 1_000_000. Иногда компилятор будет требовать указать тип числа с суффиксом. Например, после u или i указывается число используемых бит памяти: 8, 16, 32, или 64:
  • 10usize означает беззнаковое целое число, размер машинного кода usize, которое может быть любым из типов u8, u16,u32 или u64
  • 10isize означает знаковое целое число, размер машинного кода isize, которое может быть любым из типов i8, i16, i32, или i64
  • 3.14f32 означает 32-битное число с плавающей точкой
  • 3.14f64 означает 64-битное число с плавающей точкой

Числовые типы i32 и f64 являются значениями по умолчанию, чтобы различить их вам нужно добавить в типе f64 .0, например так: let e = 7.0;.

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

Приоритеты операторов в Rust похожи на те, что используются в других Си-подобных языках. Однако, Rust не имеет операторов инкремента (++) и декремента (). Для сравнения двух значений на равенство используется ==, а для проверки их различия- !=.

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

Документация по Rust


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

Привязка значения к переменной


Хранение всех значений в константах не самый лучший вариант, так как нам может понадобится изменить какое-нибудь из этих значений. В Rust мы можем привязать значение к переменной с помощью привязки let:
fn main() {
  let energy = 5; // значение 5 привязывается к переменной energy
}


В отличие от таких языков, как Python или Go, нам необходимо указать в конце точку с запятой, чтобы закончить объявление. В противно случае компилятор выдаст ошибку:
error: expected one of `.`, `;`, or an operator, found `}`


Привязку мы тоже создаем пока без ее применения, поэтому не обращайте внимание на предупреждение:
values.rs:2:6: 2:7 warning: unused variable: `energy`, #[warn(unused_variables)] on by default


Совет


Чтобы отключить предупреждения о неиспользуемый переменных используйте префикс нижнего подчеркивания перед именем переменной:
let _energy = 5;



Обратите внимание на то, что в предыдущем примере мы не указывали тип. Rust предположит, что тип переменной energy будет целое число. Если тип переменной неочевиден, компилятор попробует найти в коде места, где эта переменная использовалась. Однако, можно указывать тип значение и таким способом:
let energy = 5u16;


Это немного поможет компилятору с указанием типа у energy, в нашей случае мы указали 2-битное беззнаковое целое число.

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

let copy_energy = energy;
println!("Количество энергии: {}", energy););


Вот несколько других объявлений:
let level_title = "Уровень 1";
let dead = false;
let magic_number = 3.14f32;
let empty = ();  // значение модульного типа ()


Значение переменной magic_number также можно записать в формате 3.14_f32. Нижнее подчеркивание отделяет цифры от типа для лучшей читабельности.

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

let energy = "Очень много";


То старой переменной уже нельзя будет воспользоваться, а ее память будет освобождена.

Изменяемые и неизменяемые переменные


Предположим, что мы используем аптечку и наша энергия поднимается до значения 25. При этом, если мы напишем:
energy = 25;


То мы получим ошибку:
error: re-assignment of immutable variable `energy`


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

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

Если вам нужна переменная, которая будет изменяться во время выполнения кода, вам надо объявить ее вместе с mut:

let mut fuel = 34;
fuel = 60;


Объявить простую переменную тоже не получится:
let n;


Подобное приведет к ошибке:
error: unable to infer enough type information about `_`; type annotations required.


Компилятору необходимо указать тип этой переменной. Мы передаем информацию по типу когда присваиваем значение:
n = -2;


Но, как говориться в сообщение, мы также можем указать тип следующим образом:
let n: i32;


Кроме этого, мы можем написать сразу все вместе:
let n: i32 = -2;


Для примитивных типов подобное делается с помощью указания суффикса:
let x = 42u8;
let magic_number = 3.14f64;


Попытка использовать неинициализированную переменную приведет к ошибке:
error: use of possibly uninitialized variable


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

Область действия переменной и затенение


Взглянем еще раз на пример, который рассматривали выше:
fn main() {
  let energy = 5; // значение 5 привязывается к переменной energy
}


Здесь переменная располагается в локальной области функции между символами {}. После символа } переменная выходит из области видимости и ее выделенная память освобождается.

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

fn main() {
    let outer = 42;
    { // начало блока кода
        let inner = 3.14;
        println!("Переменная inner: {}", inner);
        let outer = 99; // показать первую переменную outer
        println!("Переменная outer: {}", outer);
    } // конец блока кода
    println!("Переменная outer: {}", outer);
}


Мы получим такой вывод:
Переменная inner: 3.14
Переменная outer: 99
Переменная outer: 42


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

Итак, зачем нам может понадобиться использовать блок кода? В разделе “Выражения” мы увидим, что блок кода может возвращать значение, которое возможно привязать к переменной с let. Блок кода также может быть пустым – {}.

Проверка и преобразование типа


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

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

fn main() {
  let score: i32 = 100;
  score = "ВЫ ПОБЕДИЛИ!"
}


Мы получим ошибку компилятора:
error: mismatched types: expected `i32`, found `&'static str`(expected i32, found &-ptr)


Однако, нам разрешено писать вот так:
let score = "YOU WON!";


Rust позволяет нам переопределять переменные. Каждая привязка let создает новую переменную score, скрывая предыдущую, которая освобождает память. На самом деле, это очень полезно, потому как переменные по умолчанию являются неизменными.

Добавление строки с помощью + в Rust не будет работать:

let player1 = "Ваня";
let player2 = "Петя";
let player3 = player1 + player2;


Мы получим ошибку:
error: binary operation `+` cannot be applied to type `&str`.


Вы можете воспользоваться функцией to_string(), чтобы преобразовать тип значения в String:
let player3 = player1.to_string() + player2;


Или же воспользуйтесь макросом format!:
let player3 = format!("{}{}", player1, player2);


В обоих случаях переменная player3 будет иметь значение “ВаняПетя“.

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

fn main() {
  let points = 10i32;
  let mut saved_points: u32 = 0;
  saved_points = points; // ошибка!
}


Опять не получилось. Мы получили ошибку:
error: mismatched types: expected `u32`, found `i32` (expected u32, found i32)


Для максимальной проверки типа Rust не позволяет автоматическое (или неявное) преобразование одного типа в другой, как это сделано в C++. Например, когда значение f32 преобразовано в значение ani32, числа после десятичной запятой теряются, если делать это автоматически, то могут произойти ошибки. Однако, мы можем сделать явное преобразование (кастинг) с помощью ключевого слова as:
saved_points = points as u32;


Когда points содержит отрицательное значение, знак будет утерян после преобразования. Точно так же большое значение, как float, преобразуется в целое число, десятичная часть отсекается:
let f2 = 3.14;
saved_points = f2 as u32; // будет обрезано до значения 3


Кроме того, значение должно быть конвертируемым в новый тип, так как строку нельзя будет преобразовать в целое число:
let mag = "Gandalf";
saved_points = mag as u32; // error: non-scalar cast:`&str`as`u32`


Сглаживание


Иногда может быть полезно дать новое, более описательное или короткое имя существующему типу. Это можно сделать с помощью type:
type MagicPower = u16;
 
fn main() {
  let run: MagicPower= 7800;  
}


Имя в type начинается с заглавной буквы, как и каждое слово, которое является частью имени. Что произойдет, если мы поменяем значение с 7800 на 78000? Компилятор выдаст нам следующее предупреждение:
warning: literal out of range for its type.


Выражения


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

Привязки let являются операторами объявления. Они не являются выражениями:

let a = 2;    // a привязывается к 2
let b = 5;    // b привязывается к  5
let n = a + b;   // n привязывается к  7


a + b; является оператором выражения, он возвращает пусто значение (). Если нам надо вернуть результат сложения, то нужно убрать точку с запятой. Rust’у необходимо знать когда операторы заканчивают свое действие, по это причине, практически все строки в Rust заканчиваются точкой с запятой.

Как вы думаете, что здесь присваивается?

m = 42;


Это не привязка, поскольку отсутствует let. Это – выражение, возвращающее пустое значение (). Составная привязка, как здесь:
let p = q = 3;


в Rust вообще запрещена. Это вернет ошибку:
error: unresolved name q


Однако, вы можете воспользоваться привязкой let:
let mut n = 0;
let mut m = 1;
let t = m; m = n; n  = t;
println!("{} {} {}", n, m, t); // будет напечатано 1 0 1


Блок кода также является выражением, который будет возвращать значение своего последнего выражения, если мы опустим точку с запятой. Например, в следующем фрагменте кода, n1 получает значение 7, но n2 получает не значение, потому что возвращаемое значение во втором блоке было подавлено:
let n1 = {
        let a = 2;
        let b = 5;
        a + b   // <-- нет точки с запятой!
};
println!("n1: {}", n1);  // выведет - "n1: 7"
 
let n2 = {
        let a = 2;
        let b = 5;
        a + b;
};
println!("n2: {:?}", n2);  // выведет - "n2: ()"


Здесь переменные a и b объявлены в блоке кода и живы пока живет сам блок, так как переменные локальны для блока. Обратите внимание на то, что необходима точка с запятой после закрывающей фигурной скобки блока } ;. Для печати пустого значения (), нам нужно указать {:?}, как спецификатор формата.

Стек и куча


Поскольку выделение памяти является очень важной темой в Rust, нам надо иметь четкую картину всего происходящего. Память программы разделяется на стек и кучу. На stackoverflow более детально описали эти понятия. Примитивные значения, такие как цифры, символы, значения true/false, хранятся в стеке, в то время, как значения более сложных объектов, которые могут расти, размещаются в куче памяти. В стеке содержатся адреса памяти на объекты, которые располагаются в куче:

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

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

let health = 32;
let mut game = "Космические захватчики";


Значения хранятся в памяти и имеют адреса. Переменная health содержит целое число со значением 32, которое хранится в стеке с адресом 0x23fba4, в то время, как переменная game содержит строку, которая хранится в куче, начиная свое положение с адреса 0x23fb90. Это были адреса на момент запуска программы, когда вы запустите программу они будут другими.

Переменные, к которым привязаны значения, являются указателями или ссылками на значения. game является ссылкой на “Космические захватчики“. Адрес задается оператором &. Таким образом, &health будет указывать на место расположения значения 32, а &game на место где лежит “Космические захватчики“.

Мы можем распечатать эти адреса при помощи строки, используя формат {:p} для указателей:

println!("Адрес значения health: {:p}", &health); // 0x23fba4
println!("Адрес значения game: {:p}", &game); //  0x23fb90
println!("Значение переменной game: {}", game); // напечатает "Космические захватчики"


Итак, мы имеем следующую ситуацию в памяти (адреса памяти будут отличаться при каждом выполнении):

Мы можем создать псевдоним, который будет является другой ссылкой, указывающей на то же место в памяти:
let game2 = &game;
println!("{:p}", game2); // 0x23fb90


Чтобы получить значение объекта, а не его ссылку, добавьте к имени звездочку:
println!("{}", *game2); // напечатает "Космические захватчики"


Эта строка эквивалентна:
println!("game: {}", &game);


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

Мы уже знаем, что привязка let является неизменяемой, так что это значение нельзя изменить:

health = 33; // error: re-assignment of immutable variable `health`.


Если y объявить как:
let y = &health;


Тогда *y будет иметь значение 32. Ссылочным переменным можно также дать тип:
let x: &i64;


После привязки let, переменная x еще не указывает на значение, и она не содержит адрес памяти. В Rust нет способа создать нулевой указатель, как это делается в других языках. При попытке присвоить переменной x значение nil, null или пустое значение (), приведет к ошибке. Одна только эта особенность спасает программистов Rust от бесчисленных ошибок. Кроме того, пытаясь использовать x в выражении, например:
println!("{:?}", x);


Мы получим ошибку:
error: use of possibly uninitialized variable: `x`error


Запрещено размещать изменяемую ссылку на неизменяемый объект, в противном случае неизменяемую переменную можно будет редактировать через изменяемую ссылку:
let tricks = 10;
let reftricks = &mut tricks;


Будет выдана ошибка:
error: cannot borrow immutable local variable `tricks` as mutable


Ссылка на изменяемую перемененную score может быть неизменяемой или изменяемой соответственно:
let mut score = 0;
let score2 = &score;
// error: cannot assign to immutable borrowed content *score2
// *score2 = 5; 
 
let mut score = 0;
let score3 = &mut score;
*score3 = 5;


Значение в score можно изменить только с помощью изменяемой ссылки score3.

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

let score4 = &mut score;


Будет выдана ошибка:
error: cannot borrow `score` as mutable more than once at a time 


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

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

Значения стека можно упаковать, то есть, разместить в куче, для этого используется Box:

let x = Box::new(5i32);


Box является объектом, который ссылается на значения в куче.

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

[Перевод] Как команда PVS-Studio улучшила код Unreal Engine

Наша компания создаёт, продвигает и продаёт статический анализатор кода PVS-Studio для C/C++ программистов. Однако, наше взаимодействие с клиентами не ограничивается исключительно продажей им лицензий на продукт PVS-Studio. Например, мы занимаемся некоторыми контрактными работами. В силу NDA обычно рассказать о них мы не можем, да и интересного рассказа не получится. Названия проектов, в которых мы принимаем участие, тоже ничего не скажут большинству наших читателей. Но в этот раз, название как раз говорит о многом. Мы поработали вместе с компанией Epic Games над проектом Unreal Engine. Об этом и будет наш рассказ.
Для продвижения статического анализатора кода PVS-Studio мы придумали интересный формат статей. Мы проверяем открытые проекты и пишем про найденные недочёты в коде. Вот здесь можно взглянуть на эти статьи: обновляемый список. От этих статей все в выигрыше. Читателям интересно смотреть на чужие ошибки. Заодно они узнают для себя новые способы как избежать этих ошибок, используя какие-то приёмы или стиль написания кода. Для нас польза в том, что больше людей узнаёт о существовании PVS-Studio. Для авторов проектов тоже польза — они могут исправить некоторые баги.

Одной из таких статей была "Долгожданная проверка Unreal Engine 4". Исходный код Unreal Endine отличался высоким качеством, но как известно, все проекты по разработке программного обеспечения содержат ошибки и PVS-Studio отлично справился с нахождением этих ошибок. Мы проверили проект и отправили результаты проверки в Epic Games. Авторы проекта сказали нам спасибо и поправили найденные нами дефекты. Но нам этого было мало, и мы решили постараться продать компании Epic Games лицензию на PVS-Studio.

Компания Epic Games была заинтересована в использовании PVS-Studio, чтобы улучшить свой движок. Таким образом, мы проверяем и правим исходники Unreal Engine с таким расчётом, чтобы в коде не осталось багов и при этом анализатор больше не выдавал ни одного ложного срабатывания. После этого они начнут использовать PVS-Studio на своей кодовой базе, сведя к минимуму сложность интеграции анализатора в свой процесс разработки. В этом случае Epic Games согласилась оплатить не только лицензию, но и дополнительно вознаградить нас за проделанную работу.

Мы согласились. Работа выполнена. И теперь предлагаем читателю познакомиться с интересными моментами, с которыми мы встретились в процессе работы c исходными кодами Unreal Engine.

Со стороны PVS-Studio в проекте участвовали Павел Еремеев, Святослав Размыслов, Антон Токарев. Со стороны Epic Games больше всего помощи мы получили от Andy Bayle и Dan O'Connor – без них наша работа была бы невозможна, спасибо!

Интеграция анализа PVS-Studio в сборку Unreal Engine


Для сборки в Unreal Engine используется собственная сборочная система – Unreal Build Tool. Имеется также набор скриптов для генерации проектных файлов для разных платформ и компиляторов. Так как PVS-Studio ориентирована в первую очередь на работу с компилятором Microsoft Visual C++, мы воспользовались скриптом для получения проектных файлов (*.vcxproj) для среды Microsoft Visual Studio.

PVS-Studio имеет плагин, встраивающийся в среду разработки Visual Studio и позволяющий выполнить анализ «в один клик». Однако, проекты, сгенерированные для Unreal Engine, не являются «обычными» проектами MSBuild, которые использует Visual Studio.

Во время компиляции Unreal Engine в Visual Studio, среда вызывает MSBuild при запуске сборки, однако, сам MSBuild используется лишь как «обёртка» для вызова упомянутого ранее Unreal Build Tool.

Для анализа исходного кода в PVS-Studio нужны результаты работы препроцессора. Анализатору требуется *.i файл в котором вставлены все заголовочные файлы и раскрыты все макросы.

Примечание. Далее этот раздел будет интересен только тем, кто использует нестандартную сборочную систему, например, как у Unreal Engine. Если вы планируете попробовать проверить с помощью PVS-Studio свой проект, который собирается как-то хитро, то предлагаю дочитать этот раздел до конца. Возможно, он поможет совладать и с проверкой вашего проекта. Если же у вас обыкновенный проект для Visual Studio, или хочется побыстрее почитать про найденные ошибки, то можно пропустить этот раздел.

Для того, чтобы корректно запустить препроцессор, необходима информация о параметрах компиляции. В «обычных» MSBuild проектах эта информация имеется, и плагин PVS-Studio «видит» её и может самостоятельно препроцессировать нужные исходники для последующего запуска анализатора. Иначе обстоят дела с проектами Unreal Engine.

Как уже было сказано выше, эти проекты – всего лишь «обёртка», реальный же вызов компилятора осуществляет Unreal Build Tool. Поэтому параметры компиляции и недоступны плагину PVS-Studio для Visual Studio. Запустить анализ «одним кликом» не получается, хотя плагин можно использовать для просмотра результатов анализа.

Непосредственно сам анализатор (PVS-Studio.exe) является command-line приложением, схожим по своему сценарию использования с C++ компилятором. Его, как и компилятор, необходимо запускать для каждого исходного файла, передавая параметры компиляции этого файла через командную строку или response файл. Анализатор же сам запустит правильный препроцессор и впоследствии выполнит анализ.

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

Таким образом, универсальным решением для интеграции анализатора PVS-Studio будет вызов его исполняемого файла в том же месте, где происходит вызов компилятора – т.е. в сборочной системе. В данном случае в Unreal Build Tool. Понятно, что это потребует модификации используемой сборочной системы, что, как в нашем случае, может быть нежелательно. Поэтому, как раз для подобных ситуаций мы создали систему «перехвата» вызовов компилятора – Compiler Monitoring.

Система Compiler Monitoring способна «отлавливать» запуск процессов компиляции (в случае с Visual C++ это процессы cl.exe), собирать все необходимые для препроцессирования параметры у таких процессов, и перезапускать препроцессирование компилируемых файлов, с последующим запуском их анализа. Так и поступим.

Рисунок 1. Схематичное изображение процесса проверки проекта Unreal Engine

Интеграция анализа Unreal Engine сводится для нас к запуску непосредственно перед сборкой процесса мониторинга (CLMonitor.exe), который уже выполнит по завершении сборки все необходимые действия по препроцессированию и непосредственному вызову анализатора. Для запуска мониторинга нужно выполнить простую команду:

CLMonitor.exe monitor

CLMonitor.exe запустит сам себя в режиме отслеживания и завершится. При этом ещё один процесс CLMonitor.exe останется висеть в фоне, осуществляя непосредственное «отлавливание» компиляторов. По завершению сборки нужно выполнить ещё одну простую команду:
CLMonitor.exe analyze "UE.plog"

Теперь CLMonitor.exe запустит непосредственно анализ собранных ранее исходных файлов и сохранит результаты в файл UE.plog, с которым уже можно работать в нашем IDE плагине.

Мы настроили на своём Continuous Integration сервере регулярную ночную сборку интересных для нас конфигураций Unreal Engine с последующим запуском анализа. Таким образом мы, во-первых, проверяли, что наши правки не сломали сборку, а во-вторых, получали каждое утро новый отчёт о проверке Unreal Engine, с учётом всех заложенных в предыдущий день правок. И перед отправкой Pull Request'а о включении наших правок в основной репозиторий проекта, мы могли легко проверить, что текущая версия в нашем репозитории стабильна, просто перезапустив сборку на сервере.

Нелинейная скорость правок


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

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

То есть теоретически можно ожидать какой-то такой график:

Рисунок 2. Идеальный график. Количество ошибок уменьшается равномерно с каждым рабочим днём.

На самом деле, сообщения исчезают в начале быстрее, чем потом. Во-первых, на начальном этапе подавляются предупреждения, находящихся в макросах, и это быстро сокращает их количество. Во-вторых, получается так, что в начале исправляются очевидные вещи, а непонятные места откладываются на потом. Мы можем объяснить, почему в начале правились простые вещи. Хотелось продемонстрировать разработчикам из Epic Games, что мы приступили к работе и процесс пошел. Странно было бы начать со сложного и застрять на этом.

Всего непосредственно на работу с кодом Unreal Engine ушло 17 рабочих дней. Нашей целью было изничтожить все предупреждения первого и второго уровня общего назначения. Вот как продвигалась работа:

Таблица 1. Количество предупреждений анализатора в различные дни.

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

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

Теперь те же самые данные представленные в виде сглаженного графика:

Рисунок 3. Сглаженный график количества предупреждений.

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

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

О найденных ошибках


Мы поправили достаточно много фрагментов кода. Правки условно можно разделить на 3 группы:
  1. Настоящие ошибки. Ниже мы приведём несколько таких ошибок в качестве примера.
  2. Беды не было, но код сбивал с толку анализатор и мог запутать человека, который начнёт изучать этот код. Другими словами, этот код, который «пахнет» и его тоже полезно поправить. Что мы и сделали.
  3. Правки, которые вызваны исключительно потребностью «угодить» анализатору кода, выдающему ложные срабатывания. Мы старались по возможности вынести подавление ложных сообщений в отдельный специальный файл или улучшить работу самого анализатора. Но всё равно в нескольких местах пришлось провести рефакторинг, чтобы помочь анализатору понять, что к чему.
Как и обещали, посмотрим на некоторые примеры ошибки. Мы выбрали код попроще и поинтересней.

Первое интересное сообщение PVS-Studio: V506 Pointer to local variable 'NewBitmap' is stored outside the scope of this variable. Such a pointer will become invalid. fontcache.cpp 466

void GetRenderData(....)
{
  ....
  FT_Bitmap* Bitmap = nullptr;
  if( Slot->bitmap.pixel_mode == FT_PIXEL_MODE_MONO )
  {
    FT_Bitmap NewBitmap;
    ....
    Bitmap = &NewBitmap;
  }
  ....
  OutRenderData.RawPixels.AddUninitialized(
    Bitmap->rows * Bitmap->width );
  ....
}

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

При попытке использовать указатель для доступа к уничтоженному объекту возникнет неопределённое поведение. Как оно проявит себя — неизвестно. Программа даже может долгие годы успешно работать, если благодаря везению данные мертвого объекта (расположенные на стеке) не перетираются чем-то другим.

Правильно будет вынести объявление NewBitmap за пределы оператора 'if':

void GetRenderData(....)
{
  ....
  FT_Bitmap* Bitmap = nullptr;

  FT_Bitmap NewBitmap;
  if( Slot->bitmap.pixel_mode == FT_PIXEL_MODE_MONO )
  {
    FT_Bitmap_New( &NewBitmap );
    // Convert the mono font to 8bbp from 1bpp
    FT_Bitmap_Convert( FTLibrary, &Slot->bitmap, &NewBitmap, 4 );

    Bitmap = &NewBitmap;
  }
  else
  {
    Bitmap = &Slot->bitmap;
  }
  ....
  OutRenderData.RawPixels.AddUninitialized(
    Bitmap->rows * Bitmap->width );
  ....
}

Следующее предупреждение PVS-Studio: V522 Dereferencing of the null pointer 'GEngine' might take place. Check the logical condition. gameplaystatics.cpp 988
void UGameplayStatics::DeactivateReverbEffect(....)
{
  if (GEngine || !GEngine->UseSound())
  {
    return;
  }
  UWorld* ThisWorld = GEngine->GetWorldFromContextObject(....);
  ....
}

Если указатель GEngine ненулевой, то происходит выход из функции. Всё хорошо. А вот если указатель GEngine нулевой, то он разыменовывается.

Мы исправили код следующим образом:

void UGameplayStatics::DeactivateReverbEffect(....)
{
  if (GEngine == nullptr || !GEngine->UseSound())
  {
    return;
  }

  UWorld* ThisWorld = GEngine->GetWorldFromContextObject(....);
  ....
}

Интересная опечатка ждёт читателей в следующем фрагменте кода. В нём анализатор обнаружил бессмысленный вызов функции: V530 The return value of function 'Memcmp' is required to be utilized. pathfollowingcomponent.cpp 715
int32 UPathFollowingComponent::OptimizeSegmentVisibility(
  int32 StartIndex)
{
  ....
  if (Path.IsValid())
  {
    Path->ShortcutNodeRefs.Reserve(....);
    Path->ShortcutNodeRefs.SetNumUninitialized(....);
  }
  FPlatformMemory::Memcmp(Path->ShortcutNodeRefs.GetData(),
                          RaycastResult.CorridorPolys,
                          RaycastResult.CorridorPolysCount *
                            sizeof(NavNodeRef));
  ....
}

Результат функции Memcmp не используется. Это и насторожило анализатор.

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

int32 UPathFollowingComponent::OptimizeSegmentVisibility(
  int32 StartIndex)
{
  ....
  if (Path.IsValid())
  {
    Path->ShortcutNodeRefs.Reserve(....);
    Path->ShortcutNodeRefs.SetNumUninitialized(....);

    FPlatformMemory::Memcpy(Path->ShortcutNodeRefs.GetData(),
                            RaycastResult.CorridorPolys,
                            RaycastResult.CorridorPolysCount *
                              sizeof(NavNodeRef));
  }
  ....
}

Поговорим о диагностическом сообщении, которое можно встретить при проверке практически любого проекта. Уж очень распространённый тип ошибки она выявляет. Речь идёт о диагностике V595. В нашей базе ошибок, она занимает первое место по количеству найденных недочётов (см. примеры). На первый взгляд, список не такой уж большой, по сравнению, скажем, с V501. Но дело в том, что диагностики V595 несколько скучны, чтобы выписывать много примеров из какого-то проекта. Часто выписан один пример и сделана приписка вида: And 161 additional diagnostic messages. В половине случаев — это самые настоящие ошибки. Вот как это выглядит:

Рисунок 4. Ужасы диагностики V595.

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

float SGammaUIPanel::OnGetGamma() const
{
  float DisplayGamma = GEngine->DisplayGamma;
  return GEngine ? DisplayGamma : 2.2f;
}
Предупреждение: V595 The 'GEngine' pointer was utilized before it was verified against nullptr. Check lines: 47, 48. gammauipanel.cpp 47

Мы исправили эту функцию следующим образом:
float SGammaUIPanel::OnGetGamma() const
{
  return GEngine ? GEngine->DisplayGamma : 2.2f;
}

Перейдем к следующему фрагменту:

V517 The use of 'if (A) {...} else if (A) {...}' pattern was detected. There is a probability of logical error presence. Check lines: 289, 299. automationreport.cpp 289

void FAutomationReport::ClustersUpdated(const int32 NumClusters)
{
  ...
  //Fixup Results array
  if( NumClusters > Results.Num() )         //<==
  {
    for( int32 ClusterIndex = Results.Num();
         ClusterIndex < NumClusters; ++ClusterIndex )
    {
      ....
      Results.Add( AutomationTestResult );
    }
  }
  else if( NumClusters > Results.Num() )    //<==
  {
    Results.RemoveAt(NumClusters, Results.Num() - NumClusters);
  }
  ....
}

В текущем виде второе условие никогда не выполняется. Логично предположить, что ошибка в знаке второго условия, чтобы из массива 'Result' удалялись лишние элементы:
void FAutomationReport::ClustersUpdated(const int32 NumClusters)
{
  ....
  //Fixup Results array
  if( NumClusters > Results.Num() )
  {
    for( int32 ClusterIndex = Results.Num();
         ClusterIndex < NumClusters; ++ClusterIndex )
    {
      ....
      Results.Add( AutomationTestResult );
    }
  }
  else if( NumClusters < Results.Num() )
  {
    Results.RemoveAt(NumClusters, Results.Num() - NumClusters);
  }
  ....
}

Пример кода на внимательное чтение. Сообщение: V616 The 'DT_POLYTYPE_GROUND' named constant with the value of 0 is used in the bitwise operation. pimplrecastnavmesh.cpp 2006
/// Flags representing the type of a navigation mesh polygon.
enum dtPolyTypes
{
  DT_POLYTYPE_GROUND = 0,
  DT_POLYTYPE_OFFMESH_POINT = 1,
  DT_POLYTYPE_OFFMESH_SEGMENT = 2,
};

uint8 GetValidEnds(...., const dtPoly& Poly)
{
  ....
  if ((Poly.getType() & DT_POLYTYPE_GROUND) != 0)
  {
    return false;
  }
  ....
}

На первый взгляд всё хорошо. Кажется, что по маске выделяется какой-то бит и проверяется его значение. Но, на самом деле, в перечислении 'dtPolyTypes' определяются просто именованные константы, не предназначенные для выделения определённых бит.

В данном условии константа DT_POLYTYPE_GROUND равна 0, а значит условие никогда не выполнится.

Исправленный вариант:

uint8 GetValidEnds(...., const dtPoly& Poly)
{
  ....
  if (Poly.getType() == DT_POLYTYPE_GROUND)
  {
    return false;
  }
  ....
}

Выявленная опечатка: V501 There are identical sub-expressions to the left and to the right of the '||' operator: !bc.lclusters ||!bc.lclusters detourtilecache.cpp 687
dtStatus dtTileCache::buildNavMeshTile(....)
{
  ....
  bc.lcset = dtAllocTileCacheContourSet(m_talloc);
  bc.lclusters = dtAllocTileCacheClusterSet(m_talloc);
  if (!bc.lclusters || !bc.lclusters)   //<==
    return status;
  status = dtBuildTileCacheContours(....);
  ....
}

При копировании переменной, её забыли переименовать из 'bc.lclusters' в 'bc.lcset'.

Результаты регулярной проверки


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

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

Сейчас мы отлично подкрепим свои слова с помощью проекта Unreal Engine.

В начале мы правили код, не обращая внимания, свежий это код или старый. Было не до того. Зато мы очень хорошо заметили, как анализатор PVS-Studio начал выявлять ошибки в новом или исправленном коде, когда мы свели количество предупреждений до 0.

На самом деле, мы возились с кодом немного больше, чем 17 рабочих дней. Когда мы закончили вносить правки, и анализатор начал выдавать 0 предупреждений, мы в течение ещё 2 дней ждали, когда разработчики Unreal Engine примут наш последний Pull Request. И в течение этого времени продолжали обновляться из основного репозитория и выполнять проверку кода.

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

Фактически, конец графика «количество сообщений» приобрёл вид:

Рисунок 5. Схематичный график роста количества предупреждений, после того как их стало 0.

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

День первый


Первое сообщение анализатора: V560 A part of conditional expression is always true: FBasicToken::TOKEN_Guid. k2node_mathexpression.cpp 235
virtual FString ToString() const override
{
  if (Token.TokenType == FBasicToken::TOKEN_Identifier ||
      FBasicToken::TOKEN_Guid) //<==
  {
    ....
  }
  else if (Token.TokenType == FBasicToken::TOKEN_Const)
  {
    ....
}

Забыли дописать «Token.TokenType ==». В результате, поскольку именованная константа 'FBasicToken::TOKEN_Guid' не равна 0, условие всегда истинно.

Второе сообщение анализатора: V611 The memory was allocated using 'new T[]' operator but was released using the 'delete' operator. Consider inspecting this code. It's probably better to use 'delete [] CompressedDataRaw;'. crashupload.cpp 222

void FCrashUpload::CompressAndSendData()
{
  ....
  uint8* CompressedDataRaw = new uint8[BufferSize];         //<==

  int32 CompressedSize = BufferSize;
  int32 UncompressedSize = UncompressedData.Num();
  ....
  // Copy compressed data into the array.
  TArray<uint8> CompressedData;
  CompressedData.Append( CompressedDataRaw, CompressedSize );
  delete CompressedDataRaw;                                 //<==
  CompressedDataRaw = nullptr;
  ....
}

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

День второй


Первое предупреждение анализатора: V521 Such expressions using the ',' operator are dangerous. Make sure the expression is correct. unrealaudiodevicewasapi.cpp 128
static void GetArrayOfSpeakers(....)
{
  Speakers.Reset();
  uint32 ChanCount = 0;
  // Build a flag field of the speaker outputs of this device
  for (uint32 SpeakerTypeIndex = 0;
       SpeakerTypeIndex < ESpeaker::SPEAKER_TYPE_COUNT,    //<==
       ChanCount < NumChannels; ++SpeakerTypeIndex)
  {
    ....
  }

  check(ChanCount == NumChannels);
}

Хорошая такая, жирная ошибка.

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

В результате, условием остановки цикла является только эта проверка: ChanCount < NumChannels.

Исправленное условие:

static void GetArrayOfSpeakers(....)
{
  Speakers.Reset();
  uint32 ChanCount = 0;
  // Build a flag field of the speaker outputs of this device
  for (uint32 SpeakerTypeIndex = 0;
       SpeakerTypeIndex < ESpeaker::SPEAKER_TYPE_COUNT &&
       ChanCount < NumChannels; ++SpeakerTypeIndex)
  {
    ....
  }
  check(ChanCount == NumChannels);
}

Второе предупреждение. V543 It is odd that value '-1' is assigned to the variable 'Result' of HRESULT type. unrealaudiodevicewasapi.cpp 568
#define S_OK       ((HRESULT)0L)
#define S_FALSE    ((HRESULT)1L)

bool
FUnrealAudioWasapi::OpenDevice(uint32 DeviceIndex,
                               EStreamType::Type StreamType)
{
  check(WasapiInfo.DeviceEnumerator);

  IMMDevice* Device = nullptr;
  IMMDeviceCollection* DeviceList = nullptr;
  WAVEFORMATEX* DeviceFormat = nullptr;
  FDeviceInfo DeviceInfo;
  HRESULT Result = S_OK;                      //<==
  ....
  if (!GetDeviceInfo(DataFlow, DeviceIndex, DeviceInfo))
  {
    Result = -1;                              //<==
    goto Cleanup;
  }
  ....
}

HRESULT — это 32-разрядное значение, разделенное на три различных поля: код серьезности ошибки, код устройства и код ошибки. Для работы со значением HRESULT служат специальные константы, такие как S_OK, E_FAIL, E_ABORT и так далее. А для проверки значений типа HRESULT предназначены такие макросы как SUCCEEDED, FAILED.

Предупреждение V543 выдается в том случае, если в переменную типа HRESULT пытаются записать значение -1, true или false.

Запись значения "-1" некорректна. Если хочется сообщить о какой-то непонятной ошибке, то следует использовать значение 0x80004005L (Unspecified failure). Эта и аналогичные константы описаны в «WinError.h».

Эх, какое сложное внедрение...


Некоторые программисты и менеджеры могут опечалиться, узнав, что для интеграции статического анализа в их проект потребуется N дней. Однако, не обязательно идти этим путём. Надо понимать, что разработчики Epic Games выбрали ИДЕАЛЬНЫЙ ПУТЬ, но не самый простой и быстрый.

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

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

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

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

«А вы отписали разработчикам?»


После каждой статьи о проверке какого-нибудь проекта нас спрашивают: «А вы сообщили разработчикам?» И хотя мы всегда сообщаем, такие вопросы изрядно утомляют. Однако, в этот раз мы не только «отписали разработчикам», но и исправили все эти ошибки. В чем любой желающий может убедиться в репозитории на GitHub.

Заключение


Надеемся, проделанная работа по правке и улучшению кода Unreal Engine понравилась разработчикам Epic Games и принесла пользу проекту. И ждем новых проектов на движке Unreal Engine.

Некоторые выводы, которые можно сделать по результатам нашей работы:

  1. Код проекта Unreal Engine весьма качественный. Пусть читателей не смущает большое количество предупреждений на начальном этапе. Это нормальная ситуация. Большинство этих предупреждений было убрано с помощью различных методов и настроек. Количество настоящих ошибок, обнаруженных в коде, для проекта такого размера, очень невелико.
  2. Править чужой незнакомый код часто очень сложно. Впрочем, это и так понятно любому разработчику на интуитивном уровне. Тем не менее решили это отметить.
  3. Скорость «переработки» предупреждений не линейна. Она будет замедляться и это надо учитывать при расчёте, сколько ещё осталось до конца правок.
  4. Максимальная польза от статического анализа может быть получена только при его регулярном использовании.
Спасибо всем, кто прочитал эту статью и желаем всем безбажного кода. С уважением, разработчики анализатора PVS-Studio. И сейчас самое время его скачать и попробовать на своём проекте.

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.

Датчики и микроконтроллеры. Часть 3. Измеряем ток и напряжение


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

Содержание


Часть 1. Мат. часть. В ней рассматривается датчик, не привязанный к какому-то конкретному измеряемому параметру. Рассматриваются статические и динамические характеристики датчика.
Часть 2. Датчики климат-контроля. В ней рассматриваются особенности работы с датчиками температуры, влажности, давления и газового состава
Часть 3. Датчики электрических величин. В этой части я рассмотрю датчики тока и напряжения

ВНИМАНИЕ: Не вставляйте спицы в розеткуНе лезьте в сеть 220В без необходимых на то навыков!



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

Рисунок 1: напряжение в цепях переменного тока
При измерении переменного тока мы имеем 4 различных величины, которыми будем руководствоваться при проведении измерений. Все нижеприведенные формулы и термины применимы и к измерителю тока.
1. Мгновенное значение напряжения — это разность потенциалов между двумя точками. Измеренная в определенный момент времени. Это значение является базовым во всех остальных вычислениях. Фактически, наша задача будет заключаться в считывании последовательного набора мгновенных значений напряжения через равные промежутки времени, чтобы впоследствии с их помощью получить некие другие данные.
u = u(t) (1)
Получится примерно следующий график:

Рисунок 2: Измерение серии мгновенных значений напряжения
При выборе частоты опроса датчиков мы руководствуемся теоремой Котельникова-Шеннона, когда для того, чтобы восстановить сигнал с частотой f необходимо производить считывание с частотой Больше чем 2f. Отмечу необходимость строгого неравенства, т. е. если нам надо оцифровать сигнал с частотой 50Гц, то считывание необходимо производить с частотой, не менее 101 Гц. Но, понятное дело, чем больше тем лучше.
Если вспомнить ГОСТ на показатели качества электроэнергии, в раздел Гармоник, то там мы найдем, что интересными для нашего измерения являются гармоники вплоть до 40, т. е. до 2кГц. И микросхемы счетчиков электроэнергии производят считывание с частотой 4096 раз в секунду. Степень двойки выбрана для того, чтобы можно было применять быстрые алгоритмы преобразования Фурье.
Имея этот большой набор данных, собранный за единицу времени, например, 1с переходим к следующим:
2. Амплитудное значение напряжения — которое определяется как максимальное по модулю значение из нашей выборки:
(2)
где [u(t)] – массив с данными.
Для гармонических колебаний это значение используется в следующей формуле:
(3)
3. Среднее значение напряжения, т. е. Среднее арифметическое, т. е. постоянная составляющая переменного напряжения.
(4)
Где — период дискретизации аналогового сигнала. Я намеренно пишу сумму вместо интеграла. В промышленной сети переменного тока среднее значение должно быть равно нулю. Если это условие не выполняется, могут быть определенные проблемы, так как постоянный ток подмагничивает трансформаторы, вводя их в насыщение, либо подогревает питающую линию. Последнее кстати может быть полезно для решения проблемы намерзшего льда на проводах — провод подогревают и лед отваливается.
В слаботочных аналоговых цепях постоянная составляющая присутствует сплошь и рядом и может быть очень полезна. А если она нам будет мешать, то мы от нее быстро избавимся, но об этом позже.
4. Среднеквадратичное значение напряжения. — известное также как действующее значение напряжения — на линейной активной нагрузке оно совершает ту же самую работу, что и постоянное напряжение аналогичного уровня. Определяется по следующей формуле:
(5)
При измерении напряжения в розетке нас, как правило, интересует именно это самое действующее напряжение, которое составляет 230/380В.
Амплитудное и действующее значения синусоидального напряжения связаны между собой через. Во время проектирования измерительной системы нас будет интересовать в первую очередь именно амплитудное значение напряжения и тока.
Во время измерений будем руководствоваться одной из следующих схем:

Рисунок 3: Подключение измерительных приборов
Загадка для ума — обе схемы подключения правильные, но при каких обстоятельствах важно правильно выбрать одну из них? Ответы в комментариях.

Датчики напряжения



Первым делом произведем измерение напряжения. Все нижесказанное относится к напряжениям не менее напряжения питания АЦП нашего контроллера. Таким образом, нам необходимо измерить напряжение с амплитудой большей, чем АЦП способен прожевать. Следовательно, уровень напряжения необходимо понизить — т.е. произвести ослабление сигнала.
Для малых напряжений (например как термоЭДС термопары из прошлой статьи) нужна обратная задача — усиление сигнала. Это более сложная задача и мы обязательно к ней вернемся в следующих статьях.
Поставим условие для расчета наших датчиков:
Измеряемое напряжение: переменное, 0-1000В, частота 50/60Гц. Для трехфазного напряжения в 380В амплитудное составляет почти 600В, а ведь есть сети и на 660В. Так что пусть будет. На самом деле этот расчет я взял из своей железки и переделывать его мне лень.
Выходное напряжение ± 1,65В — половина от питающего +3,3В
Делитель напряжения

Самым простым способом будет делитель напряжения.

Рисунок 4: Делитель напряжения
Напряжение на нашем измерительном приборе будет определяться как входное напряжение, умноженное на коэффициент делителя:
(6)
При выборе сопротивления резисторов необходимо определиться со следующими требованиями:
1. Ток через цепь резисторов должен быть на 1-2 порядка больше, чем ток нашего измерительного прибора для того, чтобы этот ток не влиял на показания. Измеритель имеет конечное значение сопротивления и получается, что к резистору R2 подключен еще один резистор. Чем внутреннее сопротивление больше, чем ближе общее сопротивление к сопротивлению R2. Сопротивление внутренних цепей АЦП ATmega, к примеру, 100 МОм.
2. Мощность, выделяемая на нашем делителе не должна быть слишком большой
3. Приложенное входное напряжение должно быть меньше напряжения пробоя резистора.
Пусть ток через наш делатель составит 1мА. Тогда, общее сопротивление резисторов будет равно:
(7)
Определим требуемый коэффициент передачи нашего делителя:
(8)
По ряду номиналов резисторов E24 выберем ближайшее значение, дающее около 1МОм:
R1 = 990 кОм (три резистора по 330 кОм)
тогда резистор R2 = K·R1 = 1,63 кОм
Из ряда Е24 выбираем второй резистор R2 = 1,6 кОм
Проверим коэффициент:
(9)
Погрешность с ранее расчетным 2,3%, что нас вполне устроит. Вообще-то можно точно подобрать резисторы из ряда Е192, но это не обязательно.
Выглядеть измерительная цепь будет следующим образом(кусок из рабочей схемы):

Рисунок 5: Цепь измерителя напряжения
На базе цепочки R17-C22 собран фильтр низких частот, чтобы никакие помехи нам не были страшны. Можем кстати быстренько посмотреть что делает этот фильтр:

Рисунок 6: Частотная характеристика фильтра
Как видно из графика ЛАЧХ, для рабочего диапазона частот от 0 до 2000Гц фильтр практически не портит амплитуду и фазу сигнала. А вот помехи на частотах порядка 100кГц и выше, исходящие от ВЧ преобразователей, надежно давятся. Так что все супер.
Достоинства:
  • широкий диапазон напряжений и частот, определяемый номиналами резисторов;
  • высокая точность, опять таки определяемая точностью и термостабильностью резисторов;
  • измеряет постоянное и переменное напряжение.

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

Трансформатор напряжения


Рисунок 7: Трансформатор напряжения
Для случаев, когда нужно измерить очень высокие напряжения, 6/10кВ и выше, используется трансформатор напряжения Фактически, он представляет собой обычный трансформатор, основным режимом работы которого является режим холостого хода.
Класс точности такого трансформатора зависит от рабочего участка характеристики намагничивания. Ведь нам надо пропустить через него не просто сигнал с определенной амплитудой, но и не испортить ее форму. Здесь как раз проблема — трансформатор практически не пропускает гармоники ввиду больших индуктивностей. Так что для измерения гармонических искажений большинство трансформаторов напряжения не подойдет.
Обычный класс точности трансформатора — 0,5, 1, 3
Достоинства:
  • огромный диапазон рабочих напряжений — до сотен киловольт и выше;
  • столь необходимая гальваническая развязка.

Недостатки:
  • работает на определенной полосе частот;
  • работает только с переменным напряжением;

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

Рисунок 8: Измерение постоянного тока с помощью магнитного усилителя
Почитать об этом чуде техники можно здесь: http://ift.tt/1MT8Xlk
Если тема будет интересна, то запилю обзор этих старинных регуляторов.
Электронный изолированный датчик

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

Рисунок 9: Структурная схема электронного изолированного датчика
Мне попадались на глаза только промышленные датчики с выходом по напряжению 0-10В или по току 0-10мА. В отличие от предыдущих датчиков выдает однополярный сигнал. В принципе, такую схему можно разработать и самостоятельно.
Достоинства:
гальваническая развязка;
высокая точность;
широкий диапазон напряжений и частот;
измеряет постоянное и переменное напряжение.

Недостатки:
дорого;
сложная схемотехника.

Дополнительные ссылки

http://ift.tt/1MT8VtC
http://ift.tt/1QHl0Zb
Измерители электроэнергиии STMP32 http://ift.tt/1QHl3Ek
http://ift.tt/1QHl3Ep

Датчики тока



Измерительный шунт

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

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

Рисунок 11: Токовый шунт типа ШСМ
На напряжение в 75мВ откалибровано большинство измерительных головок для шунтов. Обратите внимание на вторую пару винтов — они предназначены специально для подключения к измерительному прибору для снижения потерь.
Для измерения тока с помощью таких шунтов требуется использовать операционные усилители. При этом, средний коэффициент усиления составляет 20-40, что под силу широко-распространенным операционным усилителям. В принципе, такой можно сварганить на базе одного биполярного транзистора.
Получим следующую схему:

Рисунок 12: Использование ОУ в качестве усилителя
Следует учитывать, что при измерении переменного тока, выходной сигнал будет биполярный и операционный усилитель требуется запитать от двухполярного источника питания.
Глянем на всякий случай, как работает наша схема:

Рисунок 13: Моделирование усилителя датчика тока
На вход подаем 75мВ, умножаем на 20, на выходе имеем сигнал с амплитудой 1,5В для тока в 10А. В следующем материале мы разберемся чем плох биполярный сигнал.
Достоинства:
  • высокая точность;
  • широкий диапазон напряжений и частот;
  • измеряет постоянный и переменный ток.

Недостатки:
  • отсутствует гальваническая развязка;
  • низкий КПД.

Измерительный трансформатор тока

Измерительный трансформатор тока представляет собой трансформатор, первичная обмотка которого подключается к источнику тока, а вторичная замыкается на измерительные приборы или устройства защитной автоматики.
Трансформаторы тока используются для измерения токов в сильноточных цепях, зачастую я высоким потенциалом. Например, нам захотелось измерить ток в сети 10кВ. Либо, мы хотим получить простой и относительно дешевый способ гальванической развязки измеряемой цепи тока нашего устройства на 220В. Основная проблема трансформаторов тока заключается в том, что они умеют измерять только переменное напряжение.
Трансформатор тока всегда нагружается. Если вторичная обмотка трансформатора тока окажется разомкнутой, то на ней возникнет потенциал в пару тысяч киловольт, который покалечит персонал и выведет из строя прибор, пробив его изоляцию.
Трансформаторы бывают со встроенной первичной обмоткой. Например такие:

Рисунок 14: Трансформатор тока серии CS2106L от Coilcraft
Либо вот такие слоники, имеющие подобие первичной обмотки в виде огромной шины, либо вовсе окно для пропускания через него провода

Рисунок 15: Промышленный трансформатор тока на много ампер
Основной недостаток трансформатора тока — это работа только на определенной частоте. Шаг влево-шаг вправо — расстрел. Виной всему металлический сердечник.
А вот если мы его удалим, то получим воздушный трансформатор, или, т. н. Катушку Роговского:

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

Рисунок 17: Датчик катушка роговского
Диапазон измеряемых токов — от десятков до тысяч ампер, но они страдают от невысокой точности.
Достоинства:
  • гальваническая развязка;
  • работа с большими токами в тысячи Ампер;

Недостатки:
  • измеряет только переменный ток в определенном диапазоне частот(кроме катушки Роговского);
  • изменяет фазу сигнала и требует компенсации

Датчики тока на эффекте Холла

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

Рисунок 18: Эффект Холла
При создании датчика мы берем магнитопровод, пропускаем через него провод измеряемой цепи и в разрез магнитопровода помещаем датчик Холла, получая датчик тока открытого типа:

Рисунок 19: Датчик тока на эффекте Холла открытого типа
Достоинством такого датчика является простота. Недостатком — наличие подмагничивания сердечника, следовательно, повышение нелинейности показаний.
Добавим на сердечник обмотку и пустим по ней ток, пропорциональный измеряемому току:

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

Рисунок 22: Датчик тока на эффекте Холла
Существуют датчики с разделяемым сердечником — однако их стоимость просто зашкаливает.
Датчики с интегрированной силовой цепью на базе эффекта Холла с гальванической развязкой 2,1кВ и 3кВ выпускаются компанией Allegro. Ввиду своих малых размеров они не обеспечивают высокой точности, но зато компактны и просты в использовании.

Рисунок 23: датчик тока Allegro ACS754
  • Датчик ACS712 – измерение постоянного и переменного тока до 30А с точностью ± 1,5%
  • Датчик ACS713 – оптимизирован для измерения постоянного тока до 30А. Имеет вдвое большую чувствительность чем его универсальный собрат.
  • Датчик ACS754 – измерение постоянного и переменного тока до 200А с точностью ± 1,5%
  • Датчик ACS755 – оптимизирован для измерения постоянного тока.
  • Датчик ACS756 – датчик для измерения постоянного и переменного тока до 100А с напряжением питания 3-5В.


Рисунок 24: Зависимость выходного напряжения датчика от тока
Достоинства:
  • широкий диапазон измеряемых токов с частотой до 50-100кГц и выше;
  • измеряет постоянный и переменный ток.
  • гальваническая развязка

Недостатки:
  • Дорого

Дополнительные ссылки:

Измерительные трансформаторы постоянного тока http://ift.tt/1MT8Xlk
Катушки Роговского http://ift.tt/1MT8XBL
Эффект Холла в википедии: http://ift.tt/1h7LKQu
Датчики Холла http://ift.tt/1QHl1fL
Данилов А. Современные промышленные датчики тока http://ift.tt/1MT8XBN
Проектирование схем на базе аналогового усилителя HCPL-7851 http://ift.tt/1QHl3V1

Заключение

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

This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.