Последнее обновление: 08.11.2019
Что вы будете строить
В этой лабораторной работе вы создадите веб-страницу, использующую Web Serial API для взаимодействия с платой BBC micro:bit для отображения изображений на ее светодиодной матрице 5x5. Вы узнаете об API Web Serial и о том, как использовать доступные для чтения, записи и преобразования потоки для связи с последовательными устройствами через браузер.
Что вы узнаете
- Как открыть и закрыть последовательный веб-порт
- Как использовать цикл чтения для обработки данных из входного потока
- Как отправить данные через поток записи
Что вам понадобится
- Плата BBC micro:bit с последней прошивкой Espruino.
- Последняя версия Chrome (78 или новее)
- Знание HTML, CSS, JavaScript и Chrome DevTools
Мы решили использовать micro:bit для этой кодовой лаборатории, потому что он доступен по цене, предлагает несколько входов (кнопки) и выходов (светодиодный дисплей 5x5) и может предоставлять дополнительные входы и выходы. См . страницу BBC micro:bit на сайте Espruino, чтобы узнать, на что способен micro:bit.
Web Serial API предоставляет веб-сайтам возможность чтения и записи на последовательное устройство с помощью сценариев. API связывает Интернет и физический мир, позволяя веб-сайтам взаимодействовать с последовательными устройствами, такими как микроконтроллеры и 3D-принтеры.
Существует множество примеров управляющего программного обеспечения, созданного с использованием веб-технологий. Например:
В некоторых случаях эти веб-сайты взаимодействуют с устройством через собственное приложение-агент, которое устанавливается пользователем вручную. В других случаях приложение доставляется в виде упакованного собственного приложения через такую инфраструктуру, как Electron. В других случаях от пользователя требуется выполнить дополнительный шаг, например, скопировать скомпилированное приложение на устройство с помощью флешки.
Пользовательский опыт можно улучшить, обеспечив прямую связь между сайтом и устройством, которым он управляет.
Включить веб-последовательный API
Web Serial API в настоящее время находится в разработке и доступен только с пометкой. Вы должны включить флаг #enable-experimental-web-platform-features
в chrome://flags
.
Получить код
Мы поместили все, что вам нужно для этой лаборатории кода, в проект Glitch.
- Откройте новую вкладку браузера и перейдите по адресу https://web-serial-codelab-start.glitch.me/ .
- Щелкните ссылку Remix Glitch , чтобы создать собственную версию стартового проекта.
- Нажмите кнопку « Показать» , а затем выберите «В новом окне », чтобы увидеть свой код в действии.
Проверьте, поддерживается ли Web Serial API.
Первое, что нужно сделать, это проверить, поддерживается ли Web Serial API в текущем браузере. Для этого проверьте, есть ли serial
в navigator
.
В событии DOMContentLoaded
добавьте в проект следующий код:
script.js - DOMContentLoaded
// CODELAB: Add feature detection here.
if ('serial' in navigator) {
const notSupported = document.getElementById('notSupported');
notSupported.classList.add('hidden');
}
Это проверяет, поддерживается ли Web Serial. Если это так, этот код скрывает баннер, в котором говорится, что Web Serial не поддерживается.
Попытайся
- Загрузите страницу.
- Убедитесь, что на странице не отображается красный баннер о том, что Web Serial не поддерживается.
Откройте последовательный порт
Далее нам нужно открыть последовательный порт. Как и большинство других современных API, Web Serial API является асинхронным. Это предотвращает блокировку пользовательского интерфейса при ожидании ввода, но это также важно, поскольку последовательные данные могут быть получены веб-страницей в любое время, и нам нужен способ их прослушивания.
Поскольку компьютер может иметь несколько последовательных устройств, когда браузер пытается запросить порт, он предлагает пользователю выбрать, к какому устройству подключиться.
Добавьте в свой проект следующий код:
script.js - connect()
// CODELAB: Add code to request & open port here.
// - Request a port and open a connection.
port = await navigator.serial.requestPort();
// - Wait for the port to open.
await port.open({ baudrate: 9600 });
requestPort
подсказывает пользователю, к какому устройству он хочет подключиться. Вызов port.open
открывает порт. Нам также необходимо указать скорость, с которой мы хотим обмениваться данными с последовательным устройством. BBC micro:bit использует соединение на скорости 9600 бод между чипом USB-to-serial и главным процессором.
Давайте также подключим кнопку подключения, чтобы она вызывала connect()
, когда пользователь нажимает на нее.
Добавьте в свой проект следующий код:
script.js - clickConnect()
// CODELAB: Add connect code here.
await connect();
Попытайся
Теперь у нашего проекта есть необходимый минимум для начала работы. Нажатие кнопки « Подключить » предлагает пользователю выбрать последовательное устройство для подключения, а затем подключиться к micro:bit.
- Перезагрузите страницу.
- Нажмите кнопку Подключить .
- В диалоговом окне выбора последовательного порта выберите устройство BBC micro:bit и нажмите « Подключиться ».
- На вкладке вы должны увидеть значок, указывающий, что вы подключились к последовательному устройству:
Настройте входной поток для прослушивания данных из последовательного порта
После того, как соединение было установлено, нам нужно настроить входной поток и ридер для чтения данных с устройства. Во-первых, мы получим читаемый поток из порта, вызвав port.readable
. Поскольку мы знаем, что получим текст с устройства, мы пропустим его через декодер текста. Далее мы получим считыватель и запустим цикл чтения.
Добавьте в свой проект следующий код:
script.js - connect()
// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable;
reader = inputStream.getReader();
readLoop();
Цикл чтения — это асинхронная функция, которая работает в цикле и ожидает содержимого, не блокируя основной поток. При поступлении новых данных средство чтения возвращает два свойства: value
и done
логическое значение. Если done
равно true, порт закрыт или данные больше не поступают.
Добавьте в свой проект следующий код:
script.js - readLoop()
// CODELAB: Add read loop here.
while (true) {
const { value, done } = await reader.read();
if (value) {
log.textContent += value + '\n';
}
if (done) {
console.log('[readLoop] DONE', done);
reader.releaseLock();
break;
}
}
Попытайся
Теперь наш проект может подключиться к устройству и добавит любые данные, полученные от устройства, в элемент журнала.
- Перезагрузите страницу.
- Нажмите кнопку Подключить .
- В диалоговом окне выбора последовательного порта выберите устройство BBC micro:bit и нажмите « Подключиться ».
- Вы должны увидеть логотип Espruino:
Настройте выходной поток для отправки данных на последовательный порт
Последовательная связь обычно двунаправленная. Помимо получения данных из последовательного порта, мы также хотим отправить данные в порт. Как и в случае с входным потоком, мы будем отправлять текст только через выходной поток в micro:bit.
Во-первых, создайте поток текстового кодировщика и направьте поток в port.writeable
.
script.js - connect()
// CODELAB: Add code setup the output stream here.
const encoder = new TextEncoderStream();
outputDone = encoder.readable.pipeTo(port.writable);
outputStream = encoder.writable;
При последовательном подключении к прошивке Espruino плата BBC micro:bit действует как цикл чтения-оценки-печати JavaScript (REPL) , аналогичный тому, что вы получаете в оболочке Node.js. Далее нам нужно предоставить метод для отправки данных в поток. Приведенный ниже код получает средство записи из выходного потока, а затем использует write
для отправки каждой строки. Каждая отправляемая строка включает символ новой строки ( \n
), чтобы указать micro:bit оценить отправленную команду.
script.js - writeToStream()
// CODELAB: Write to output stream
const writer = outputStream.getWriter();
lines.forEach((line) => {
console.log('[SEND]', line);
writer.write(line + '\n');
});
writer.releaseLock();
Чтобы перевести систему в известное состояние и остановить ее от повторения символов, которые мы ей отправляем, нам нужно отправить CTRL-C и отключить эхо.
script.js - connect()
// CODELAB: Send CTRL-C and turn off echo on REPL
writeToStream('\x03', 'echo(false);');
Попытайся
Наш проект теперь может отправлять и получать данные от micro:bit. Давайте проверим, что мы можем правильно отправить команду:
- Перезагрузите страницу.
- Нажмите кнопку Подключить .
- В диалоговом окне выбора последовательного порта выберите устройство BBC micro:bit и нажмите « Подключиться ».
- Откройте вкладку « Консоль » в Chrome DevTools и введите
writeToStream('console.log("yes")');
Вы должны увидеть что-то вроде этого, напечатанного на странице:
Построить строку матричной сетки
Чтобы управлять светодиодной матрицей на micro:bit, нам нужно вызвать show()
. Этот метод показывает графику на встроенном светодиодном экране 5x5. Это принимает двоичное число или строку.
Мы пройдемся по флажкам и сгенерируем массив из 1 и 0, указывающий, что отмечено, а что нет. Затем нам нужно перевернуть массив, потому что порядок наших флажков противоположен порядку светодиодов в матрице. Далее мы конвертируем массив в строку и создаем команду для отправки в micro:bit.
script.js - sendGrid()
// CODELAB: Generate the grid
const arr = [];
ledCBs.forEach((cb) => {
arr.push(cb.checked === true ? 1 : 0);
});
writeToStream(`show(0b${arr.reverse().join('')})`);
Установите флажки, чтобы обновить матрицу
Затем нам нужно прослушивать изменения флажков и, если они меняются, отправлять эту информацию в micro:bit. В коде обнаружения функций ( // CODELAB: Add feature detection here.
) добавьте следующую строку:
script.js - DOMContentLoaded
initCheckboxes();
Давайте также сбросим сетку при первом подключении micro:bit, чтобы она показывала счастливое лицо. Функция drawGrid()
уже предоставлена. Эта функция работает аналогично sendGrid()
; он принимает массив из 1 и 0 и устанавливает соответствующие флажки.
script.js - clickConnect()
// CODELAB: Reset the grid on connect here.
drawGrid(GRID_HAPPY);
sendGrid();
Попытайся
Теперь, когда страница открывает соединение с micro:bit, она будет отправлять счастливое лицо. Установка флажков обновит отображение на светодиодной матрице.
- Перезагрузите страницу.
- Нажмите кнопку Подключить .
- В диалоговом окне выбора последовательного порта выберите устройство BBC micro:bit и нажмите « Подключиться ».
- Вы должны увидеть улыбку на светодиодной матрице micro:bit.
- Нарисуйте другой рисунок на светодиодной матрице, изменив флажки.
Добавьте событие просмотра на кнопки micro:bit
На micro:bit есть две кнопки, по одной с каждой стороны светодиодной матрицы. Espruino предоставляет функцию setWatch
, которая отправляет событие/обратный вызов при нажатии кнопки. Поскольку мы хотим прослушивать обе кнопки, мы сделаем нашу функцию универсальной и заставим ее печатать подробности события.
script.js - watchButton()
// CODELAB: Hook up the micro:bit buttons to print a string.
const cmd = `
setWatch(function(e) {
print('{"button": "${btnId}", "pressed": ' + e.state + '}');
}, ${btnId}, {repeat:true, debounce:20, edge:"both"});
`;
writeToStream(cmd);
Затем нам нужно подключить обе кнопки (названные BTN1 и BTN2 на плате micro:bit) каждый раз, когда последовательный порт подключается к устройству.
script.js - clickConnect()
// CODELAB: Initialize micro:bit buttons.
watchButton('BTN1');
watchButton('BTN2');
Попытайся
Помимо отображения счастливого лица при подключении, нажатие любой из кнопок на micro:bit добавит на страницу текст, указывающий, какая кнопка была нажата. Скорее всего, каждый символ будет на своей линии.
- Перезагрузите страницу.
- Нажмите кнопку Подключить .
- В диалоговом окне выбора последовательного порта выберите устройство BBC micro:bit и нажмите « Подключиться ».
- Вы должны увидеть улыбку на светодиодной матрице micro:bits.
- Нажимайте кнопки на micro:bit и убедитесь, что он добавляет новый текст на страницу с информацией о нажатой кнопке.
Базовая обработка потоков
Когда одна из кнопок micro:bit нажата, micro:bit отправляет данные на последовательный порт через поток. Потоки очень полезны, но они также могут быть проблемой, потому что вы не обязательно получите все данные сразу, и они могут быть произвольно разбиты на части.
В настоящее время приложение печатает входящий поток по мере его поступления (в readLoop
). В большинстве случаев каждый символ находится на отдельной строке, но это не очень удобно. В идеале поток должен быть разбит на отдельные строки, и каждое сообщение должно отображаться как отдельная строка.
Преобразование потоков с помощью TransformStream
Для этого мы можем использовать поток преобразования ( TransformStream
), который позволяет анализировать входящий поток и возвращать проанализированные данные. Поток преобразования может располагаться между источником потока (в данном случае micro:bit) и тем, что потребляет поток (в данном случае readLoop
), и может применять произвольное преобразование до того, как оно будет окончательно использовано. Думайте об этом как о сборочной линии: по мере того, как виджет спускается по конвейеру, каждый шаг в этой линии изменяет виджет, так что к тому времени, когда он достигает конечного пункта назначения, это полностью функционирующий виджет.
Для получения дополнительной информации см . Концепции MDN Streams API .
Преобразуйте поток с помощью LineBreakTransformer
Давайте создадим класс LineBreakTransformer
, который будет принимать поток и разбивать его на части на основе разрывов строк ( \r\n
). Классу нужны два метода: transform
и flush
. Метод transform
вызывается каждый раз, когда поток получает новые данные. Он может либо поставить данные в очередь, либо сохранить их на потом. Метод flush
вызывается при закрытии потока и обрабатывает любые данные, которые еще не были обработаны.
В нашем методе transform
мы добавим новые данные в container
, а затем проверим, есть ли в container
разрывы строк. Если есть, разделите его на массив, а затем выполните итерацию по строкам, вызвав controller.enqueue()
для отправки проанализированных строк.
script.js - LineBreakTransformer.transform()
// CODELAB: Handle incoming chunk
this.container += chunk;
const lines = this.container.split('\r\n');
this.container = lines.pop();
lines.forEach(line => controller.enqueue(line));
Когда поток закрывается, мы просто сбрасываем все оставшиеся данные в контейнере, используя enqueue
.
script.js - LineBreakTransformer.flush()
// CODELAB: Flush the stream.
controller.enqueue(this.container);
Наконец, нам нужно направить входящий поток через новый LineBreakTransformer
. Наш исходный входной поток передавался только через TextDecoderStream
, поэтому нам нужно добавить дополнительный pipeThrough
, чтобы передать его через наш новый LineBreakTransformer
.
script.js - connect()
// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable
.pipeThrough(new TransformStream(new LineBreakTransformer()));
Попытайся
Теперь, когда вы нажимаете одну из кнопок micro:bit, напечатанные данные должны возвращаться в одну строку.
- Перезагрузите страницу.
- Нажмите кнопку Подключить .
- В диалоговом окне выбора последовательного порта выберите устройство BBC micro:bit и нажмите « Подключиться ».
- Вы должны увидеть улыбку на светодиодной матрице micro:bit.
- Нажмите кнопки на micro:bit и убедитесь, что вы видите что-то вроде следующего:
Преобразование потока с помощью JSONTransformer
Мы могли бы попытаться преобразовать строку в JSON в readLoop
, но вместо этого давайте создадим очень простой преобразователь JSON, который преобразует данные в объект JSON. Если данные недействительны в формате JSON, просто верните то, что пришло.
script.js - JSONTransformer.transform
// CODELAB: Attempt to parse JSON content
try {
controller.enqueue(JSON.parse(chunk));
} catch (e) {
controller.enqueue(chunk);
}
Затем направьте поток через JSONTransformer
после того, как он прошел через LineBreakTransformer
. Это позволяет нам сделать наш JSONTransformer
простым, поскольку мы знаем, что JSON всегда будет отправляться только в одной строке.
script.js - connect
// CODELAB: Add code to read the stream here.
let decoder = new TextDecoderStream();
inputDone = port.readable.pipeTo(decoder.writable);
inputStream = decoder.readable
.pipeThrough(new TransformStream(new LineBreakTransformer()))
.pipeThrough(new TransformStream(new JSONTransformer()));
Попытайся
Теперь, когда вы нажимаете одну из кнопок micro:bit, вы должны увидеть [object Object]
, напечатанный на странице.
- Перезагрузите страницу.
- Нажмите кнопку Подключить .
- В диалоговом окне выбора последовательного порта выберите устройство BBC micro:bit и нажмите « Подключиться ».
- Вы должны увидеть улыбку на светодиодной матрице micro:bit.
- Нажмите кнопки на micro:bit и убедитесь, что вы видите что-то вроде следующего:
Реакция на нажатие кнопок
Чтобы реагировать на нажатия кнопки micro:bit, обновите readLoop
, чтобы проверить, являются ли полученные данные object
со свойством button
. Затем вызовите buttonPushed
для обработки нажатия кнопки.
script.js - readLoop()
const { value, done } = await reader.read();
if (value && value.button) {
buttonPushed(value);
} else {
log.textContent += value + '\n';
}
При нажатии кнопки micro:bit изображение на светодиодной матрице должно измениться. Используйте следующий код для установки матрицы:
script.js - buttonPushed()
// CODELAB: micro:bit button press handler
if (butEvt.button === 'BTN1') {
divLeftBut.classList.toggle('pressed', butEvt.pressed);
if (butEvt.pressed) {
drawGrid(GRID_HAPPY);
sendGrid();
}
return;
}
if (butEvt.button === 'BTN2') {
divRightBut.classList.toggle('pressed', butEvt.pressed);
if (butEvt.pressed) {
drawGrid(GRID_SAD);
sendGrid();
}
}
Попытайся
Теперь, когда вы нажимаете одну из кнопок micro:bit, светодиодная матрица должна измениться либо на счастливое, либо на грустное лицо.
- Перезагрузите страницу.
- Нажмите кнопку Подключить .
- В диалоговом окне выбора последовательного порта выберите устройство BBC micro:bit и нажмите « Подключиться ».
- Вы должны увидеть улыбку на светодиодной матрице micro:bits.
- Нажмите кнопки на micro:bit и убедитесь, что светодиодная матрица изменилась.
Последним шагом является подключение функции отключения, чтобы закрыть порт, когда пользователь закончит работу.
Закрывайте порт, когда пользователь нажимает кнопку «Подключить/Отключить».
Когда пользователь нажимает кнопку Connect / Disconnect , нам нужно закрыть соединение. Если порт уже открыт, вызовите метод disconnect()
и обновите пользовательский интерфейс, чтобы указать, что страница больше не подключена к последовательному устройству.
script.js - clickConnect()
// CODELAB: Add disconnect code here.
if (port) {
await disconnect();
toggleUIConnected(false);
return;
}
Закрыть потоки и порт
В функции disconnect
нам нужно закрыть входной поток, закрыть выходной поток и закрыть порт. Чтобы закрыть входной поток, вызовите reader.cancel()
. Вызов cancel
является асинхронным, поэтому нам нужно использовать await
, чтобы дождаться его завершения:
script.js - disconnect()
// CODELAB: Close the input stream (reader).
if (reader) {
await reader.cancel();
await inputDone;
reader = null;
inputDone = null;
}
Чтобы закрыть выходной поток, получите writer
, вызовите close()
и дождитесь закрытия объекта outputDone
:
script.js - disconnect()
// CODELAB: Close the output stream.
if (outputStream) {
await outputStream.getWriter().close();
await outputDone;
outputStream = null;
outputDone = null;
}
Наконец, закройте последовательный порт и подождите, пока он закроется:
script.js - disconnect()
// CODELAB: Close the port.
await port.close();
port = null;
Попытайся
Теперь вы можете открывать и закрывать последовательный порт по своему желанию.
- Перезагрузите страницу.
- Нажмите кнопку Подключить .
- В диалоговом окне выбора последовательного порта выберите устройство BBC micro:bit и нажмите « Подключиться».
- Вы должны увидеть улыбку на светодиодной матрице micro:bit.
- Нажмите кнопку Disconnect и убедитесь, что светодиодная матрица погасла и в консоли нет ошибок.
Поздравляем! Вы успешно создали свое первое веб-приложение, использующее Web Serial API.
Следите за https://goo.gle/fugu-api-tracker , чтобы быть в курсе последних новостей о Web Serial API и обо всех других интересных новых веб-возможностях, над которыми работает команда Chrome.