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

Точно!
Значит, у русских слух в 10 раз совершеннее, чем у британцев.

1 лайк

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

Последние несколько вечеров ушли на наведение маломальского порядка с использованием изображений в MoDyz. Исторически сложилось так, что приложение поддерживает работу с тремя вариантами изображений - с растрами, спрайтами и картинками. Самый простой вариант - растры, представляющие собой монохромные изображения размером 16х16 пикселей. Они определены в самом приложении и используются при заливке замкнутых областей (прямоугольников, окружностей и т.п.). Спрайты (для них имеется ограничение по размеру 48х48 пикселей) также используются для заливки замкнутых областей, кроме того, их можно размещать на кнопках, оживляя таким образом формируемое на дисплее изображение, ну и просто выводить в любом месте экрана. Наконец, самые крупные объекты в этом ряду - картинки. Их размер не ограничен ничем, кроме здравого смысла.
Далее - о том, как этими типами можно управлять с микроконтроллера, заливая области экрана.

1. Растры
В приложении имеется набор предопределенных растров, которые можно использовать для двухцветной заливки различных областей экрана. На следующей картинке представлены все 25 имеющихся в распоряжении программиста растров. Нумерация - слева направо и сверху вниз, то есть в первой строке располагаются растры с номерами с 1 по 5, во второй - с номерами с 6 по 10 и так далее.

Для того, чтобы включить заполнение области растром, необходимо отправить на смартфон команду “#FSR<rasterNum>,<factor>,<color>;” (соответствующей ей функции в mdwriter пока нет). Здесь rasterNum - это номер растра от 1 до 25 (если в качестве параметра передать 0, то растровая заливка выключается), factor - коэффициент масштабирования в процентах (все, что меньше 100 - уменьшение, все, что больше - увеличение), color - номер цвета в массиве стандартных цветов, который следует применить при отрисовке растра.

