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

Знакомство с Arduino, часть 2. Морзе-клавиатура: альфа-версия

Время на прочтение 10 мин
Количество просмотров 23K
Даже на базе простейшего — лампочка с кнопкой — скетча можно собрать вполне самостоятельное устройство. Думаете, речь пойдет о настольной лампе? Нет. Попробуем собрать простейший вариант аккордной клавиатуры.



Аккордная клавиатура — это устройство, позволяющее вводить текст, но в отличие от обычной полноразмерной qwerty-клавиатуры аккордная имеет небольшое число кнопок (обычно от 3-5 и до дюжины). При этом символ вводится одновременным нажатием не одной, а нескольких кнопок — аккордом. Также символ может вводиться не одним, а несколькими последовательными нажатиями. На клавиатуре компьютера мы тоже время от времени используем аккорды — например, вводя заглавную букву одновременным нажатием соответствующей клавиши и shift'а. Или используя разнообразные заклинания вроде Shift+Ctrl+S или Ctrl+Alt+Del. Какую же аккордную клавиатуру можно собрать, используя только одну кнопку? Вспомним старую добрую проверенную временем азбуку Морзе.

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

Начнем с разработки альфа-версии устройства. Для альфы нам потребуются:
— Arduino-совместимая плата
— кнопка
— светодиод
— резистор 330 Ом
— резистор 1 кОм
— резистор 10 кОм
— макетная плата
— 7 проводков для макетной платы
— кабель USB/miniUSB

Поведение устройства.

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

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

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

Схема скетча:


Соберем схему, показанную на рисунке выше. Черные проводки на схеме идут к «земле», красные — к +5В. Не забудем, что подключать диод нужно так, чтобы к «земле» был подключен отрицательный, более короткий вывод диода.

Схему скетча можно логически разбить на 2 цепи — диодную и кнопочную.

Светодиод имеет весьма небольшое внутреннее сопротивление, и если мы подключим его напрямую, то возникший в цепи большой ток может сжечь порт микроконтроллера. Поэтому, подключив небольшое 330-омное сопротивление, мы ограничим ток. Величину тока легко оценить, воспользовавштись законом Ома, гласящему, что ток равен отношению напряжения к сопротивлению: I = V/R. В нашем случае примем сопротивление светодиода за 0 и получим по формуле ток, равный 5 Вольт / 330 Ом = 15.1515… миллиампер. Порты микроконтроллера выдерживают ток до 40 мА (сколько выдержат ваши диод и микроконтроллер — смотрите по спецификациям), так что 15-16 мА — вполне в пределах допустимого.

Кнопочная цепь несколько сложнее. Что касается резистора R2 — тут все понятно. Мы подключаем его чтобы ограничить текущий через нажатую кнопку ток. Зачем нужен резистор R3? Попробуем разобраться в «физике» работы цифрового порта. Если мы подключим кнопку без этого резистора, подключающего «землю» к порту7, то при ненажатой кнопке к 7-му порту будет подключен только синий проводок, по которому не идет никакого тока. Казалось бы, это значит, что к порту ничего не подключено. Вовсе нет! Этот проводок, играя роль антенны, начнет собирать наводки от электросети, компьютера и т.п. В результате на вход порта посыплется цифровой мусор, хаотическая последовательность логических нулей и единиц — как если бы мы постоянно случайным образом нажмали/отпускали кнопку. Сопротивление R3 подводит к порту «землю» для того, чтобы при ненажатой кнопке на порт поступал совершенно конкретный потенциал — потенциал «земли», логический «ноль». Когда мы замкнем кнопку, на порт пойдет +5В — логическая «единица».

Исходный текст скетча для морзе-клавиатуры:

#define BUTTON  7
#define LED  11

// Максимальная длительность точки, мс. Все, что нажато 1/4 секунды или быстрее - точка.
//  Остальное - тире.
#define MAX_DOT_PRESS_TIME  250
// Для переключения раскладки удерживаем кнопку 2 секунды. 
#define SWITCH_LAYOUT_TIME  2000
// Максимальная пауза между точками и тире в букве - 0.3 секунды.
// Если пауза больше - считаем ввод буквы завершенным и переходим к вводу следующей буквы.
#define DELIMITER_TIME  300
// Если кнопка не нажималась более минуты - переходим в спящий режим.
#define SLEEP_TIME  60000
// Выйти из спящего режима можно, удерживая нажатой кнопку в течение 2 секунд.
#define WAKEUP_TIME  2000

// Режимы работы
#define TYPING_MODE  0
#define SLEEP_MODE  1
int mode;

// Текущее и предыдущее состояния кнопки.
int btnState;
int btnPrevState;

// Время нажатия и отпускания кнопки.
unsigned long int timePress;
unsigned long int timeRelease;

// Этими символами мы будем обозначать точки и тире.
#define MORSE_DOT  '*'
#define MORSE_TIRE  '-'

