MoDyz: Примеры работы с виртуальным дисплеем

MoDyz - это приложение для Android-устройств, позволяющее использовать их в качестве виртуального дисплея/панели управления для микроконтроллеров.
MoDyz общается с микроконтроллером по Bluetooth- или WiFi-каналу и старательно выполняет все поступающие от МК команды, позволяющие рисовать на экране смартфона, выводить текстовые сообщения, создавать органы управления…
И существенно экономить на пинах МК, оставляя их свободными для более важных дел.

Но к делу. Вот простейший скетч - ставший классикой “Hello world!”:

#include <SoftwareSerial.h>

SoftwareSerial mySerial(7, 6); // RX, TX

String cmdBuffer;

void setup() {
  mySerial.begin(9600); // инициализация и настройка скорости программного последовательного порта
}

void loop() {
  readSerialData();
}

void drawHelloWorld() {
  mySerial.print("#ZS30;");
  mySerial.print("#FZ8,1;");
  mySerial.println("#ZC270,100,Hello world!;");
}

void readSerialData() {
  char c;
  if (mySerial.available()) {
    c = (char)mySerial.read();
    if (c==0x0A || c==0x0D) interpreteCommand();
    else cmdBuffer=cmdBuffer+c;
  }
} // of readSerialData

void interpreteCommand() {
  if(cmdBuffer.length()==0) return;
  if(cmdBuffer.equalsIgnoreCase("drw"))          drawHelloWorld();//drawScene();
  cmdBuffer = "";
} // of interpreteCommand

Скетч предназначен для работы по Bluetooth-каналу. В конкретном примере Bluetooth-модуль подключен к пинам 6, 7 и использует программный Serial (аппаратный остается для заливки скетчей и для вывода отладочной информации).
После инициализации канала в функции setup() скетч непрерывно прослушивает его (в функции loop()) и после обнаружения той или иной команды (в этом скетче - одна единственная команда “drw”), поступившей от смартфона, выполняет соответствующие действия. В данном скетче это функция drawHelloWorld():

  • задание размера шрифта
  • задание цвета шрифта
  • вывод текста в заданном месте экрана

Результат работы этого простенького примера (часть скриншота):

1 лайк

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

1 лайк

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

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

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

#include <SoftwareSerial.h>
// библиотека, автоматизирующая процесс создания управляющих строковых последовательностей
#include <mdwriter.h>
#include <mdsender_bt.h>

SoftwareSerial mySerial(7, 6); // RX, TX
MD_Sender_BT mdScout;

String cmdBuffer, sResponse;

void setup() {
  // put your setup code here, to run once:
  mySerial.begin(9600); // инициализация и настройка скорости программного последовательного порта

}

void loop() {
  readSerialData();
}

void addCmdString(String sCmd) {
  sResponse += sCmd;
  if(sResponse.length()>100) sendString(sResponse);
}

void sendString(String s) {
  for(int i=0;i<s.length();i++) {
    mySerial.write(s.charAt(i));
  }
  mySerial.write('\r'); mySerial.write('\n');
  sResponse = "";
} // of sendString

void drawHelloWorld() {
  addCmdString(mdScout.setTextColors(C_YELLOW, C_BLACK));
  addCmdString(mdScout.setTextSize(30));
  addCmdString(mdScout.outTextC(270,200,"Hello world!!!"));
  sendString(sResponse);
}

void readSerialData() {
  char c;
  if (mySerial.available()) {
    c = (char)mySerial.read();
    if (c==0x0A || c==0x0D) interpreteCommand();
    else cmdBuffer=cmdBuffer+c;
  }
} // of readSerialData

void interpreteCommand() {
  if(cmdBuffer.length()==0) return;
  if(cmdBuffer.equalsIgnoreCase("drw"))          drawHelloWorld();
  cmdBuffer = "";
} // of interpreteCommand

Основные изменения - в функции drawHelloWorld():

// было
  mySerial.print("#ZS30;");
  mySerial.print("#FZ8,1;");
  mySerial.println("#ZC270,100,Hello world!;");
// стало
  addCmdString(mdScout.setTextColors(C_YELLOW, C_BLACK));
  addCmdString(mdScout.setTextSize(30));
  addCmdString(mdScout.outTextC(270,200,"Hello world!!!"));

Ну и, соответственно, добавились строки подключения необходимых модулей библиотеки (3, 4), инициализации экземпляра класса (7) и функции обслуживания сборки управляющей последовательности (addCmdString) и ее отправки на смартфон (sendString) .

Скриншот результатов работы этого скетча не привожу - он идентичен ранее опубликованному. Только цвет выводимого текста изменен с белого (8) на желтый (C_YELLOW).

В обновленной версии библиотеки MD_Scout появился новый пример “Buttons” (точнее два примера - для подключения смартфона по Bluetooth- и WIFI-каналу соответствено). Этот пример демонстрирует, как с помощью нескольких строчек кода можно создать “телефонную” клавиатуру и использовать ее для запуска тех или иных функций скетча.
Не буду приводить здесь весь скетч - желающие могут ознакомиться с ним, скачав версию библиотеки за 30.11.2022. Покажу лишь два куска кода, отличающие его от первого примера (“hello world”).


Кусок № 1: Функция drawButtons():

