Буквально месяц назад я натолкнулся на эту статью, где повествуется о педалировании Vim. Чуть позже, после своего длительного трёхминутного исследования, я выяснил, что что тема эта уже не новая и довольно популярная. Сам я Vim использую только в случае крайней необходимости (если уж и приходится работать в консоли, то предпочитаю Nano), но ведь можно сделать подобное и под другие приложения.
Изначально я хотел сделать небольшую статейку, однако у меня получился целый туториал по созданию данного девайса с пошаговым написанием кода и пояснением что да как. Дабы не раздувать статью, под спойлерами будет различная информация, которая показалась мне интересной и достойной внимания новичков в Arduino, продвинутые и особо торопливые же пользователи могут не тратить на то время. Полный исходный код также представлен в конце статьи.
А зачем оно мне?
Если у вас нет сомнений в необходимости и полезности этого устройства, то можете пропустить этот пункт. Для остальных сначала хотелось бы рассказать о предпосылках создания данного устройства.
Во все времена программисты и дизайнеры старались сделать удобный и дружественный интерфейс, чтобы пользователь мог без лишних заморочек работать с приложением используя мышь и клавиатуру, так зачем же нам ещё один манипулятор? Что же, заглянем немного в историю, а точнее, в начало XVIII века, когда был изобретён такой музыкальный инструмент, как фортепиано. Как известно, это слово буквально переводится как «громко-тихо», но мало кто задумывается, что такой инструмент умный итальянский мастер получил, фактически «запедалировав» существовавший тогда клавесин, что и позволило в какой-то степени управлять громкостью звука, при этом не отнимая руки от клавиш.
Примеров можно приводить много. Педали есть у автомобиля, чтобы не бросать руль, если надо добавить газ. Барабанная установка тоже имеет педали, чтобы стучать в бас-бочку и тарелки. А что могут дать педали при использовании компьютера? Ну, например, можно задать какую-нибудь горячую комбинацию клавиш, или вообще добавить клавишу, которой нет, вроде включения и выключения звука. Педали могут помочь, если заняты руки: сам я играю на гитаре, при этом иногда под аккомпанемент, я было бы очень удобно проматывать подложку, не пытаясь постоянно дотянуться до клавиатуры. Ну и, наконец, контроллеры могут давать и совершенно нечеловеческие возможности в играх: было бы круто одним кликом построить себе всю базу в стратегии или крушить врагов со скоростью десятка ударов в секунду в шутерах, не так ли?
В общем, надеюсь, я вас убедил, а значит, пора приступать непосредственно к самой разработке.
Необходимые ресурсы
- Собственно, педали. Тут сразу же возникли некоторые сложности из-за того, что я никак не мог придумать название для такой педали. Я знал лишь то, что подобные вещи используются в швейных машинках. В общем, по запросу electric pedal мне всё же удалось найти то, что нужно, на Aliexpress, и я, недолго думая, заказал 3 штуки.
- Контроллер. Педалборд должен эмулировать работу клавиатуры и, возможно, мыши для возможности подключения к ПК без лишних драйверов. Для этого отлично подойдёт плата Arduino ProMicro, которая хоть и не содержит имеет некоторых выводов, но зато сделана максимально компактно. Идём на тот же Aliexpress, и покупаем китайскую версию этого чуда.
- Провода. Чтобы поместить 3 педали под стол, нужен как минимум четырёхжильный провод длиной не меньше метра. Тут, думаю, проблем возникнуть не должно.
- RGB-светодиод и кнопка. Первый нужен для индикации режимов, а вторая — для их переключения.
- Ну и, понятное дело, нам нужны Arduino IDE, паяльник и прямые руки.
Схема устройства
Ещё до того, как мне пришли посылки, я притупил к созданию схемы устройства. Хотя это сильно сказано, так как мне надо было всего лишь подключить педали, диод и кнопку. Получилось как-то так:
Для педалей я решил выделить сразу 4 порта PB1-PB4, то есть две для левой, и две для правой ноги, хотя пока педали у меня только 3. К тому же, они все находятся в одной группе и расположены в одном месте. Под светодиод я отвёл выводы PD0, PD1 и PD4, под кнопку — PD7.
При этом нам не понадобятся никакие подтягивающие резисторы, если использовать те, что встроены в контроллер. Правда, тогда, при нажатии кнопки или педали, на входе будет низкий уровень, а при отпускании — высокий, то есть, нажатия будут инвертироваться, и об этом не стоит забывать.
Написание кода
Этот этап был самым трудным: из-за моего пары ошибок в указателях я несколько раз стёр загрузчик и в итоге чуть не завалил плату на программном уровне. Ниже подробно расписаны все этапы создания прошивки, для тех же, кто просто хочет получить работающий код, он будет в конце статьи.
Подготовка
Для начала нам нужно понять, что вообще такое педаль с точки зрения программы. Я решил сделать возможность задания педали одного из двух режимов — реального времени и триггера. Каждая педаль при этом имеет две программы: первая выполняется при удержании педали в режиме реального времени или при нечётных нажатиях в режиме триггера, вторая — при отпускании педали в режиме реального времени или при чётных нажатиях в режиме триггера. Так же у педали есть порт, состояние, и две переменные — текущие позиции в программах 1 и 2. У меня получилась вот такая структура:
struct pedal {
char port; // порт педали
char state; // состояние педали, для триггеров
char oldState; // старое состояние, для дебоунса
char pos1; // позиция 1
char pos2; // позиция 2
unsigned char type; //0 — режим реального времени, 1 — режим триггера;
unsigned char act1[16]; //программа 1
unsigned char act2[16]; //программа 2
};
Arduino имеет довольно мало памяти и к тому же 8-разрядная, так что лучше стараться использовать char нежели int там, где это возможно.
Так же нам понадобится стандартная библиотека Keyboard для работы в качестве клавиатуры.
Обработка нажатий
Сейчас нам нужно сделать интерпретатор, который будет читать данные из массива и отправлять их в виде нажатий клавиш на машину, а так же выделить несколько значений под различные внутренние команды. Открываем страницу с кодами клавиш, и смотрим что и как мы можем нажать. Я не стал глубоко копать и изучать всякие стандарты клавиатур, так как информации здесь мне показалось вполне достаточно для такого проекта. Первая половина отведена под стандартные ASCII-символы (хотя некоторые из них и непечатаемы или не используются), вторая же — под различные клавиши-модификаторы. Есть даже отдельные коды для левых и правых клавиш, что очень порадовало, а вот специальных кодов для цифр с нампада я не увидел, хотя, насколько я знаю, они немного по-особому воспринимаются в системе, нежели обычные цифры. Возможно, их коды находятся где-то в «дырах», между диапазонами, но сейчас не об этом. Итак, самый большой код имеет клавиша «вверх» — 218, а значит, диапазон 219-255 можно считать свободным, ну или по крайней мере там нет каких-то важных клавиш.
void pedalAction() {
//255 будет означать, что педаль не объявлена
if (pedal1->type == 255)
return;
//указатель на массив с программой
unsigned char *prg;
//указатель на позицию в программе
char *pos;
if (pedal1->type) {
//код для определения педали в режиме триггера
int current;
if ((current = digitalRead(ports[num])) != oldState[num]) {
if (!current)
state[num] = !state[num];
oldState[num] = current;
}
if (!state[num]) {
//act1
pos2[num] = 0;
pos = &(pos1[num]);
prg = pedal1->act1;
} else {
//act2
pos1[num] = 0;
pos = &(pos2[num]);
prg = pedal1->act2;
}
} else {
//код для определения педали в режиме реального времени
if (!digitalRead(ports[num])) {
//act1
pos2[num] = 0;
pos = &(pos1[num]);
prg = pedal1->act1;
} else {
//act2
pos1[num] = 0;
pos = &(pos2[num]);
prg = pedal1->act2;
}
}
while (1) {
if (prg[*pos] == 254) {
//Удерживать клавишу, следующую за *pos
Keyboard.press(prg[++*pos]);
} else if (prg[*pos] == 253) {
//Отпустить клавишу, следующую за *pos
Keyboard.release(prg[++*pos]);
} else if (prg[*pos] == 252) {
//"Пропуск хода", ничего не делать
++*pos;
return;
} else if (prg[*pos] == 251) {
//Переместиться в программе на позицию в ячейке *pos+1
*pos = prg[*pos + 1];
return;
} else if (prg[*pos] == 255 || prg[*pos] == 0) {
//Конец программы, просто заглушка
return;
} else {
//Отправляем нажатие клавиши
Keyboard.write(prg[*pos]);
}
//Циклически переходим на ячейку вперёд после тех команд, после которых это необходимо
if (++*pos>=16)
pos = 0;
}
}
Думаю, даже у человека с не самым высоким уровнем знания Си не возникнет вопросов о том, что тут происходит. Сначала функция выбирает нужную педаль и определяет в зависимости от режима и состояния педали, какую программу стоит выполнять. При чтении каждого элемента массива, если он не является управляющим символом, вызывается функция Keyboard.write(), которая эмулирует нажатие и отпускание клавиши. Управляющие же символы обрабатывются отдельно и нужны для зажатия комбинаций клавиш и навигации по программе.
Итак, у нас есть интерпретатор и примерное понимание того, как наш педалборд взаимодействует с компьютером. Теперь надо всё это довести до состояния полноценной прошивки и проверить работоспособность на одной педали. Если создать экземпляр педали и циклично вызывать pedalAction(), то по идее у нас будет выполняться заданная в структуре программа.
struct pedal *pedal1 = {15, 0, 0, 0, 0, 0, "Hello, world!", 0};
void prepare () {
pinMode(15, 2); //2 - INPUT_PULLUP, то есть вход с подтяжкой к питанию
Keyboard.begin();
}
void loop() {
pedalAction();
}
Кстати, никогда не забывайте про нуль-терминаторы в данных «программах», если их длина меньше размера массива и если они не цикличны, потому что Arduino будет не только пытаться интерпретировать не заданные данные, но и будет отправлять их в машину с огромной скоростью, а это всё равно, что дать клавиатуру обезьяне.
Одна педаль хорошо, а две — лучше
Теперь пришло время разобраться с обработкой сигналов с нескольких педалей, а также добавить переключение режимов. В начале статьи было выделено 4 порта под педали, каждой из которых надо позволить работать в семи режимах. Почему 7? Потому что без использования ШИМ наш светодиод может давать всего 7 цветов, и восьмой — выключенный. Такого количества вполне хватит обычному пользователю, ну а в крайнем случае его легко можно увеличить. Значит педали будем хранить двумерном в массиве 7 х 4. Чтобы не засорять память, общие для нескольких структур значения, такие, как номер порта можно вынести в отдельные массивы. В итоге мы получаем что-то такое:
struct pedal {
unsigned char type;
unsigned char act1[16];
unsigned char act2[16];
};
struct pedal pedals[7][4] = {
{
{ 255, {"Hello, world!"}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}
},
{
{ 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}
},
{
{ 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}
},
{
{ 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}
},
{
{ 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}
},
{
{ 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}
},
{
{ 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}
}
};
char ports[4] = {15, 16, 14, 8};
char pos1[4] = {0, 0, 0, 0};
char pos2[4] = {0, 0, 0, 0};
char state[4] = {0, 0, 0, 0};
char oldState[4] = {0, 0, 0, 0};
char mode = 0; //текущий режим
char curPedal = 0; //текущая обрабатываемая педаль
Для нас важно знать только тип педали и две программы, поэтому только их мы оставим непосредственно в структуре, остальными же вещами пусть занимается автоматика. Методы prepare и loop теперь будет выглядеть следующим образом:
void prepare(){
pinMode(2, 1);
pinMode(3, 1);
pinMode(4, 1);
pinMode(6, 2);
for (int i : ports)
pinMode(i, 2);
Keyboard.begin();
}
void loop() {
for (int i = 0; i < 6; i++) {
int current;
if ((current = digitalRead(modeButton)) != last) {
if (!current) {
if (++mode >= 7)
mode = 0;
while (pedals[mode][0].type == 255 && pedals[mode][1].type == 255 && pedals[mode][2].type == 255 && pedals[mode][3].type == 255)
if (++mode >= 7) {
mode = 0;
break;
}
}
last = current;
digitalWrite(2, (mode + 1) & 0b001);
digitalWrite(3, (mode + 1) & 0b010);
digitalWrite(4, (mode + 1) & 0b100);
for (int i = 0; i < 4; i++) {
pos1[i] = 0;
pos2[i] = 0;
state[i] = 0;
oldState[i] = 0;
}
delay(50);
}
curPedal = i;
pedalAction
}
}
}
Контроллер буде считать режим неиспользуемым, если в нём не объявлено ни одной педали (mode=255), а значит при попадании на него сразу перейдёт к следующему, но при этом первый режим всегда будет существовать. При переключении режима все значения в массивах зануляются, так как сохранять их для каждого режима нам не требуется (верно?), а затем цикл обходит все педали и вызывает pedalAction для них.
Также в начале метода pedalAction() нужно добавить следующую строчку, чтобы он понимал, с какой из структур надо иметь дело:
struct pedal *pedal1 = &pedals[mode][curPedal];
Уже существующую структуру pedal1 можно удалить за ненадобностью.
Всё это так же вполне работает, однако я столкнулся с одной проблемой: некоторые программы не успевают принимать нажатия с такой скоростью, с которой их отправляет Arduino. Самое очевидное решение — добавить возможность устанавливать задержки между действиями там, где это необходимо. Вот только когда мы садимся писать программы под микроконтроллеры, все фишки, вроде аппаратной многопоточности, остались где-то там, в высокоуровневых ЭВМ, у нас же при добавлении задержки останавливается вся программа, пока контроллер не отсчитает нужное количество циклов. Раз многопоточности у нас нет, то придётся её создать.
Тяжело сказать, да легко сделать
Я не стал изобретать велосипед, а взял готовую библиотеку ArduinoThread. Здесь можно немного почитать о том как она работает и скачать её. Загрузить библиотеку можно и из самой Arduino IDE. Кратко говоря, она позволяет периодически выполнять функцию с определённым интервалом, при этом не позволяя уйти в бесконечный цикл в случае, если выполнение займёт больше времени, чем интервал. То, что нужно. Создадим ещё один массив с потоками для каждой педали:
Thread pedalThreads[6] = {Thread(pedalAction, 10), Thread(pedalAction, 10), Thread(pedalAction, 10), Thread(pedalAction, 10), Thread(pedalAction, 10), Thread(pedalAction, 10)};
Теперь у нас есть 6 одинаковых виртуальных потоков, но при этом являющихся разными объектами.
Немного перепишем цикл обхода педалей для работы с новым функционалом:
...
for (int i = 0; i < 4; i++) {
if (pedalThreads[i].shouldRun()) {
curPedal = i;
pedalThreads[i].run();
}
}
...
Теперь значение 252 в массиве программы, которое соответствует «ничегонеделанию», будет давать задержку в 10 миллисекунд (хотя на самом деле чуть больше, так как выполнение кода тоже занимает время). Добавив несколько строк в интерпретатор, получится сделать возможным установку задержки в несколько таких «квантов», потратив всего 2 байта массива:
...
if (wait[num]) {
wait[num]--;
return;
} else if (prg[*pos] == 250) {
wait[num] = prg[++*pos];
}
...
В отличии от остальных команд, данную инструкцию необходимо добавить именно в начало интерпретатора, то есть сразу после «while (1) {», так как задержка должна обрабатываться до того, как интерпретатор перейдёт к чтению программы. Массив wait нужно так же объявить, как это было сделано с ports, state и т.д. и так же обнулять его ячейки при переключении режима, чтобы задержка не перешла в другую программу.
Теперь, при возможности установки задержки до 2.55 секунд проблем с определением клавиш программами возникать не должно.
Программирование «на ходу»
В принципе, тут можно было бы закончить с кодом и приступить к сборке устройства, но в этом случае, если кто-то вдруг захочет перепрограммировать педали, то ему придётся открывать Arduino IDE, править код, и заново загружать прошивку. Естественно, такой вариант не самый лучший, поэтому я решил добавить возможность менять программу с последовательного порта Arduino, а сами программы хранить в EEPROM. Для работы с энергонезависимой памятью необходимо подключить стандартную библиотеку EEPROM.h. Код режима программирования выглядит следующим образом:
...
if (!digitalRead(modeButton)) {
//Режим программирования
Serial.begin(9600);
while (!Serial) {
PORTD = 0b00000000 + (PORTD & 0b11101100);
delay(250);
PORTD = 0b00010000 + (PORTD & 0b11101100);
delay(250);
}
Serial.println(F("***Programming mode***"));
Serial.println(F("Write the command as <m> <p> <c>"));
Serial.println(F("m - number of mode, one digit"));
Serial.println(F("p - number of pedal, one digit"));
Serial.println(F("c - command, it can be:"));
Serial.println(F("tr - read pedal info"));
Serial.println(F("tw - enter to writing mode and change pedal programm"));
Serial.println(F("te - erase pedal programm and delete it"));
Serial.println(F("There are up to 7 modes and 6 pedals per mode can be configured"));
Serial.println(F("Mode will be incative if there is no pedal configured in it"));
while (1) {
while (Serial.available()) {
Serial.read();
delay(1);
}
PORTD = 0b00000001 + (PORTD & 0b11101100);
Serial.println("");
Serial.println(F("Enter command"));
while (!Serial.available());
PORTD = 0b00000010 + (PORTD & 0b11101100);
delay(3);
if (Serial.available() == 3) {
int curMode = Serial.read() - 48;
int curPedal = Serial.read() - 48;
char cmd = Serial.read();
if (curMode > 6 || curMode < 0) {
Serial.print(F("Mode must be in 0-6. You entered "));
Serial.println(curMode);
continue;
}
if (curPedal > 3 || curPedal < 0) {
Serial.print(F("Pedal must be in 0-3. You entered "));
Serial.println(curPedal);
continue;
}
Serial.println();
if (cmd == 'r') {
int beginAddress = sizeof(struct pedal) * (curMode * 6 + curPedal);
Serial.print("type: ");
int curAddress = beginAddress;
Serial.println(EEPROM[curAddress++]);
Serial.print("act1: ");
for (int i = curAddress ; i < curAddress + (sizeof(struct pedal) - 1) / 2; i++) {
Serial.print(EEPROM[i]);
Serial.print("t");
}
Serial.println();
curAddress = beginAddress + 1 + (sizeof(struct pedal) - 1) / 2;
Serial.print("act2: ");
for (int i = curAddress ; i < curAddress + (sizeof(struct pedal) - 1) / 2; i++) {
Serial.print(EEPROM[i]);
Serial.print("t");
}
Serial.println();
} else if (cmd == 'w') {
Serial.println(F("Enter type:"));
PORTD = 0b00000001 + (PORTD & 0b11101100);
while (!Serial.available());
int beginAddress = sizeof(struct pedal) * (curMode * 6 + curPedal);
int curAddress = beginAddress;
PORTD = 0b00000010 + (PORTD & 0b11101100);
EEPROM[curAddress++] = (char)Serial.parseInt();
PORTD = 0b00000001 + (PORTD & 0b11101100);
Serial.println(F("Enter act1 in DEC divided by space:"));
while (Serial.available()) {
Serial.read();
delay(1);
}
while (!Serial.available());
PORTD = 0b00000010 + (PORTD & 0b11101100);
while (Serial.available()) {
EEPROM[curAddress++] = (char)Serial.parseInt();
delay(1);
}
PORTD = 0b00000001 + (PORTD & 0b11101100);
curAddress = beginAddress + 1 + (sizeof(struct pedal) - 1) / 2;
Serial.println(F("Enter act2 in DEC divided by space:"));
while (Serial.available()) {
Serial.read();
delay(1);
}
while (!Serial.available());
PORTD = 0b00000010 + (PORTD & 0b11101100);
while (Serial.available()) {
EEPROM[curAddress++] = (char)Serial.parseInt();
delay(1);
}
PORTD = 0b00000001 + (PORTD & 0b11101100);
Serial.println(F("Finished, don't forget to verify written data!"));
} else if (cmd == 'e') {
int beginAddress = sizeof(struct pedal) * (curMode * 6 + curPedal);
Serial.println(F("Disabling pedal..."));
PORTD = 0b00000010 + (PORTD & 0b11101100);
EEPROM[beginAddress] = 255;
PORTD = 0b00000001 + (PORTD & 0b11101100);
Serial.println(F("Pedal disabled"));
}
} else {
Serial.println(F("Incorrect command, please read help above"));
}
};
}
...
Что делает этот код поясняет содержащаяся в нём справка: через пробел вводится номер режима, номер педали, и команда, которых существует 3 — чтение, запись и выполнение удаление программы. Все данные о педалях хранятся друг за другом в виде последовательности из 33-х байт, то есть тип педали, и две программы, и того мы занимаем 7*4*33=924 из 1024 байт EEPROM. Вариант использования динамического размера педалей в памяти я отбросил, так как в этом случае при перепрограммировании одной педали придётся перезаписать почти все ячейки, а циклов перезаписи эта память имеет конечное количество, поэтому рекомендуют делать это как можно реже.
PORTD = 0b00000010 + (PORTD & 0b11101100);
...
PORTD = 0b00000001 + (PORTD & 0b11101100);
Благодаря данной библиотеке, с точки зрения программиста, энергонезависимая память является обычным массивом char, но, как «ардуинщикам», нам нужно понимать, что запись в ПЗУ — очень тяжёлая операция, которая занимает у контроллера целых ~3 секунды, и желательно не прерывать этот процесс. Данная конструкция заставляет диод светить красным во время таких операций, а затем возвращает обратно «безопасный» зелёный цвет.
В режиме записи программы ввод производится непосредственно значениями байтов в десятичной системе счисления через пробел. Получается довольно сурово, но зато не приходится писать сложный парсер. Тем более, перепрограммирование происходит не так часто, и в этих случаях вполне можно заглянуть в ASCII таблицу.
С сохранением структур разобрались, теперь надо наши данные как-то оттуда вытащить и преобразовать к «педальному» виду:
...
for (int i = 0; i < 7; i++) {
for (int j = 0; j < 4; j++) {
struct pedal *p = &pedals[i][j];
int beginAddress = sizeof(struct pedal) * (i * 6 + j);
int curAddress = beginAddress;
unsigned char type = EEPROM[curAddress++];
if (type == 0 || type == 1) {
p->type = type;
for (int k = 0 ; k < 16; k++) {
p->act1[k] = EEPROM[curAddress++];
}
for (int k = 0 ; k < 16; k++) {
p->act2[k] = EEPROM[curAddress++];
}
}
}
}
...
Здесь так же не происходит ничего сверхъестественного: контроллер считывает данные из памяти и заполняет ими уже существующие структуры.
Преимущество программирования через UART заключается в том, что нам опять же не требуется никаких специальных драйверов, поэтому задавать поведение манипулятора можно даже с телефона.
Демонстрация
https://www.youtube.com/embed/pkjtRLlNZnU
Полный исходный код
#include <Keyboard.h>
#include <Thread.h>
#include <EEPROM.h>
#define modeButton 6
struct pedal {
unsigned char type; //0 — режим реального времени, 1 — режим триггера, 255 — педаль не назначена
unsigned char act1[16];
unsigned char act2[16];
};
struct pedal pedals[7][4] = {
{
{ 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}
}, {
{ 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}
}, {
{ 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}
}, {
{ 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}
}, {
{ 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}
}, {
{ 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}
}, {
{ 255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}, {255, {255}, {255}}
}
};
char ports[4] = {8, 16, 15, 14};
char pos1[4] = {0, 0, 0, 0};
char pos2[4] = {0, 0, 0, 0};
char state[4] = {0, 0, 0, 0};
char oldState[4] = {0, 0, 0, 0};
char wait[4] = {0, 0, 0, 0};
void pedalAction();
char mode = 0;
char curPedal;
Thread pedalThreads[6] = {Thread(pedalAction, 10), Thread(pedalAction, 10), Thread(pedalAction, 10), Thread(pedalAction, 10), Thread(pedalAction, 10), Thread(pedalAction, 10)};
void setup() {
pinMode(2, 1);
pinMode(3, 1);
pinMode(4, 1);
pinMode(modeButton, 2);
if (!digitalRead(modeButton)) {
//Режим программирования
Serial.begin(9600);
while (!Serial) {
PORTD = 0b00000000 + (PORTD & 0b11101100);
delay(250);
PORTD = 0b00010000 + (PORTD & 0b11101100);
delay(250);
}
Serial.println(F("***Programming mode***"));
Serial.println(F("Write the command as <m> <p> <c>"));
Serial.println(F("m - number of mode, one digit"));
Serial.println(F("p - number of pedal, one digit"));
Serial.println(F("c - command, it can be:"));
Serial.println(F("tr - read pedal info"));
Serial.println(F("tw - enter to writing mode and change pedal programm"));
Serial.println(F("te - erase pedal programm and delete it"));
Serial.println(F("There are up to 7 modes and 6 pedals per mode can be configured"));
Serial.println(F("Mode will be incative if there is no pedal configured in it"));
while (1) {
while (Serial.available()) {
Serial.read();
delay(1);
}
PORTD = 0b00000001 + (PORTD & 0b11101100);
Serial.println("");
Serial.println(F("Enter command"));
while (!Serial.available());
PORTD = 0b00000010 + (PORTD & 0b11101100);
delay(3);
if (Serial.available() == 3) {
int curMode = Serial.read() - 48;
int curPedal = Serial.read() - 48;
char cmd = Serial.read();
if (curMode > 6 || curMode < 0) {
Serial.print(F("Mode must be in 0-6. You entered "));
Serial.println(curMode);
continue;
}
if (curPedal > 3 || curPedal < 0) {
Serial.print(F("Pedal must be in 0-3. You entered "));
Serial.println(curPedal);
continue;
}
Serial.println();
if (cmd == 'r') {
int beginAddress = sizeof(struct pedal) * (curMode * 6 + curPedal);
Serial.print("type: ");
int curAddress = beginAddress;
Serial.println(EEPROM[curAddress++]);
Serial.print("act1: ");
for (int i = curAddress ; i < curAddress + (sizeof(struct pedal) - 1) / 2; i++) {
Serial.print(EEPROM[i]);
Serial.print("t");
}
Serial.println();
curAddress = beginAddress + 1 + (sizeof(struct pedal) - 1) / 2;
Serial.print("act2: ");
for (int i = curAddress ; i < curAddress + (sizeof(struct pedal) - 1) / 2; i++) {
Serial.print(EEPROM[i]);
Serial.print("t");
}
Serial.println();
} else if (cmd == 'w') {
Serial.println(F("Enter type:"));
PORTD = 0b00000001 + (PORTD & 0b11101100);
while (!Serial.available());
int beginAddress = sizeof(struct pedal) * (curMode * 6 + curPedal);
int curAddress = beginAddress;
PORTD = 0b00000010 + (PORTD & 0b11101100);
EEPROM[curAddress++] = (char)Serial.parseInt();
PORTD = 0b00000001 + (PORTD & 0b11101100);
Serial.println(F("Enter act1 in DEC divided by space:"));
while (Serial.available()) {
Serial.read();
delay(1);
}
while (!Serial.available());
PORTD = 0b00000010 + (PORTD & 0b11101100);
while (Serial.available()) {
EEPROM[curAddress++] = (char)Serial.parseInt();
delay(1);
}
PORTD = 0b00000001 + (PORTD & 0b11101100);
curAddress = beginAddress + 1 + (sizeof(struct pedal) - 1) / 2;
Serial.println(F("Enter act2 in DEC divided by space:"));
while (Serial.available()) {
Serial.read();
delay(1);
}
while (!Serial.available());
PORTD = 0b00000010 + (PORTD & 0b11101100);
while (Serial.available()) {
EEPROM[curAddress++] = (char)Serial.parseInt();
delay(1);
}
PORTD = 0b00000001 + (PORTD & 0b11101100);
Serial.println(F("Finished, don't forget to verify written data!"));
} else if (cmd == 'e') {
int beginAddress = sizeof(struct pedal) * (curMode * 6 + curPedal);
Serial.println(F("Disabling pedal..."));
PORTD = 0b00000010 + (PORTD & 0b11101100);
EEPROM[beginAddress] = 255;
PORTD = 0b00000001 + (PORTD & 0b11101100);
Serial.println(F("Pedal disabled"));
}
} else {
Serial.println(F("Incorrect command, please read help above"));
}
};
}
for (int i : ports)
pinMode(i, 2);
pinMode(17, 1);
for (int i = 0; i < 7; i++) {
for (int j = 0; j < 4; j++) {
struct pedal *p = &pedals[i][j];
int beginAddress = sizeof(struct pedal) * (i * 6 + j);
int curAddress = beginAddress;
unsigned char type = EEPROM[curAddress++];
if (type == 0 || type == 1) {
p->type = type;
for (int k = 0 ; k < 16; k++) {
p->act1[k] = EEPROM[curAddress++];
}
for (int k = 0 ; k < 16; k++) {
p->act2[k] = EEPROM[curAddress++];
}
}
}
}
Keyboard.begin();
}
int last = 0;
void loop() {
int current;
if ((current = digitalRead(modeButton)) != last) {
if (!current) {
if (++mode >= 7)
mode = 0;
while (pedals[mode][0].type == 255 && pedals[mode][1].type == 255 && pedals[mode][2].type == 255 && pedals[mode][3].type == 255)
if (++mode >= 7) {
mode = 0;
break;
}
}
last = current;
digitalWrite(2, (mode + 1) & 0b001);
digitalWrite(3, (mode + 1) & 0b010);
digitalWrite(4, (mode + 1) & 0b100);
for (int i = 0; i < 4; i++) {
pos1[i] = 0;
pos2[i] = 0;
state[i] = 0;
oldState[i] = 0;
wait[i] = 0;
}
delay(50);
}
for (int i = 0; i < 4; i++) {
if (pedalThreads[i].shouldRun()) {
curPedal = i;
pedalThreads[i].run();
}
}
}
void pedalAction() {
struct pedal *pedal1 = &pedals[mode][curPedal];
if (pedal1->type == 255)
return;
unsigned char *prg;
char *pos;
if (pedal1->type) {
int current;
if ((current = digitalRead(ports[curPedal])) != oldState[curPedal]) {
if (!current)
state[curPedal] = !state[curPedal];
oldState[curPedal] = current;
}
if (!state[curPedal]) {
//act1
pos2[curPedal] = 0;
pos = &(pos1[curPedal]);
prg = pedal1->act1;
} else {
//act2
pos1[curPedal] = 0;
pos = &(pos2[curPedal]);
prg = pedal1->act2;
}
} else {
if (!digitalRead(ports[curPedal])) {
//act1
pos2[curPedal] = 0;
pos = &(pos1[curPedal]);
prg = pedal1->act1;
} else {
//act2
pos1[curPedal] = 0;
pos = &(pos2[curPedal]);
prg = pedal1->act2;
}
}
while (1) {
if (wait[curPedal]) {
wait[curPedal]--;
return;
} else if (prg[*pos] == 250) {
wait[curPedal] = prg[++*pos];
} else if (prg[*pos] == 254) {
//Удерживать клавишу, следующую за *pos
Keyboard.press(prg[++*pos]);
} else if (prg[*pos] == 253) {
//Отпустить клавишу, следующую за *pos
Keyboard.release(prg[++*pos]);
} else if (prg[*pos] == 252) {
delay(10);
//"Пропуск хода", ничего не делать
++*pos;
return;
} else if (prg[*pos] == 251) {
//Переместиться в программе на позицию в ячейке *pos+1
*pos = prg[*pos + 1];
return;
} else if (prg[*pos] == 255 || prg[*pos] == 0) {
//Конец программы, просто заглушка
return;
} else {
//Отправляем нажатие клавиши
Keyboard.write(prg[*pos]);
}
//Циклически переходим на ячейку вперёд после тех команд, после которых это необходимо
if (++*pos >= 16)
pos = 0;
}
}
Послесловие
Хотя изначально я и делал педальборд для возможности проматывания записи во время игры на гитаре, однако лично мне показалось удобным испольщование педалей и в обычных задачах, главное немного привыкнуть к такому необычному манипулятору. А вот тут кроется ещё одна проблема: уже без любимых педалей работать становится наоборот сложнее, так как приходится вспоминать, что, куда и для чего нажимать. Если в офис педали ещё можно носить и подключать, то в институте бегать с ними по кабинетам уже сложнее. Так что использовать этот девайс для чего-то, кроме его изначального предназначения стоит на свой страх и риск.
Собраный педальборд:
Автор: HukuToc2288