// Максимальная длина символа азбуки Морзе (в точках и тире)
#define MAX_MORSE_SYMBOL_LENGTH  8
// Буфер для записи морзе-символа.
byte morseSymbol[MAX_MORSE_SYMBOL_LENGTH];
unsigned int morseSymbolLen;

// Таблица кодов Морзе. N-ный элемент кода соответствует n-ному символу раскладки.
char* code[] = {
  "*-","-***","*--","--*","-**","*","***-","--**","**","*---",
  "-*-","*-**","--","-*","---","*--*","*-*","***","-","**-",
  "**-*","****","-*-*","---*","----","--*-","-*--","-**-","**-**","**--",
  "*-*-",
  "*----","**---","***--","****-","*****","-****","--***","---**","----*","-----",
  "......","*-*-*-","---***","-*-*-","-*--*-","*----*","*-**-*","-****-","-**-*","**--**","--**--",
  "-***-","********","*--*-*","**-*-",
  ""
};

// Кириллическая раскладка.
char* layoutCyrillic[] = {
  "а","б","в","г","д","е","ж","з","и","й",
  "к","л","м","н","о","п","р","с","т","у",
  "ф","х","ц","ч","ш","щ","ы","ь","э","ю",
  "я",
  "1","2","3","4","5","6","7","8","9","0",
  ".",",",":",";","(","\'","\"","-","/","?","!",
  " *DELIMITER* "," *ERR* ","@"," *END* ",
  ""
  };
// Латинская раскладка.
char* layoutLatin[] = {
  "a","b","w","g","d","e","v","z","i","j",
  "k","l","m","n","o","p","r","s","t","u",
  "f","h","c","ö","ch","q","y","x","é","ü",
  "ä",
  "1","2","3","4","5","6","7","8","9","0",
  ".",",",":",";","(","\'","\"","-","/","?","!",
  " *DELIMITER* "," *ERR* ","@"," *END* ",
  ""
};

char** currentLayout;
char** newLayout;

// Яркость моргания в спящем режиме.
#define MAX_SLEEP_BRIGHTNESS  16
int sleepBrightness;
// Яркость при выходе из спящего режима.
#define WAKEUP_BRIGHTNESS  255

void setup() {
  Serial.begin(9600);
  pinMode(LED, OUTPUT);
  pinMode(BUTTON, INPUT);
  mode = TYPING_MODE;
  morseSymbolLen = 0;
  btnPrevState = LOW;
  timePress = millis();
  timeRelease = millis();
  currentLayout = layoutLatin;
  newLayout = 0;
}

// Отправим на компьютер введенный символ.
void sendMorseSymbol() {
  int i, j;
  if (morseSymbolLen < 1) {
    return;
  }
  for (i = 0; code[i][0] != '\0'; i++) {
    // Сравним введенный символ с символами из таблицы кодов Морзе.
    for (j = 0; (j < morseSymbolLen) && (code[i][j] != '\0'); j++) {
      if (code[i][j] != morseSymbol[j]) {
        j = -1;
        break;
      }
    }
    if ((j != -1) && (j == morseSymbolLen) && (code[i][j]=='\0')) {
      // Символ из таблицы кодов Морзе соответствует введенному символу.
      //  Отправим символ на компьютер.
      Serial.print(currentLayout[i]);
      morseSymbolLen = 0;
      return;
    }
  }
  // Символ в таблице не найден. Напечатаем нераспознанный символ.
  Serial.print(" [");
  for (i = 0; i < morseSymbolLen; i++) {
    Serial.print(morseSymbol[i]);
  }
  Serial.print("] ");
  morseSymbolLen = 0;
}

void loop() {
  switch(mode) {
    case TYPING_MODE:
      typingLoop();
      break;
    case SLEEP_MODE:
      sleepLoop();
      break;
  }
}

