- PVSM.RU - https://www.pvsm.ru -
Привет. У меня дома уже много лет лежит iRobot Roomba — та самая, из середины нулевых. Выкидывать жалко, работать как пылесос она уже толком не хочет, а тут я вспомнил про старый проект HackingRoomba Тода Курта: он ещё в 2006-м показал, что Roomba можно заставить играть мелодии через открытый протокол Roomba Open Interface. Идея была классная, но утилиты RoombaMidi и RoombaMidi2 давно перестали запускаться на современных ОС — драйверов нет, порты другие, Java несовместимая. В общем, всё умерло, кроме самих роботов.
Вот я и решил: а почему бы не написать своё приложение, которое будет работать на нормальном современном ПК? Так появился RoombaMusic. О том, как я до этого дошёл, что сломалось по дороге и почему в итоге всё-таки заиграло — ниже.
Roomba общается по последовательному порту через USB-TTL кабель, и протокол у неё довольно дружелюбный. Сначала отправляешь команду 128 (START) — робот просыпается. Потом 131 (SAFE MODE) или 132 (FULL MODE) — и он готов слушать команды. Дальше можно слать что угодно: музыку, движение, запросы к датчикам.
С музыкой всё интереснее. Есть команда 140 — запись песни в слот памяти, и команда 141 — воспроизведение этого слота. Казалось бы, записал мелодию, нажал play, радуйся. Но есть нюанс: в один слот влезает не больше 16 нот. Для короткого риффа хватит, для нормальной мелодии — нет. Именно на этом ограничении спотыкались все, кто пытался играть что-то серьёзнее «Ля-Ми-Ля-Соль-Фа».
Слот — это небольшой кусок памяти внутри контроллера Roomba, куда команда 140 записывает последовательность пар «нота — длительность». Формат простой: сначала идёт номер слота (их несколько), потом количество нот, а дальше пары байтов. Каждая нота — это число от 31 до 127 (MIDI-нотация), каждая длительность — число от 1 до 255 в единицах 1/64 секунды. То есть максимальная длительность одной ноты — примерно 4 секунды, а минимальная — около 15 миллисекунд.
Ограничение в 16 нот — это ограничение размера буфера под одну песню. При попытке записать больше Roomba просто обрежет мелодию или вернёт ошибку, в зависимости от прошивки. И вот тут начинается самое интересное: стандартный путь «записал — играй» для реальной музыки не работает. MIDI-файлы, которые я хотел играть, содержат сотни и тысячи нот. Даже простенькая мелодия на 30 секунд — это далеко за пределами 16.
Я рассматривал несколько обходных путей. Можно было бы использовать несколько слотов одновременно — но их количество тоже ограничено, и переключение между ними на ходу даёт слышимые щелчки. Можно было бы вообще не использовать слоты, а слать команды «проиграть ноту» напрямую — но такой команды в протоколе нет, есть только запись в слот и воспроизведение слота. Так что остался единственный вариант, который в итоге и заработал: постоянная перезапись слота на ходу.
Логика такая: приложение записывает одну ноту в слот, сразу отправляет команду play, и пока Roomba её тянет — успевает записать следующую. Одна нота перезаписывает другую, и снаружи это звучит как непрерывная мелодия, хотя внутри слота в любой момент времени лежит ровно одна нота.
Вот этот механизм в коде — по сути, весь фокус проекта:
private void sendNoteToRoomba(int note) {
try {
// Записываем одну ноту в слот 0
byte[] songData = { (byte) 140, 0, 1, (byte) note, 8 };
serialPort.writeBytes(songData);
// Сразу запускаем воспроизведение
byte[] playCmd = new byte[]{(byte) 141, 0};
serialPort.writeBytes(playCmd);
} catch (SerialPortException e) {
logText("Ошибка отправки ROI: " + e.getMessage());
}
}
Выглядит просто, но за этими шестью строками — несколько вечеров отладки. Основная боль была в таймингах. Roomba — не синтезатор, она не любит, когда ей шлют команды слишком быстро: байты начинают слипаться, и вместо мелодии получается каша. Если же слать слишком медленно — между нотами появляются слышимые паузы, и ритм разваливается.
Я экспериментировал с задержками между записью и play, смотрел на логи и слушал результат. В какой-то момент понял, что идеальная задержка зависит от длительности самой ноты: короткие ноты можно отправлять почти без паузы, а длинные требуют, чтобы play успел сработать до перезаписи. В итоге остановился на том, что приложение просто шлёт команды так быстро, как позволяет порт, а Roomba сама разбирается — её буфер успевает принять следующую пару байтов, пока играет предыдущая нота. На слух это работает, хотя в логах видно, что иногда команды уходят буквально друг за другом без зазора.
Я выбрал Java, потому что хотелось кроссплатформенности: у меня самого Linux, но мало ли, кто ещё захочет запустить. А для работы с последовательным портом взял библиотеку jssc — она живая, обновляется и нормально дружит с современными ОС, в отличие от старых Java-обёрток, на которых висели оригинальные RoombaMidi. Собственно, одна из причин, почему старый софт умер — как раз в том, что библиотеки для портов перестали собираться и работать.
С портами, кстати, отдельная история. На Windows они называются COM3, COM4 и так далее, на Linux — /dev/ttyUSB0. Чтобы не заставлять пользователя вбивать это руками, я сделал выпадающий список через SerialPortList.getPortNames() — приложение само показывает, что нашло. Подключение и инициализация выглядят так:
private void connect() {
try {
serialPort = new SerialPort((String) portList.getSelectedItem());
serialPort.openPort();
serialPort.setParams(115200, 8, 1, 0);
// START (128) + CONTROL MODE (130)
byte[] initCmd = new byte[]{(byte) 128, (byte) 130};
serialPort.writeBytes(initCmd);
btnConnect.setEnabled(false);
btnOpenFile.setEnabled(true);
statusLabel.setText("Roomba Ready!");
} catch (Exception ex) {
logText("ERROR: " + ex.getMessage());
}
}
Скорость 115200, 8 бит данных, 1 стоп-бит, без чётности — стандартные параметры для ROI. После инициализации кнопка «Подключить» блокируется, а кнопка выбора файла разблокируется, чтобы нельзя было случайно переподключиться посреди игры.
Самая приятная часть — приложение умеет читать обычные .midi файлы. Внутри Java есть javax.sound.midi, который умеет разбирать MIDI и проигрывать его через Sequencer. Я подписался на сообщения секвенсора и при каждом NOTE_ON отправляю ноту в Roomba. То есть робот становится таким странным синтезатором, который играет всё, что вы ему подсунете, — были бы ноты в диапазоне, который он умеет.
Тут есть тонкость с длительностями. MIDI-файл содержит информацию о том, когда нота началась и когда закончилась, а команда 140 требует явную длительность. Я решил не заморачиваться с точным соответствием и всегда шлю длительность 8 (то есть примерно 125 миллисекунд) — на слух этого достаточно, чтобы нота прозвучала, а следующая команда успела её перезаписать. Если нота в MIDI длиннее, она всё равно обрывается перезаписью, если короче — Roomba доигрывает свои 125 мс. Для большинства мелодий это звучит приемлемо, хотя идеального ритма таким способом, конечно, не добиться.
Ещё один момент — диапазон нот. Roomba понимает не все MIDI-ноты, а только от 31 до 127. Если в файле встречаются ноты ниже, приложение их просто пропускает, иначе робот может зависнуть или издать странный звук. Это ограничение протокола, с ним ничего не сделать.
Внутри всё устроено довольно просто, но с одним важным нюансом. Основной поток — это Swing-интерфейс. Когда пользователь выбирает файл, запускается отдельный поток, который открывает Sequencer, подписывается на сообщения и стартует воспроизведение. Receiver секвенсора вызывает sendNoteToRoomba напрямую — то есть команды в порт уходят из потока MIDI-секвенсора, а не из UI-потока. Это важно: если бы я слал команды из UI-потока, интерфейс бы подвисал на время отправки, а так всё работает гладко.
Логи выводятся в текстовое поле через SwingUtilities.invokeLater — это стандартный способ обновлять Swing-компоненты из другого потока. В логе видно каждую отправленную команду в DEC и HEX, так что если что-то звучит не так, можно посмотреть, что именно улетело в робота.
Честно говоря, идеального звука добиться не удалось. Главная проблема — длинные ноты. Если в MIDI нота тянется секунду, а приложение шлёт её с длительностью 125 мс, она обрывается раньше времени. Можно было бы вычислять длительность из MIDI-событий, но тогда пришлось бы ждать окончания ноты перед отправкой следующей, а это убивает всю идею перезаписи. Так что я остановился на компромиссе: короткие ноты звучат нормально, длинные — обрываются. Для быстрых мелодий это почти незаметно, для медленных — слышно.
Ещё одна проблема — дрейф темпа. Поскольку я не синхронизирую отправку команд с реальным временем MIDI, а просто шлю их так быстро, как получается, мелодия может слегка «плыть» относительно оригинала. На коротких отрывках это незаметно, на длинных — накапливается. В будущем можно было бы прикрутить таймер, который отправляет команды строго по timestamp'ам из MIDI, но пока руки не дошли.
Из идей на будущее: добавить поддержку движения, чтобы робот не только играл, но и «танцевал»; сделать сохранение настроек (порт, задержки); может быть, прикрутить веб-интерфейс, чтобы можно было управлять роботом с телефона. Но это уже как пойдёт.
Код лежит на GitHub: github.com/TimoNiki [1]. Видео, как это звучит: youtu.be/AulgS5baU0M [2].
Если у вас лежит старая Roomba и вы не знаете, куда её деть — попробуйте, это реально весело. Протокол открытый, документация есть, а ощущение, когда робот-пылесос играет вашу любимую мелодию, — бесценно.
Автор: TimoNiki
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/461954
Ссылки в тексте:
[1] github.com/TimoNiki: http://github.com/TimoNiki
[2] youtu.be/AulgS5baU0M: http://youtu.be/AulgS5baU0M
[3] Источник: https://habr.com/ru/articles/1106144/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1106144
Нажмите здесь для печати.