void drawButtons() {
  int x, y, w, h, dx, dy;

  if(dWidth<700) textSize=30;
  else textSize=40;

  addCmdString(mdScout.setTextColors(C_YELLOW, C_BLACK));
  addCmdString(mdScout.setTextSize(textSize));
  outCenteredString("Hello world!!!");

  if(dWidth==DWIDTH) addCmdString(mdScout.defineTouchButton(10, 10, 310, 90, 112, 132, "1080"));
  else addCmdString(mdScout.defineTouchButton(10, 10, 310, 90, 112, 132, "540"));
  w=dWidth/18*5; h=w/2;
  dx=10;         dy=10;
 
  x=dWidth/18; y=300;
  x = addButton(x,y,w,h,dx,1,"1");
  x = addButton(x,y,w,h,dx,2,"2");
  x = addButton(x,y,w,h,dx,3,"3");

  x=dWidth/18; y=y+h+dy;
  x = addButton(x,y,w,h,dx,4,"4");
  x = addButton(x,y,w,h,dx,5,"5");
  x = addButton(x,y,w,h,dx,6,"6");
 
  x=dWidth/18; y=y+h+dy;
  x = addButton(x,y,w,h,dx,7,"7");
  x = addButton(x,y,w,h,dx,8,"8");
  x = addButton(x,y,w,h,dx,9,"9");

  x=dWidth/18; y=y+h+dy;
  x = addButton(x,y,w,h,dx,10,"*");
  x = addButton(x,y,w,h,dx,0,"0");
  x = addButton(x,y,w,h,dx,11,"#");
 
  sendString(sResponse);
}

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

Далее формируется клавиатура, состоящая из четырех рядов по три кнопки в каждом. В сумме это двенадцать вызовов функции addButton():

int addButton(int x, int y, int w, int h, byte dx, byte b, String s) {
  addCmdString(mdScout.defineTouchButton( x, y, x+w, y+h, 100+b, 120+b, s));
  return x+w+dx;
}

Основная операция в ней вызов метода defineTouchButton, создающего на экране новую кнопку по координатам ее левого верхнего (x, y) и правого нижнего (x+w, y+h) угла, с указанием кодов, которые будут отправляться микроконтроллеру при нажатии (100+b) и отпускании (120+b) этой клавиши, и, наконец, текста (s), выводимого на кнопке.
В результате выполнения этой функции получаем на экране смартфона следующую картинку:


Кусок №2: После того, как клавиатура создана, в дело вступает еще одна функция interpreteCommand():

void interpreteCommand() {
  if(cmdBuffer.length()==0) return;
  if(cmdBuffer.equalsIgnoreCase("drw"))          drawButtons();
  else if(cmdBuffer.substring(0,2).equals("10")) outCodeOn(String(cmdBuffer.charAt(2)));
  else if(cmdBuffer.substring(0,2).equals("12")) outCodeOff(String(cmdBuffer.charAt(2)));
  else if(cmdBuffer.equals("110")) outCodeOn("*");
  else if(cmdBuffer.equals("130")) outCodeOff("*");
  else if(cmdBuffer.equals("111")) outCodeOn("#");
  else if(cmdBuffer.equals("131")) outCodeOff("#");
  //else if(cmdBuffer.equals("112")) outCodeOn("#");
  else if(cmdBuffer.equals("132")) {
    if (dWidth==DWIDTH) dWidth=DWIDTH*2; else dWidth=DWIDTH;
    addCmdString("#DF1;"); // заполнение дисплея черным цветом
    sendString(sResponse);
    drawButtons();
  }
  else { Serial.print("code "); Serial.println(cmdBuffer); }
  cmdBuffer = "";
} // of interpreteCommand

В ней анализируются управляющие коды, поступающие от смартфона при нажатии/отпускании той или иной кнопки и в зависимости от результата либо выводится строка, информирующая о нажатии/отпускании той или иной кнопки (на приведенном снимке экрана отображен результат отпускания кнопки ‘6’ строка “Кнопка 6 отпущена”), либо выполняется иное действие.

1 лайк

Одна из простейших команд базовой графики - рисование точки в заданных координатах. Чтобы вывести точку на экране, например, в координатах (40,240) необходимо отправит на смартфон команду GP такого вида: #GP40,240; . При подключенной библиотеке mdwriter для этой цели можно воспользоваться функцией drawPoint(40,240) .
В результате на экране появится точка того цвета, который до этого был задан командой определения цветов переднего плана и фона для точек и прямых - FG. Эта команда задает два цвета - цвет переднего плана (именно он используется при рисовании точки или отрезка прямой либо иной линии) и цвет фона (зарезервирован для единообразия формата этой команды с подобными из группы настройки инструментов рисования, его предполагается использовать для поддержки каких-либо дополнительных эффектов). Например, если предполагается рисовать красным цветом (цвет переднего плана) на черном фоне (цвет фона), то необходимо отправить в смартфон либо непосредственно команду #FG3,1;, либо воспользоваться для этого функцией из библиотеки mdwriter: setLineColors(C_RED,C_BLACK);. Еще одна команда, оказывающая влияние на результат работы команды GP - это команда задания толщины линии GZ (или библиотечная функция setLineWidth). Например, чтобы приступить к рисованию линиями толщиной 5 пикселей, следует отправить команду #GZ5; или вызвать функцию setLineWidth(5);. После этого, вплоть до получения новой команды GZ на дисплее смартфона будут выводиться линии толщиной 5 пикселей или рисоваться точки диаметром 5 пикселей.