void typingLoop() {
  int ledLevel;
  // Если включена кириллическая раскладка, при нажатии диод будет гореть.
  // Если включена латинская раскладка, при нажатии диод будет гаснуть.
  if (newLayout == 0) {
    ledLevel = (currentLayout == layoutCyrillic) ? 255 : 0;
  } else {
    ledLevel = (newLayout == layoutCyrillic) ? 255 : 0;
  }
  
  btnState = digitalRead(BUTTON);
  if (btnState == HIGH) {
    analogWrite(LED, ledLevel);
    if (btnPrevState == LOW) {
      // Пользователь нажал кнопку.
      timePress = millis();
    } else {
      if ((newLayout == 0) && ((millis() - timePress) > SWITCH_LAYOUT_TIME)) {
        // Если кнопка долго удерживалась в нажатом состоянии, сменим раскладку.
        if (currentLayout == layoutLatin) {
          newLayout = layoutCyrillic;
Serial.println("\nLayout: cyrillic\n");
        } else {
          newLayout = layoutLatin;
Serial.println("\nLayout: latin\n");
        }
      }
    }
  } else {
    analogWrite(LED, 255-ledLevel);
    if (btnPrevState == HIGH) {
      // Пользователь отпустил кнопку.
      timeRelease = millis();
      // Если мы только что поменяли раскладку - сменим ее.
      if (newLayout != 0) {
        sendMorseSymbol();
        currentLayout = newLayout;
        newLayout = 0;
      } else {
        if ((timeRelease - timePress) > MAX_DOT_PRESS_TIME) {
          morseSymbol[morseSymbolLen++] = MORSE_TIRE;
        } else {
          morseSymbol[morseSymbolLen++] = MORSE_DOT;
        }
        if (morseSymbolLen > MAX_MORSE_SYMBOL_LENGTH) {
          // Мы ввели максимальное число морзе-сигналов, которое может быть
          //  в морзе-символе. Анализируем символ и отправляем на компьютер.
          sendMorseSymbol();
        }
      }
    } else {
      unsigned long int timePause = millis() - timeRelease;
      if (timePause > SLEEP_TIME) {
        sleepBrightness = MAX_SLEEP_BRIGHTNESS;
        mode = SLEEP_MODE;
Serial.println("\nSLEEP_MODE\n");
      } else if ((timePause > DELIMITER_TIME) && (morseSymbolLen > 0)) {
        // Если пауза была слишком долгой, закончим ввод буквы.
        sendMorseSymbol();
      }
    }
  }
  btnPrevState = btnState;
  delay(10);
}

void sleepLoop() {
  // Мы же  спим - поэтому будем проверять статус кнопки редко - раз в 0.3 с.
  delay(300);

  // Определим, достаточно ли долго была нажата кнопка для выхода из спячки.
  boolean flagWakeUp = ((millis() - timePress) >= WAKEUP_TIME);

  // Помигаем светодиодом.
  analogWrite(LED, sleepBrightness);
  sleepBrightness = MAX_SLEEP_BRIGHTNESS - sleepBrightness;

  btnState = digitalRead(BUTTON);
  if (btnState == LOW) {
    if (btnPrevState == HIGH) {
      timeRelease = millis();
      if (flagWakeUp) {
        // Просыпаемся.
        mode = TYPING_MODE;
Serial.println("\nTYPING_MODE\n");
      }
    }
  } else {
    if (btnPrevState == LOW) {
      timePress = millis();
    } else {
      if (flagWakeUp) {
	// Просигнализируем пользователю, что кнопка удерживалась достаточно
	//  долго, чтобы устройство проснулось.
        analogWrite(LED, WAKEUP_BRIGHTNESS);
      }
    }
  }
  btnPrevState = btnState;
}


Скопируем текст скетча в Arduino IDE. Кнопкой Verify откомпилируем скетч.

Если компиляция прошла без проблем, загрузим код в микроконтроллер — подключим собранный макет устройства к компьютеру miniUSB-кабелем и нажмем кнопку Upload . Если IDE заявит, что не может соединиться с устройством, укажем, на каком USB-порту находится Arduino, перейдя в меню Tools -> Serial Port и выбрав в списке наше устройство.

Программа начнет работу сразу же после загрузки скетча на Arduino. Кнопкой запустим монитор COM-порта. Попробуем напечатать слово «привет». На азбуке Морзе оно будет выглядеть следующим образом:
*--*   *-*   **   *--   *   -
При этом непрерывно горящая лампочка (показывающая этим, что мы в латинской раскладке) будет гаснуть на время нажатий кнопки. Если мы подстроились под ритм программы и правильно «отстучали» все точки и тире (не забывая о паузах между буквами), на экране монитора COM-порта появится латинское «priwet»:


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

Снова отстучим «привет». В окошке COM-монитора вместо ожидаемых русских букв вылезут кракозябры. Естественно — скетч отправляет кириллицу в Unicode.

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

Альфа-версия готова. Она делает именно то, что мы планировали. Но до релизного варианта устройству пока далеко. Стоило бы проработать следующие моменты:

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

— Фиксированная граница между длительностью точки и длительностью тире — неудобна. Такой подход привязывает пользователя к ритму, на который настроена наша морзе-клавиатура. Можно, конечно, поработать над логикой, заставив программу подстраиваться под скорость работы пользователя. Это было бы неплохое упражнение для начинающего программиста. Но с точки зрения коммерческого проекта — лишняя трата времени. Куда проще и удобнее заменить ввод одной кнопкой, к примеру, на ввод двумя кнопками: одна для точки, вторая для тире.

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

Поработать над перечисленным мы попробуем в следующей статье.

Предыдущие статьи:
Arduino: первое знакомство
Теги:
Хабы:
+63
Комментарии 42
Комментарии Комментарии 42

Публикации

Истории

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

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн