#include <SoftwareSerial.h> SoftwareSerial SerialBt(2, 3); void setup() { Serial.begin(9600); SerialBt.begin(9600); } void loop() { if (SerialBt.available()) { Serial.write(SerialBt.read()); } if (Serial.available()) { SerialBt.write(Serial.read()); } }
CR+LF
, “Both NL & CR” option in Serial Monitor). Some BLE modules by default work at a different speed, for example, at 38400, some modules enter configuration mode after clicking a button located on their board, some modules do not require commands to be in upper case — check the specifications of your particular module.0xFFE0
, and the characteristic UUID is set as 0xFFE1
— we will need this later. Some commands that work with my module:AT
- performance check;AT+HELP
- output all commands;AT+DEFAULT
- reset to factory settings;AT+RESET
- soft reboot;AT+ROLE
- output mode;AT+ROLE0
- setting the slave mode;AT+NAME
- display the module name;AT+NAMESimon
- set the module name as Simon
;AT+PIN
- output PIN code (password) for pairing;AT+PIN123456
- set the PIN code as 123456
;AT+UUID
- display the UUID of the service;AT+UUID0xFFE0
- setting the service UUID as 0xFFE0
;AT+CHAR
- output UUID characteristics;AT+CHAR0xFFE1
- set the UUID characteristics as 0xFFE1
.onclick
button handler is triggered, which sends the message GRIPPER=CLOSE
. The controller receives the message, understands what is required of it, closes the claw and sends back the message GRIPPER=CLOSED
. Processing this message, we in JS remember the state of the claw and change the text on the button to “Open”.index.html
, one stylesheet file styles.css
and one main.js
file in which all the magic will occur. <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link href="styles.css" rel="stylesheet"> </head> <body> <button id="connect" type="button">Connect</button> <button id="disconnect" type="button">Disconnect</button> <div id="terminal"></div> <form id="send-form"> <input id="input" type="text"> <button type="submit">Send</button> </form> <script src="main.js"></script> </body> </html>
<div id="terminal"> <div> ...</div> <div class="out"> </div> <div class="in"> </div> </div>
#terminal div { color: gray; } #terminal div.out { color: red; } #terminal div.in { color: blue; }
main.js
// UI let connectButton = document.getElementById('connect'); let disconnectButton = document.getElementById('disconnect'); let terminalContainer = document.getElementById('terminal'); let sendForm = document.getElementById('send-form'); let inputField = document.getElementById('input'); // Connect connectButton.addEventListener('click', function() { connect(); }); // Disconnect disconnectButton.addEventListener('click', function() { disconnect(); }); // sendForm.addEventListener('submit', function(event) { event.preventDefault(); // send(inputField.value); // inputField.value = ''; // inputField.focus(); // }); // Bluetooth function connect() { // } // function disconnect() { // } // function send(data) { // }
// let deviceCache = null; // Bluetooth function connect() { return (deviceCache ? Promise.resolve(deviceCache) : requestBluetoothDevice()). then(device => connectDeviceAndCacheCharacteristic(device)). then(characteristic => startNotifications(characteristic)). catch(error => log(error)); } // Bluetooth function requestBluetoothDevice() { // } // , function connectDeviceAndCacheCharacteristic(device) { // } // function startNotifications(characteristic) { // } // function log(data, type = '') { // }
connect()
function, we implemented a Promise chain (a chain of functions returning Promise objects) corresponding to the connection stages.deviceCache
variable, into which we will later write down the object of the device selected by the user in order to know what to reconnect in case of disconnection.connect()
function, the ternary operator immediately creates a completed Promise with the deviceCache
object, if it is non-zero, or calls the function of requesting a Bluetooth device to select otherwise. Thus, if the user has already connected to the device, then when you next click on the “Connect” button, the device selection dialog will not appear.log()
function, which we also implement later.navigator.bluetooth.requestDevice()
function with the configuration object as a required argument that describes which Bluetooth devices we are interested in. You can use the filter by service, by name, you can accept all devices, but you still need to specify the used service, otherwise the browser will not provide access to it. // Bluetooth function requestBluetoothDevice() { log('Requesting bluetooth device...'); return navigator.bluetooth.requestDevice({ filters: [{services: [0xFFE0]}], }). then(device => { log('"' + device.name + '" bluetooth device selected'); deviceCache = device; return deviceCache; }); }
0xFFE0
, which the BLE module was configured to use. After the user selects a device, the Promise runs with a device object, which we write to the above cache and return later. // let characteristicCache = null; // , function connectDeviceAndCacheCharacteristic(device) { if (device.gatt.connected && characteristicCache) { return Promise.resolve(characteristicCache); } log('Connecting to GATT server...'); return device.gatt.connect(). then(server => { log('GATT server connected, getting service...'); return server.getPrimaryService(0xFFE0); }). then(service => { log('Service found, getting characteristic...'); return service.getCharacteristic(0xFFE1); }). then(characteristic => { log('Characteristic found'); characteristicCache = characteristic; return characteristicCache; }); }
characteristicCache
- by analogy with deviceCache
- saves the resulting feature object, it will be needed to write data to it, that is, to send a message from the browser to the device.getPrimaryService()
and getCharacteristic()
functions, the UUIDs are used as arguments, with which the BLE module is configured. // function startNotifications(characteristic) { log('Starting notifications...'); return characteristic.startNotifications(). then(() => { log('Notifications started'); }); }
startNotifications()
method of the characteristic object, and then hang the handler on the characteristic change event, but more on that later. // function log(data, type = '') { terminalContainer.insertAdjacentHTML('beforeend', '<div' + (type ? ' class="' + type + '"' : '') + '>' + data + '</div>'); }
insertAdjacentHTML()
method, we insert a div with the class specified in the type
argument at the end of the terminal's div container — very simple.gattserverdisconnected
event whose handler should be hung onto the device object. The most logical place for this is the device selection function: // Bluetooth function requestBluetoothDevice() { log('Requesting bluetooth device...'); return navigator.bluetooth.requestDevice({ filters: [{services: [0xFFE0]}], }). then(device => { log('"' + device.name + '" bluetooth device selected'); deviceCache = device; // deviceCache.addEventListener('gattserverdisconnected', handleDisconnection); return deviceCache; }); } // function handleDisconnection(event) { let device = event.target; log('"' + device.name + '" bluetooth device disconnected, trying to reconnect...'); connectDeviceAndCacheCharacteristic(device). then(characteristic => startNotifications(characteristic)). catch(error => log(error)); }
gattserverdisconnected
event, otherwise the browser will simply reconnect: // function disconnect() { if (deviceCache) { log('Disconnecting from "' + deviceCache.name + '" bluetooth device...'); deviceCache.removeEventListener('gattserverdisconnected', handleDisconnection); if (deviceCache.gatt.connected) { deviceCache.gatt.disconnect(); log('"' + deviceCache.name + '" bluetooth device disconnected'); } else { log('"' + deviceCache.name + '" bluetooth device is already disconnected'); } } characteristicCache = null; deviceCache = null; }
deviceCache
, then the next time you click the "Connect" button, the device selection dialog will not appear, connecting to the previous device instead.characteristicvaluechanged
. This should be done after the inclusion of notifications. It will also correctly remove the handler from the characteristic when the device is disconnected: // function startNotifications(characteristic) { log('Starting notifications...'); return characteristic.startNotifications(). then(() => { log('Notifications started'); // characteristic.addEventListener('characteristicvaluechanged', handleCharacteristicValueChanged); }); } // function disconnect() { if (deviceCache) { log('Disconnecting from "' + deviceCache.name + '" bluetooth device...'); deviceCache.removeEventListener('gattserverdisconnected', handleDisconnection); if (deviceCache.gatt.connected) { deviceCache.gatt.disconnect(); log('"' + deviceCache.name + '" bluetooth device disconnected'); } else { log('"' + deviceCache.name + '" bluetooth device is already disconnected'); } } // if (characteristicCache) { characteristicCache.removeEventListener('characteristicvaluechanged', handleCharacteristicValueChanged); characteristicCache = null; } deviceCache = null; } // function handleCharacteristicValueChanged(event) { let value = new TextDecoder().decode(event.target.value); log(value, 'in'); }
event.target.value
is a DataView object containing an ArrayBuffer containing the message from your device. Using TextDecoder
( MDN, in English only ), we distill the byte array into text.CR
, LF
symbols. Long messages reach completely, but are broken multiple of 20 bytes.LF
, \n
). It may also be useful to remove the whitespace from the beginning and end of the message: // let readBuffer = ''; // function handleCharacteristicValueChanged(event) { let value = new TextDecoder().decode(event.target.value); for (let c of value) { if (c === '\n') { let data = readBuffer.trim(); readBuffer = ''; if (data) { receive(data); } } else { readBuffer += c; } } } // function receive(data) { log(data, 'in'); }
receive()
function to your needs, being sure that you are working with a solid message from the device.writeValue()
of the characteristic object with ArrayBuffer
as an argument. To convert the string to the ArrayBuffer
easiest to use TextEncoder
( MDN, in English only ): // function send(data) { data = String(data); if (!data || !characteristicCache) { return; } writeToCharacteristic(characteristicCache, data); log(data, 'out'); } // function writeToCharacteristic(characteristic, data) { characteristic.writeValue(new TextEncoder().encode(data)); }
String
. // function send(data) { data = String(data); if (!data || !characteristicCache) { return; } data += '\n'; if (data.length > 20) { let chunks = data.match(/(.|[\r\n]){1,20}/g); writeToCharacteristic(characteristicCache, chunks[0]); for (let i = 1; i < chunks.length; i++) { setTimeout(() => { writeToCharacteristic(characteristicCache, chunks[i]); }, i * 100); } } else { writeToCharacteristic(characteristicCache, data); } log(data, 'out'); }
\n
).CR
, \r
) ( LF
, \n
), , , 100 .<head>
.manifest.json
: { "name": "", "icons": [ ... ], "theme_color": "#ffffff", "background_color": "#ffffff", "display": "standalone" }
name
short_name
, , 12 .icons
, display
— . standalone
, - UI , — , .theme_color
, background_color
Splash screen . theme_color
, <meta name="theme-color" content="#ffffff">
.start_url
scope
./
, -, , -, - , , -.index.html
<body>
: <script src="companion.js" data-service-worker="sw.js"></script>
sw.js
index.html
, : importScripts('sw-toolbox.js'); toolbox.precache([ 'companion.js', 'index.html', 'main.js', 'styles.css', ]);
index.html
, styles.css
, main.js
sw.js
.Source: https://habr.com/ru/post/339146/
All Articles