Почему рисуются круглые точки, а не квадратные? Android позволяет оформлять концы выводимых на дисплей отрезков тремя различными способами - обрезая полосу, представляющую широкий отрезок, под прямым углом к его оси, рисуя на конце отрезка полукруг, и формируя конец отрезка в виде “карандаша” (что-то вроде =>). При рисовании отрезками прямых второй способ оказывается наиболее предпочтительным, так как не создает неровных стыков отрезков. Побочный эффект этого - отрисовка “толстых” точек в форме кругов, а не квадратов.

В качестве примера выведем шестнадцать точек разного цвета и с постепенно увеличивающимся диаметром точки:

  for(i=0;i<15;i++) {
    addCmdString(mdScout.setLineWidth(i*3+1));
    addCmdString(mdScout.setLineColors(i+1,1));
    addCmdString(mdScout.drawPoint((i+1)*32,80));
  }

Вот что получается в результате работы этого цикла (вырезка с экрана смартфона):

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

Ну, эта команда и не предназначена для заполнения графикой всей площади дисплея. А, например, с моделированием мигающего светодиода “точкой” диаметром в 10-15 пикселей она вполне справится.
И это лишь один из имеющихся инструментов - самый простой, самый медленный. Есть в арсенале MoDyz’а и рисование линий, и заливка областей. Этот список открыт - может дополняться по мере возникновения потребности в том или ином инструменте.

После точек познакомимся с линиями.

Имеются две команды рисования отрезков прямой - построение отрезка по двум заданным точкам (GD) и проведение отрезка из текущей позиции пера в новую точку с заданными в команде координатами (GW).

Что определяет текущую позицию пера? Изначально она совпадает с левым верхним углом дисплея и после выполнения той или иной команды, использующей перо, перемещается в позицию, соответствующую последней паре координат из этой команды. То есть, рисуем точку - перо смещается в эту точку; рисуем отрезок - перо оказывается в позиции с координатами x1,y1 (см. ниже). И так далее…

Формат первой команды: #GDx0,y0,x1,y1;. Используя библиотеку mdwriter, соответствующую последовательность символов можно сгенерировать с помощью функции drawLine(x0,y0,x1,y1).

Вторая команда оказывается чуть покороче: #GWx1,y1;. В библиотеке mdwriter ей соответствует функция drawLineTo(x1,y1).

Как и при рисовании точек команды рисования линий учитывают настройки цвета и толщины пера, задаваемые командами FG и GZ.

Далее - небольшой пример применения вышеупомянутых функций:

 sResponse = "";
  addCmdString("#DF1;"); // заполнение дисплея черным цветом
  // линии по двум точкам
  addCmdString(mdScout.setLineWidth(1));
  addCmdString(mdScout.setLineColors(C_GREEN,1));
  addCmdString(mdScout.drawLine(50,50,500,50));
  addCmdString(mdScout.drawLine(50,70,500,250));
  // линии из текущей точки в заданную
  addCmdString(mdScout.setLineWidth(3));
  addCmdString(mdScout.setLineColors(C_YELLOW,1));
  addCmdString(mdScout.drawLineTo(50,300));
  addCmdString(mdScout.drawLineTo(500,300));
  // толстые линии
  addCmdString(mdScout.setLineWidth(17));
  addCmdString(mdScout.setLineColors(C_LTGRAY,1));
  addCmdString(mdScout.drawLine(50,500,230,350));
  addCmdString(mdScout.drawLineTo(390,500));

  addCmdString(mdScout.drawLineTo(390,750));
  addCmdString(mdScout.drawLineTo(70,750));
  addCmdString(mdScout.drawLineTo(70,530));
  addCmdString(mdScout.drawLineTo(370,530));

  sendString(sResponse);

Сначала все поле дисплея заливается черным цветом (команда “#DF1;”). Затем выводится два отрезка зеленого цвета толщиной 1 пиксель. После выполнения второй из этих команд перо оказывается в точке с координатами (500,250). После настройки пера на желтый цвет и толщину линии в 3 пикселя по трем точкам (500,250) (50,300) и (500,300) строится ломаная линия. В конце примера, после переключения на толстую (17 пикселей!) линию серого цвета строится еще одна ломаная линия, иллюстрирующая плавное сопряжение отдельных отрезков, имеющих закругленные концы. Упоминавшиеся ранее другие режимы оформления концов отрезков (“прямоугольные” и “заостренные” концы) приводят к образованию в вершинах непрезентабельных ступенек.

Результат исполнения приведенных инструкций:

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

Команда Примечание
#GRx0,y0,x1,y1; Рисование прямоугольника. В библиотеке MD_Scout эта команда генерируется вызовом функции drawRectangle(int x0, int y0, int x1, int y1);
#RFx0,y0,x1,y1,colorIdx; Заполнение прямоугольника цветом, переданным в качестве пятого параметра команды. Этой команде соответствует функция fillRectangle(int x0, int y0, int x1, int y1, int colorIdx) библиотеки MD_Scout.
#RLx0,y0,x1,y1; Заполнение прямоугольника цветом заднего плана. Соответствующая функция библиотеки MD_Scout - fillRectangleBG(int x0, int y0, int x1, int y1)
#RSx0,y0,x1,y1; Заполнение прямоугольника цветом переднего плана. Соответствующая функция библиотеки MD_Scout - fillRectangleFG(int x0, int y0, int x1, int y1)

Как и при рисовании точек команды рисования прямоугольников учитывают настройки цвета и толщины пера, задаваемые командами FG и GZ. Что же касается цвета заполнения прямоугольных областей, то здесь учитываются настройки цвета, определенные последней выполненной командой FD (в библиотеке MD_Scout ей соответствует функция SetBrushColors или старый вариант - SetDisplayColors).

Закрепим вышесказанное, построив несколько прямоугольников:

  sResponse = "";
  addCmdString(mdScout.setBrushColors(C_RED,C_BLUE));
  addCmdString(mdScout.fillRectangleFG(60,400,360,520));
  addCmdString(mdScout.fillRectangleBG(180,150,300,460));
  addCmdString(mdScout.setPenColors(C_CYAN,1));
  addCmdString(mdScout.setPenWidth(9));
  addCmdString(mdScout.drawRectangle(60,200,360,280));
  addCmdString(mdScout.setPenColors(C_YELLOW,1));
  addCmdString(mdScout.setPenWidth(1));
  addCmdString(mdScout.drawRectangle(140,230,420,380));
  sendString(sResponse);

Сначала производится настройка цветов кисти - объекта, занимающегося заливкой областей. Для переднего и заднего плана установлены соответственно красный и синий цвета. С использованием этих цветов заливаются две прямоугольные области дисплея - красная и синяя. Покончили с областями и переходим к рисованию прямоугольников. Перед каждым вызовом функции drawRectangle выполним настройку цвета (setPenColors) и толщины (setPenWidth) пера. В результате будут отрисованы два прямоугольника - бирюзовый пером толщиной в 9 пикселей и желтый пером в 1 пиксель.
Последний оператор отправляет сгенерированную объектом mdScout управляющую последовательность на смартфон и на его экране появляется следующее изображение:

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

Две из новых команд имеют прямое отношение к рисованию упомянутых геометрических фигур. Это команда рисования окружности #CDx,y,radius; и команда заливки круга #CFx,y,radius;. Как и в случае с другими графическими примитивами, новые команды учитывают результаты выполнения команд настройки цветов (FG - setPenColors / FD - setBrushColors) и толщины линий (GZ - setPenWidth).

Третья команда имеет формат #CSradius;. Она задает радиус закругления при построении геометрических фигур. Пока что результат ее выполнения учитывается только в командах построения прямоугольников, но, возможно, область применения будет расширена. Будущее покажет…

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

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

  sResponse = "";
  addCmdString("#DF1;"); // заполнение дисплея черным цветом

  addCmdString(mdScout.setPenWidth(1));
  addCmdString(mdScout.setPenColors(C_YELLOW,1));
  addCmdString("#CD150,150,40;"); // вывод окружности
  addCmdString(mdScout.setPenWidth(7));
  addCmdString(mdScout.setPenColors(C_VIOLET,1));
  addCmdString("#CD100,100,90;"); // вывод окружности
  addCmdString(mdScout.setBrushColors(C_LTBLUE,C_GREEN));
  addCmdString("#CF300,100,90;"); // вывод круга
  addCmdString(mdScout.setBrushColors(C_RED,C_GREEN));
  addCmdString("#CF350,150,30;"); // вывод круга
 
  addCmdString("#CS45;"); // определение радиуса закругления
  addCmdString(mdScout.setPenColors(C_MAGENTA,1));
  addCmdString(mdScout.drawRectangle(45,260,375,680));
  addCmdString("#CS30;"); // определение радиуса закругления
  addCmdString(mdScout.setPenWidth(1));
  addCmdString(mdScout.drawRectangle(60,280,360,380));
  addCmdString(mdScout.fillRectangleFG(60,400,360,520));
  addCmdString(mdScout.fillRectangleBG(60,550,360,660));
  sendString(sResponse);

Первый блок операторов (строки 4-13) отвечает за вывод кругов и окружностей, второй (строки 15-22) - за прямоугольники с закругленными углами. В результате выполнения этого отрывка на экране смартфона появляется такое изображение:

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

ZS setTextSize(textSize) задание размера шрифта в пикселях

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

ZF loadFont(fileName) загрузка шрифта из файла с именем fileName и его сохранение в системе под именем fontName (fileName минус расширение)

ZG setFont(fontName) выбор (ранее загруженного) шрифта с именем fontName в качестве текущего

Непосредственно выводом текста занимаются команды

ZC outTextC(x, y, text) вывод текста с выравниванием по центру

ZL outTextL(x, y, text) вывод текста с выравниванием по левому краю

ZR outTextR(x, y, text) вывод текста с выравниванием по правому краю

А далее - пример использования вышеперечисленных функций (пока только для стандартного шрифта Андроида):

  int y0,dy,h;
  sResponse = "";
  addCmdString("#DF1;"); // заполнение дисплея черным цветом
  addCmdString(mdScout.setPenColors(C_RED,C_BLACK));
  addCmdString(mdScout.drawLine(270,10,270,-10));
  addCmdString(mdScout.setPenColors(C_YELLOW,C_BLACK));

  y0=60;
  h=35; dy = h;
  addCmdString(mdScout.drawLine(10,y0,-10,y0));
  addCmdString(mdScout.setTextSize(h));
  addCmdString(mdScout.setTextColors(C_CYAN,C_DKGRAY));
  addCmdString(mdScout.outTextL(270,y0,"outTextL"));
  addCmdString(mdScout.setTextColors(C_CYAN,0));
  addCmdString(mdScout.outTextC(270,y0+dy,"Текст по центру"));
  addCmdString(mdScout.setTextColors(C_CYAN,C_DKGRAY));
  addCmdString(mdScout.outTextR(270,y0+2*dy,"outTextR"));

  y0=y0+4*dy;
  h=65; dy = h;
  addCmdString(mdScout.drawLine(10,y0,-10,y0));
  addCmdString(mdScout.setTextSize(h));
  addCmdString(mdScout.setTextColors(C_MAGENTA,C_DKGRAY));
  addCmdString(mdScout.outTextL(270,y0,"outTextL"));
  addCmdString(mdScout.setTextColors(C_MAGENTA,0));
  addCmdString(mdScout.outTextC(270,y0+dy,"Текст по центру"));
  addCmdString(mdScout.setTextColors(C_MAGENTA,C_DKGRAY));
  addCmdString(mdScout.outTextR(270,y0+2*dy,"outTextR"));
  sendString(sResponse);

Перво-наперво построим сетку из вспомогательных линий. Вертикальная красная линия (строка 5) позволяет проиллюстрировать различия в выполнении команд ZL ZC и ZR, то есть эффект выравнивания. Горизонтальные желтые линии (строки 10 и 21) иллюстрируют тот факт, что координаты опорной точки (x,y) относятся ни к нижнему краю прямоугольной области, в которую выводится текст, ни к ее верхнему краю, как это принято во многих других командах вывода графической информации на экран, а к базовой линии текста, ниже которой отрисовываются лишь нижние части таких символов, как j, p, q, y.

Далее идут два блока операторов, первый из которых (строки 8-17) выводит три строки текста (с выравниваем по левому краю, по центру и по правому краю) шрифтом голубого цвета размером в 35 пикселей. Второй (строки 19-28) делает то же самое, но более крупным шрифтом (65 пикселей) пурпурного цвета.

После выполнения этих операторов на экране появится такое изображение:

А еще выводимый текст можно вращать с помощью команды #TMx,y,alpha;. Но об этом, как и об использовании Truetype-шрифтов чуть позже…

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

Прежде, чем MoDyz сможет вывести строку тем или иным шрифтом, файл с его описанием необходимо сохранить в каталоге “Fonts” ресурсов приложения, который находится по адресу /Android/data/com.stepwood.modyz/files/Fonts. Далее в дело вступают команды ZF и ZG: первая из них загружает шрифт из файла с указанным в тексте команды именем, а вторая - по указанному имени шрифта (совпадает с именем соответствующего файла, но без расширения) выбирает среди загруженных шрифтов необходимый. Выбранный шрифт будет использоваться для вывода всех последующих строк текста, до тех пор, пока не поступит команда ZG с новым именем шрифта. Если переданное имя шрифта не найдено в списке загруженных шрифтов, то дальнейший вывод текста будет производиться стандартным шрифтом (“arial” или как он там в Андроиде называется).
Стандартный шрифт используется также при выводе тех символов, которые не определены в файле текущего шрифта, что проиллюстрировано на картинке примера - шрифты alger, forte, oldeng и playbill не поддерживают кириллицу, в то время как в шрифтах sportsworld и nexascript_heavy соответствующие символы имеются.
Теперь немного о вращении текста. Чаще всего эта возможность используется для вывода текста, повернутого на 90 градусов, например, при оформлении надписей вдоль оси ординат. Такой поворот без особых проблем реализуется и в стандартных библиотеках, поддерживающих работу с TFT-дисплеями. В MoDyz текст можно повернуть на любой угол, например, на 30 градусов, как в представленном примере. Поворот текста настраивается с помощью команды TM, имеющей формат

#TMx,y,alpha;

где параметры x и y определяют точку, вокруг которой будет производиться вращение текста, а alpha задает угол поворота текста в градусах. После получения команды TM MoDyz создаст соответствующую матрицу преобразования и с ее помощью будет вращать все выводимые строки на указанный угол до тех пор, пока не будет получена команда с новыми параметрами. Команда #TM0,0,0; сбрасывает матрицу преобразования в исходное состояние.
Нижеприведенный код иллюстрирует сказанное:

int h,x0,y0;
  char s[40];
  sResponse = "";
  addCmdString("#DF1;"); // заполнение дисплея черным цветом
  addCmdString(mdScout.loadFont("alger.ttf"));
  addCmdString(mdScout.loadFont("forte.ttf"));
  addCmdString(mdScout.loadFont("oldengl.ttf"));
  addCmdString(mdScout.loadFont("playbill.ttf"));
  addCmdString(mdScout.loadFont("sportsworld.otf"));
  addCmdString(mdScout.loadFont("nexascript_heavy.ttf"));
  addCmdString(mdScout.loadFont("vgafixr.fon"));

  h = 55;
  x0 = 40;
  y0 = 290;

  // точка, вокруг которой будет производиться вращение выводимого текста
  addCmdString(mdScout.setPenColors(C_RED,1));
  addCmdString(mdScout.setPenWidth(19));
  addCmdString(mdScout.drawPoint(x0,y0+1*h));
 
  // вывод текста без вращения
  addCmdString(mdScout.setTextColors(C_LTGRAY,0));
  addCmdString(mdScout.setTextSize(h));
  addCmdString(mdScout.setFont("alger"));
  addCmdString(mdScout.outTextL(x0,y0,"Шрифт Algerian"));
  addCmdString(mdScout.setFont("forte"));
  addCmdString(mdScout.outTextL(x0,y0+h,"Шрифт Forte"));
  addCmdString(mdScout.setFont("oldengl"));
  addCmdString(mdScout.outTextL(x0,y0+2*h,"Шрифт Old English"));
  addCmdString(mdScout.setFont("playbill"));
  addCmdString(mdScout.outTextL(x0,y0+3*h,"Шрифт Playbill"));
// ------------
  addCmdString(mdScout.setTextColors(C_YELLOW,0));
  addCmdString(mdScout.setFont("sportsworld"));
  addCmdString(mdScout.outTextL(x0,y0+4*h,"Шрифт sportsworld"));
  addCmdString(mdScout.setFont("nexascript_heavy"));
  addCmdString(mdScout.outTextL(x0,y0+5*h,"Шрифт Nexascript_Heavy"));
  addCmdString(mdScout.setFont("vgafixr"));
  addCmdString(mdScout.outTextL(x0,y0+6*h,"Шрифт Vgafixr"));

  sprintf(s,"#TM%d,%d,%d;",x0,y0+1*h,30);
  addCmdString(s);
 
  // вывод текста с вращением
  addCmdString(mdScout.setTextColors(C_GREEN,0));
  addCmdString(mdScout.setFont("alger"));
  addCmdString(mdScout.outTextL(x0,y0,"Шрифт Algerian"));
  addCmdString(mdScout.setFont("forte"));
  addCmdString(mdScout.outTextL(x0,y0+h,"Шрифт Forte"));
  addCmdString(mdScout.setFont("oldengl"));
  addCmdString(mdScout.outTextL(x0,y0+2*h,"Шрифт Old English"));
  addCmdString(mdScout.setFont("playbill"));
  addCmdString(mdScout.outTextL(x0,y0+3*h,"Шрифт Playbill"));
 
  addCmdString("#TM0,0,0;");

  sendString(sResponse);

строки 5-11: загрузка шрифтов из каталога ресурсов “Fonts”
строки 18-20: вывод красной точки, вокруг которой будет происходить поворот выводимого текста
строки 23-40: Вывод блока текста без поворота - шесть строк шестью различными шрифтами
строки 42-43: Настройка матрицы преобразования (поворот текста на 30 градусов вокруг точки с координатами 40,345)
строки 46-54: Вывод блока текста с поворотом - четыре строки четырьмя различными шрифтами (строки 47-54 - это полная копия строк 25-32, но результат их выполнения дополнительно обрабатывается матрицей преобразования)
строка 56: сброс матрицы преобразования
строка 58: передача последних неотправленных символов из буфера команд

Получив команды, сгенерированные в вышеприведенном блоке кода, MoDyz построит на дисплее смартфона следующую картинку:

Теперь можно бегущую строку делать.


Для таких дисплеев вообще подходит.

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

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

Для создания нового семисегментного индикатора служит команда “#A70id,x0,y0,h,n,ndec,val;”, первый параметр которой - id - определяет идентификатор индикатора, по которому в дальнейшем можно будет изменять выводимое соответствующим индикатором значение. x0,y0 определяют место на экране, где расположится новый элемент. Параметр h - это его высота, ширина элемента рассчитывается автоматически, исходя из высоты h и количества знакомест n в индикаторе. Параметр ndec определяет позицию десятичной точки, а именно, число позиций, отводящихся под дробную часть. Наконец, параметр val задает значение, которое будет высвечиваться на индикаторе до тех пор, пока оно не будет изменено очередной командой A7. В библиотеке mdwriter за генерацию этой команды отвечает функция

String define7SegmentDisplay(int id, int x0, int y0, int h, int n, int ndec, int val);

После создания индикатора выводимое на нем значение можно изменять, посылая команду “#A7Vid,val;”. Параметры id и val имеют то же назначение, что и в первой команде, то есть задают идентификатор индикатора и выводимое на нем значение. В отношении val следует еще заметить следующее - в параметре передается целочисленное значение, последние ndec цифр которого задают дробную часть, а остальные, соответственно, целую. То есть, если при создании элемента под дробную часть отведены две позиции, то после передачи числа 2974 на индикаторе высветится “29.74”. Соответствующая функция в библиотеке mdwriter:

String setValue7SegmentDisplay(int id, int val);

А теперь пара примеров программирования семисегментного индикатора:

  int x0,x1,y0,y1,h;
  x0 = 100; y0 = 30; h = 70;
  sResponse = "";
  addCmdString("#DF1;"); // заполнение дисплея черным цветом
  addCmdString(mdScout.setTextColors(C_LTGRAY,C_BLACK));
  addCmdString(mdScout.setTextSize(h));
  // вывод текстов, обрамляющих индикаторы
  y1 = y0+h;     addCmdString(mdScout.outTextR(x0-20, y1, "X"));
  y1 = y1+h*1.2; addCmdString(mdScout.outTextR(x0-20, y1, "Y"));
  y1 = y1+h*1.2; addCmdString(mdScout.outTextR(x0-20, y1, "V"));
  addCmdString(mdScout.setTextSize(h*0.4));
  x1 = x0+h*6*0.714; // рассчитана правая граница индикатора
  y1 = y0+h;     addCmdString(mdScout.outTextL(x1, y1, "mm"));
  y1 = y1+h*1.2; addCmdString(mdScout.outTextL(x1, y1, "mm"));
  y1 = y1+h*1.2; addCmdString(mdScout.outTextL(x1, y1, "mm/s"));
  // вывод индикаторов
  addCmdString(mdScout.setPenColors(C_RED,C_BLACK));
  addCmdString(mdScout.define7SegmentDisplay(1, x0, y0, h, 6, 2, 46821L));
  addCmdString(mdScout.setPenColors(C_YELLOW,C_BLACK));
  y0 += h*1.2;
  addCmdString(mdScout.define7SegmentDisplay(2, x0, y0, h, 6, 2, 213458L));
  addCmdString(mdScout.setPenColors(C_GREEN,C_DKGRAY));
  y0 += h*1.2;
  addCmdString(mdScout.define7SegmentDisplay(3, x0, y0, h, 6, 0, 287));
  sendString(sResponse);

В приведенном отрывке в строках 17…24 создаются три разноцветных (красный, желтый , зеленый) индикатора высотой в 70 пикселов (за это отвечает переменная h) и размером в 6 позиций. В двух первых индикаторах под дробную часть отводится по две позиции, в третьем выводится целое число.

В строках 8…15 вокруг выводятся пояснительные надписи. В левой части эти надписи выравниваются по правому краю (функция outTextR), в правой - по левому (функция outTextL).

Результат работы - на рисунке:

2 лайка

В арсенале MoDyz имеется две команды для создания кнопок - AT и AU, в библиотеке mdwriter им соответствует переопределенная функция defineTouchButton.


Первый вариант этой функции, реализующий генерацию команды AT, - defineTouchButton(int x0, int y0, int x1, int y1, int codePush, int codeRelease, String Title). В этом варианте передаются координаты левого верхнего (x0, y0) и правого нижнего (x1, y1) углов кнопки, коды, передаваемые кнопкой при нажатии на нее (codePush) и при отпускании (codeRelease), а также выводимая на кнопке надпись (Title).

Во втором варианте (соответствующем команде AU) - defineTouchButton(int x0, int y0, int codePush, int codeRelease, String Title) - набор параметров тот же, за исключением отсутствующих координат правого нижнего угла. Получив эту команду, приложение MoDyz самостоятельно рассчитывает размер кнопки, исходя из текущего размера шрифта (определяется последней полученной командой ZS) и переданного текста надписи.

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

Есть еще несколько параметров, воздействующих на вид создаваемой кнопки. Цвет и размер шрифта надписи можно изменить с помощью команд PZ (функция setTextColors) и ZS (функция setTextSize) соответственно. Команда PG (функция setPenColors) определяет цвета, которые будут использоваться при выводе обрамляющей кнопку рамки, ее толщина будет зависеть от команды GZ (setPenWidth) , а команда FD (функция setBrushColors) задаст цвета самой кнопки. На форму кнопки также влияет команда CS - с ее помощью можно скруглять углы.

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

int y0,h;
  y0 = 20;
  sResponse = "";
  addCmdString("#DF1;"); // заполнение дисплея черным цветом
// первый ряд кнопок
  h = 30;
  addCmdString(mdScout.setTextSize(h));
  addCmdString(mdScout.setPenWidth(1));
  addCmdString(mdScout.setPenColors(C_YELLOW,C_BLACK));
  addCmdString(mdScout.setBrushColors(C_DKGRAY,C_BLUE));
  addCmdString(mdScout.setTextColors(C_CYAN,C_WHITE));
  addCmdString(mdScout.defineTouchButton( 10, y0, 250, y0+h*2, 101, 111, "Кнопка 1"));
  addCmdString("#CS10;"); // определение радиуса закругления
  addCmdString(mdScout.defineTouchButton( 280, y0, 530, y0+h*2, 102, 112, "Кнопка 2"));
// второй ряд кнопок
  y0 = y0 + 100;
  h = 40;
  addCmdString(mdScout.setTextSize(h));
  addCmdString("#CS0;"); // сброс радиуса закругления
  addCmdString(mdScout.setPenWidth(5));
  addCmdString(mdScout.setPenColors(C_GREEN,C_YELLOW));
  addCmdString(mdScout.setBrushColors(C_RED,C_BLUE));
  addCmdString(mdScout.setTextColors(C_YELLOW,C_BLACK));
  addCmdString(mdScout.defineTouchButton( 10, y0, 250, y0+h*2, 103, 113, "Кнопка 3"));
  addCmdString("#CS20;"); // определение радиуса закругления
  addCmdString(mdScout.defineTouchButton( 280, y0, 530, y0+h*2, 104, 114, "Кнопка 4"));
// третий ряд кнопок
  y0 = y0 + 100;
  h = 50;
  addCmdString(mdScout.setTextSize(h));
  addCmdString(mdScout.setPenWidth(10));
  addCmdString("#CS0;"); // сброс радиуса закругления
  addCmdString(mdScout.setPenColors(C_RED,C_YELLOW));
  addCmdString(mdScout.setBrushColors(C_BLUE,C_DKGRAY));
  addCmdString(mdScout.setTextColors(C_GREEN,C_BLACK));
  addCmdString(mdScout.loadFont("AeroMaticsStencilBold.ttf"));
  addCmdString(mdScout.setFont("AeroMaticsStencilBold"));
  addCmdString(mdScout.defineTouchButton( 10, y0, 250, y0+h*2, 105, 115, "Кнопка 5"));
  addCmdString("#CS30;"); // определение радиуса закругления
  addCmdString(mdScout.defineTouchButton( 280, y0, 530, y0+h*2, 106, 116, "Кнопка 6"));
 // как обычно в конце сценария - очистим буфер команд
  sendString(sResponse);

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

В нижнем ряду (кнопки 5 и 6 - задаются в строках сценария 38…40) использован нестандартный шрифт “Aero Matics Stencil Bold”. Все, что нужно для вывода надписей на кнопке с использованием любого TrueType-шрифта - это скопировать его в каталог Fonts приложения, а затем вовремя (перед подачей команды на создание соответствующей кнопки) загрузить и активировать его (в данном сценарии за это отвечают строки 36 и 37).

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

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

Как и положено органам управления, при изменении тех или иных параметров кнопки отправляют сообщения об этом. Каждое сообщение представляет собой набор символов, завершающийся символом перевода строки (код 10 или 0x0A). Микроконтроллеру остается прослушивать входной поток, выделять содержащиеся в нем сообщения и реагировать на них в соответствии с запрограммированной логикой. За прослушивание отвечает функция readSerialData, которая принимает все поступающие символы и накапливает их в буфере cmdBuffer. Как только поступит символ 0x0A (ну и заодно 0x0D), сигнализирующие о конце очередного сообщения, содержимое буфера отдается на интерпретацию.

Вызываемая из главного цикла функция readSerialData выглядит следующим образом:

void readSerialData() {
  char c;
  if (mySerial.available()) {
    c = (char)mySerial.read();
    if (c==0x0A || c==0x0D) interpreteCommand();
    else cmdBuffer=cmdBuffer+c;
  }
}

Соответственно, интерпретатор поступивших команд:

void interpreteCommand() {
  if(cmdBuffer.length()==0) return;
  if(cmdBuffer.equalsIgnoreCase("drw"))
    drawTestElements();
  else if(cmdBuffer.equalsIgnoreCase("101")) {
    Serial.println("Button 1 pressed");
  }
  else if(cmdBuffer.equalsIgnoreCase("111")) {
    Serial.println("Button 1 released");
  }
// и так далее ... Обработка сообщений от других элементов управления
  else { Serial.print("Unknown code "); Serial.println(cmdBuffer); }
  cmdBuffer = "";
}

Вместо строк вывода информационных сообщений (6, 9) можно поставить вызовы функций, выполняющих более осмысленные действия, соответствующие логике выполнения скетча, как это сделано в блоке реакции на сообщение “drw” - именно здесь вызывается функция первоначальной прорисовки экрана.

Если верить англичанам, то “одна картинка стоит тысячи слов”. Для кнопок, надписи на которых редко когда превышают одно слово, это весьма актуально. Кроме того, используя иконки, можно неплохо сэкономить на месте, которое всегда в дефиците на небольших экранах. А тут и база для опытов в этом направлении появилась - на прошлой неделе я расширил палитру MoDyz командами работы с растрами, спрайтами и изображениями. Заодно решил и уже имеющиеся команды научить работе с этими объектами.

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

Для этого необходимо выполнить два шага. На первом шаге из каталога ресурсов “Sprites” загружается тот спрайт, который планируется разместить на кнопке. В команде загрузки спрайта (“#US…;”/функция loadSprite) в качестве идентификатора задается код, который будет указан в качестве кода нажатия на кнопку-контейнер. Второй шаг - создание кнопки (команда "at…;"/функция defineTouchButton). Больше ничего делать не надо. При прорисовке кнопки приложение просматривает список загруженных спрайтов и если найдет в нем элемент с идентификатором, равном коду нажатия кнопки, то выведет соответствующий спрайт слева от надписи на кнопке. Если поиск по коду нажатия не дал результата, то на кнопке выводится только надписи.
Пример программирования кнопки с надписью:

addCmdString(mdScout.loadSprite("phone.png", 1));
addCmdString(mdScout.loadSprite("auto11.png", 103));
addCmdString(mdScout.defineTouchButton( 10, 10, 250, 70, 103, 113, "Картинки"));
addCmdString(mdScout.defineTouchButton(260, 10, 330, 70, 1, 11, ""));

В результате выполнения первого оператора будет загружен спрайт “auto11.png” и сохранен в списке загруженных спрайтов под номером 103. Теперь к нему можно будет обращаться по этому номеру и совершать необходимые действия, например, задавать в качестве растра для заполнения геометрических фигур или просто вывести в любой позиции экрана.

Второй оператор создаст на экране кнопку с координатами (10-10)-(250-70), назначив ей коды нажатия (103) и отпускания (113) , а также выводимую надпись. Поскольку этой кнопке назначен код нажатия (103), совпадающий с номером, то на экране появится кнопка, снабженная иконкой.

А что будет, если задать кнопку без надписи, точнее, с пустой надписью (последняя строка примера)? А ничего страшного - на экране появится кнопка, на которой вместо надписи будет нарисовано изображение из файла “phone.png”, которому при загрузке был присвоен идентификатор 1.

Фрагмент экрана с результатами выполнения вышеприведенных операторов:
modyz_2buttons

При чем тут англичане?
По-русски это называется “Лучше один раз увидеть, чем 1000 раз услышать”.

100 раз