2. Спрайты
Если возможностей, предоставляемых вышеописанными растрами, недостаточно для творческого самовыражения, то можно воспользоваться другим типом изображений, поддерживаемым заливку областей - спрайтами. Спрайты не предопределены в исходных кодах приложения, а загружаются из файлов ресурсов. Их следует размещать в папке “Sprites” каталога, выделенного системой Android для MoDyz (дорожка к нему - “/Android/data/com.stepwood.modyz/files/”). В отличие от растров, спрайты являются многоцветными изображениями, их максимальный размер - 48х48 пикселей.
Для того, чтобы включить заполнение области растром, необходимо отправить выполнить три шага. Перво-наперво - загрузить нужный файл с изображением спрайта (“png”, “jpg”, “ico”) в папку ресурсов “Sprites”. Второй шаг - загрузка файла в оперативную память смартфона с помощью функции loadSprite(<fileName>,<spriteNum>), где fileName - имя загружаемого файла, а spriteNum - присваиваемый загруженному спрайту идентификатор. Наконец, активация одного из загруженных (или единственного загруженного) спрайта производится командой “#FSS<spriteNum>;” (как и команда FSR, она пока не имеет аналога в виде функции).
Эта команда имеет приоритет над командой FSR, то есть, если отправлены обе команды, то будет производиться заливка областей спрайтом, а не растром. До тех пор, пока заливка спрайтом не будет выключена (командой “#FSS0;”)

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


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

  1. разместить картинку в папке ресурсов “Images”
  2. загрузить картинку в оперативную память функцией loadImage(<fileName>,<imageNum>)
  3. активировать одну из загруженных картинок в качестве заполнителя области (команда “#FSI<imageNum>;”)

Эта команда имеет приоритет над командами FSR, FSS то есть, если отправлена не только она, но и какая-нибудь из этих команд, то будет производиться заливка областей картинкой, а не спрайтом или растром. До тех пор, пока заливка картинкой не будет выключена (командой “#FSI0;”)

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

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

Растры
Для работы с ними имеется единственная команда:
#FSR (ей соответствует функция selectRaster(int rasterNum, int factor, int color))
выбор активного растра. Параметр rasterNum может принимать значения от 0 до 25, при этом передача значения 0 приводит к отключению использования растровой заливки в последующих командах заполнения областей, а любое значение от 1 до 25 включает заполнение предопределенным растровым узором с этим номером. Параметр factor определяет степень увеличения/уменьшения выводимого растрового узора в процентах от оригинала, то есть значения меньше 100 уменьшают его, а больше 100 - увеличивают… Параметр color задает цвет (точнее, номер цвета в массиве стандартных цветов), используемый при выводе растра.

Спрайты
Здесь команд побольше - целых три:
#US (функция loadSprite(String fileName, int spriteNum))
загрузка спрайта с присвоением идентификатора. Если идентификатор совпадает с кодом нажатия кнопки, то спрайт связывается с этой кнопкой и выводится в ее левой части.
#UU (функция drawSprite(int spriteNum, int x, int y, int factor))
после загрузки спрайтов с присвоением им номеров (предыдущая команда) любой из них можно вывести на экран, вызвав эту функцию и передав ей в качестве параметров номер спрайта (spriteNum), координаты левого верхнего угла (x, y) и коэффициент масштабирования (factor).
#FSS (функция selectSprite(int spriteNum))
любой загруженный спрайт можно не только вывести на экран, но и выбрать его в качестве образца для заливки областей. Функция принимает в качестве единственного параметра номер загруженного спрайта. Номер активного спрайта имеет приоритет над номером активного растра - если оба номера больше нуля, то заливка будет производиться с использованием активного спрайта. Нулевое значение параметра spriteNum выключает заливку областей с использованием спрайтов.

Картинки
За работу с картинками отвечают четыре команды:
#UI (функция showImage(String fileName, int x, int y))
команда UI загружает изображение из файла с именем fileName м, выводит его на экран в указанном месте. Никаких дополнительных манипуляций с загруженным изображением не производится, в памяти оно не сохраняется.
#UIL (функция loadImage(String fileName, int imageNum))
получив эту команду, MoDyz загружает в память изображение из указанного файла fileName и присваивает ему идентификатор imageNum, по которому в дальнейшем можно производить с оператором доступные действия. Пока что их два - вывод на экран и назначение в качестве образца для заливки областей.
#UID (функция drawImage(int imageNum, int x, int y, int factor))
любое из загруженных ранее командой вывод указанной картинки на экран UIL изображений можно произвольное число раз выводить на экран в заданном месте (координаты x,y) и с заданным коэффициентом уменьшения/увеличения (параметр factor задает масштабирование картинки в процентах от ее оригинального размера).
#FSI (функция selectImage(int imageNum)) - сохраненное в памяти изображение можно назначить активной картинкой и все последующие команды рисования областей будут использовать эту картинку для заливки. При выводе картинка автоматически масштабируется так, чтобы заполнить максимально возможную часть области, не выступая при этом за заданные границы. Активная картинка имеет приоритет над спрайтами и растрами, Выключить применение картинки для заполнения областей можно, передав в качестве параметра imageNum нулевое значение.

Новая версия библиотеки MD_Scout содержит реализацию всех этих восьми функций.

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

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

Но к делу.
В настоящее время приложение MoDyz понимает две команды, связанные с использованием полей ввода - команду создания нового поля ввода и команду присвоения/изменения значения, связанного с созданным ранее полем ввода.

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

#A02<id>,<flags>,<x0>,<y0>,<width>,<height>;

В библиотеке mdwriter ей соответствует функция defineEditText(int id, int flags, int x0, int y0, int width, int height);

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

С помощью второго параметра можно указать, во-первых, клавиатуру какого типа следует открывать при перемещении фокуса на создаваемое поле ввода и, во-вторых, выравнивание текста в поле ввода - влево или вправо. Доступны четыре вида клавиатур:

  1. полнотекстовая клавиатура (та, что обычно открывается на смартфоне при вводе текста)
  2. цифровая клавиатура для ввода произвольных числовых значений
  3. цифровая клавиатура, дополненная клавишами с символами, используемыми при вводе телефонных номеров
  4. цифровая клавиатура, дополненная клавишами с символами, используемыми при вводе времени и/или даты

Вот так эти клавиатуры выглядят на экране смартфона с пятидюймовым дисплеем:

Под информации о виде клавиатуры отводится четыре младших бита (младшая шестнадцатеричная цифра) параметра flags. Информация о типе выравнивания хранится в следующей шестнадцатеричной цифре, точнее в ее самом младшем бите. 0 отвечает за выравнивание влево, а 1 - за выравнивание вправо. Например, передав параметр flag=1 (0x0001), мы выведем на дисплей полнотекстовую клавиатуру с выравниванием текста влево, а значение flag=17 (0x0011) будет отвечать за клавиатуру того же вида, но текст в поле ввода будет выравниваться вправо.

Параметры x0,y0 - координаты левого верхнего угла поля ввода, а width,height - ширина и высота прямоугольника отводимого под поле ввода.

Цвет символов и фона в поле ввода выбираются, исходя из значений, установленных при последнем выполнении команды FZ (функция setTextColors(…)). Размер шрифта определяет текущее значение размера шрифта, установленное при последнем выполнении команды ZS (функция setTextSize(…)).

После создания поля ввода необходимо установить для него начальное значение. Это делается с помощью команды AS2 (функция setEditTextStr(…)).

Формат этой функции - setEditTextStr(int id,String text);

Здесь параметр id указывает на идентификатор ранее созданного поля ввода, а text - содержит текст, который необходимо вывести в поле ввода.

После выполнения функций defineEditText и setEditTextStr на экране смартфона появится поле ввода, при тапе по которому открывается экранная клавиатура. Что с ней делать дальше, любой пользователь Android-устройства знает не хуже меня.

При тапе на клавишу подтверждения ввода MoDyz отправляет контроллеру последовательность символов, имеющую следующий вид:

#FE<id>,<text>;

Например, введя в поле ввода вида 3 (ввод телефонных номеров) текст “+7(987)524-12-98” (без кавычек, естественно), получим на входе микроконтроллера последовательность символов

#FE3,+7(987)524-12-98;

из которой делаем вывод, что элемент управления (поле ввода) с идентификатором 3 отправило нам номер +7(987)524-12-98.

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

Итак, скетч avr_MD_EditText дает возможность поиграться со всеми четырьмя упоминавшимися ранее типами клавиатуры, поддерживаемыми в Android-устройствах. При нажатии на верхнее поле ввода откроется основная клавиатура, используемая в большинстве Android-приложений. Три других поля ввода связаны со специализированными клавиатурами - цифровой, телефонной и клавиатурой для ввода даты/времени. Каждая из этих клавиатур имеет ограниченный набор символов (цифры плюс несколько символов, необходимых при работе с тем или иным типом информации) и кнопки большего размера, снижающие вероятность ошибочного нажатия.

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

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

Вот этот скетч:

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

PROGMEM const char sDrawEdits[] = "========== drawEdits";


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

byte bMode;
String cmdBuffer, sResponse;

void addCmdString(String sCmd);
void interpreteCommand();
void readSerialData();

void setup() {
  mySerial.begin(9600); // инициализация и настройка скорости програмного последовательного порта
  Serial.begin(9600);
  Serial.println(F("----------------------------------------------------------"));
  Serial.println(F("----------------- Sketch avr_MD_EditText -----------------"));
  Serial.println(F("----------------------------------------------------------"));
  Serial.println(FPStr(sDrawEdits));
  bMode = 1; // Первый тип полей ввода
}

void loop() {
  readSerialData();
}

String FPStr(const char *pstr_g) {
  char buf[81];
  strcpy_P(buf, pstr_g);
  return buf;
}

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');
  Serial.println(sResponse);
  sResponse = "";
} // of sendString

void drawEdits1() {
  int etHeight=70,
      etWidth=440;
  sResponse = "";

  addCmdString(mdScout.setPenWidth(0));
  // Вывод ткстовых меток над полями ввода
  addCmdString(mdScout.setTextSize(36));
  addCmdString(mdScout.setTextColors(C_YELLOW,C_BLACK));
  addCmdString(mdScout.outTextL(30,90,"Произвольный текст:"));
  addCmdString(mdScout.outTextL(30,240,"Цифровой ввод:"));
  addCmdString(mdScout.outTextL(30,390,"Ввод телефонного номера:"));
  addCmdString(mdScout.outTextL(30,540,"Ввод даты/времени:"));
 
  addCmdString(mdScout.setTextSize(36));
// ввод текста (желтого на синем фоне)
  addCmdString(mdScout.setTextColors(C_YELLOW,C_BLUE));
  addCmdString(mdScout.defineEditText(1, 1, 30, 100, etWidth, etHeight));
  addCmdString(mdScout.setEditTextStr(1, "Произвольный текст"));

// цифровая клавиатура
  addCmdString(mdScout.setTextSize(40));
  addCmdString(mdScout.setTextColors(C_BLUE,C_LTGRAY));
  addCmdString(mdScout.defineEditText(2, 2+16, 230, 250, etWidth-200, etHeight));
  addCmdString(mdScout.setEditTextStr(2, "567.234"));

// цифровая клавиатура для ввода номера телефона
  addCmdString(mdScout.setTextSize(44));
  addCmdString(mdScout.setTextColors(C_WHITE,C_RED));
  addCmdString(mdScout.defineEditText(3, 3, 30, 400, etWidth, etHeight));
  addCmdString(mdScout.setEditTextStr(3, "+7(987)524-12-98"));
 
// цифровая клавиатура для ввода времени и/или даты
  addCmdString(mdScout.setTextSize(48));
  addCmdString(mdScout.setTextColors(C_BLACK,C_LTGRAY));
  addCmdString(mdScout.defineEditText(4, 4, 30, 550, etWidth, etHeight));
  addCmdString(mdScout.setEditTextStr(4, "12:34 15/03/2024"));

  sendString(sResponse);
} // of drawEdits1

void drawEdits2() {
  int etHeight=70,
      etWidth=440;
  sResponse = "";

  // Вывод ткстовых меток над полями ввода
  addCmdString(mdScout.setTextSize(36));
  addCmdString(mdScout.setTextColors(C_YELLOW,C_BLACK));
  addCmdString(mdScout.outTextL(30,90,"Произвольный текст:"));
  addCmdString(mdScout.outTextL(30,240,"Цифровой ввод:"));
  addCmdString(mdScout.outTextL(30,390,"Ввод телефонного номера:"));
  addCmdString(mdScout.outTextL(30,540,"Ввод даты/времени:"));
 
// ввод текста (желтого на синем фоне)
  addCmdString(mdScout.setTextSize(36));
  addCmdString(mdScout.setTextColors(C_YELLOW,C_BLUE));
  addCmdString(mdScout.setPenColors(C_YELLOW,C_BLUE));
  addCmdString("#CS5;"); // определение радиуса закругления
  addCmdString(mdScout.setPenWidth(4));
  addCmdString(mdScout.defineEditText(1, 1, 30, 100, etWidth, etHeight));
  addCmdString(mdScout.setEditTextStr(1, "Произвольный текст"));

// цифровая клавиатура
  addCmdString(mdScout.setTextSize(40));
  addCmdString(mdScout.setTextColors(C_GREEN,C_BLACK));
  addCmdString(mdScout.setPenColors(C_GREEN,C_BLUE));
  addCmdString("#CS10;"); // определение радиуса закругления
  addCmdString(mdScout.setPenWidth(2));
  addCmdString(mdScout.defineEditText(2, 2+16, 230, 250, etWidth-200, etHeight));
  addCmdString(mdScout.setEditTextStr(2, "567.234"));

// цифровая клавиатура для ввода номера телефона
  addCmdString(mdScout.setTextSize(44));
  addCmdString(mdScout.setTextColors(C_WHITE,C_RED));
  addCmdString(mdScout.setPenColors(C_YELLOW,C_BLUE));
  addCmdString("#CS15;"); // определение радиуса закругления
  addCmdString(mdScout.setPenWidth(1));
  addCmdString(mdScout.defineEditText(3, 3, 30, 400, etWidth, etHeight));
  addCmdString(mdScout.setEditTextStr(3, "+7(987)524-12-98"));
 
// цифровая клавиатура для ввода времени и/или даты
  addCmdString(mdScout.setTextSize(48));
  addCmdString(mdScout.setTextColors(C_CYAN,C_BLACK));
  addCmdString(mdScout.setPenColors(C_CYAN,C_BLUE));
  addCmdString("#CS20;"); // определение радиуса закругления
  addCmdString(mdScout.setPenWidth(3));
  addCmdString(mdScout.defineEditText(4, 4, 30, 550, etWidth, etHeight));
  addCmdString(mdScout.setEditTextStr(4, "12:34 15/03/2024"));

  sendString(sResponse);
} // of drawEdits2

void drawScene() {
  String btnText;
  int buttonColor=C_BLUE, // цвет кнопки - синий
      captionColor=C_CYAN;// цвет надписи на кнопке - голубой
  addCmdString("#DF1;"); // заполнение дисплея черным цветом
  addCmdString(mdScout.setTextSize(40));
  if(bMode==1) {
    btnText = "Режим 2";
    addCmdString(mdScout.setPenColors(C_YELLOW,C_BLACK));  // цвет рамки вокруг кнопки - желтый
    addCmdString(mdScout.setPenWidth(3));
    addCmdString("#CS10;");
  }
  else {
    btnText = "Режим 1";
    addCmdString(mdScout.setPenColors(buttonColor,C_BLACK));
    addCmdString(mdScout.setPenWidth(0));
    addCmdString("#CS0;"); // обнуление радиуса закругления
  }
  addCmdString(mdScout.setBrushColors(buttonColor,C_BLACK));
  addCmdString(mdScout.setTextColors(captionColor,C_BLACK));
  addCmdString(mdScout.defineTouchButton(10, 700, 250, 760, 101, 111, btnText));
  addCmdString("#CS0;"); // обнуление радиуса закругления

  sendString(sResponse);
  if(bMode==1) drawEdits1();
  else drawEdits2();
}

void showETValue(String s) {
  int id,l,p;
  String EdittextValue,sid;
  l = s.length();
  p = s.indexOf(',');
  sid = s.substring(3,p);
  EdittextValue = s.substring(p+1,l-1);
  Serial.print("<");
  Serial.print(sid);
  Serial.print("><");
  Serial.print(EdittextValue);
  Serial.println(">");
} // of showETValue

void interpreteCommand() {
  if(cmdBuffer.length()==0) return;
  if(cmdBuffer.equalsIgnoreCase("drw")) { // поступила команда отрисовки стартовой
    Serial.print("command is ");          // сцены - выполним ее
    Serial.println(cmdBuffer);
    drawScene();
  }
  else if(cmdBuffer.equals("101")) {    // нажата кнопка переключения режимов -
    Serial.print("command is ");        // сменим режим (1->2 или 2->1) и отрисуем
    Serial.println(cmdBuffer);          // соответствующую выбранному режиму сцену
    if(bMode==1) bMode=2;
    else bMode=1;
    drawScene();
  }
  else if(cmdBuffer.startsWith("#FE"))  // поступила информация от поля ввода -
    showETValue(cmdBuffer);             // выведем ее в терминал для контроля
  else { Serial.print("code "); Serial.println(cmdBuffer); }
  cmdBuffer = "";
} // of interpreteCommand

void readSerialData() {
  char c;
  if (mySerial.available()) {
    c = (char)mySerial.read();
    if(c<' ') {Serial.print("<"); Serial.print(byte(c)); Serial.print(">");}
    else Serial.print(c);
    if (c==0x0A || c==0x0D) interpreteCommand();
    else cmdBuffer=cmdBuffer+c;
  }
} // of readSerialData

Функция readSerialData() собирает поступающие от смартфона символы в отдельные строки (признак завершения очередной строки - символ новой строки и/или перевода каретки) и отправляет их в функцию interpreteCommand() для интерпретации. Данный скетч “понимает” три команды - “drw” (стартовая отрисовка сцены), “101” (код нажатия кнопки смены режима), по которому переключается режим отображения и на экран выводится соответствующая ему сцена (функция drawScene()), и команды, начинающиеся с “#FE”, которые приносят в своем теле наборы символов, введенные в одном из четырех имеющихся полей ввода.

В зависимости от текущего режима отображения выполняется либо функция drawEdits1(), выводящая на экран четыре поля ввода в “классическом” исполнении, либо drawEdits2(), выводящая те же четыре поля ввода, но с дополнительными рюшечками (закруглениями и/или рамками).