Can't get notify to work in Web Bluetooth API - javascript

I'm developing a software who will use Web Bluetooth API to connect to a BT to Serial Adapter. It seems to have support for Write and Notify. But I can't get nofity to work.
The event is never fired. Right now I'm testing in Canary on Mac.
Thanks
Anders
My code for search/pair and add events:
var readCharacteristic;
var writeCharacteristic;
var serialBLEService = 'ba920001-d666-4d59-b141-05960b3a4ff7';
var txChar = 'ba920002-d666-4d59-b141-05960b3a4ff7';
var rxChar = 'ba920003-d666-4d59-b141-05960b3a4ff7';
$scope.writeToSerial = function (valueToWrite) {
var tmpValue = $('input').val();
valueToWrite = utf8AbFromStr(tmpValue);
writeCharacteristic.writeValue(valueToWrite)
.then( a => {
alert("Written: " + valueToWrite);
})
.catch(function (error) {
// And of course: error handling!
console.error('Something went wrong!', error);
});
}
function handleCharacteristicValueChanged(event) {
var value = event.target.value;
console.log('Received ' + value);
}
$scope.searchForBTDevices = function() {
navigator.bluetooth.requestDevice({
filters: [{ services: [serialBLEService] }],
optionalServices: [
serialBLEService, rxChar, txChar, configChar
]
})
.then(device => {
return device.gatt.connect();
})
.then(server => {
return server.getPrimaryService(serialBLEService);
})
.then(service => {
return service.getCharacteristics();
})
.then(characteristics => {
$scope.btResult += '>> Found Characteristics!\n';
$scope.$apply();
var queue = Promise.resolve();
characteristics.forEach(characteristic => {
switch (characteristic.uuid) {
case rxChar:
readCharacteristic = characteristic;
readCharacteristic.addEventListener('characteristicvaluechanged',
handleCharacteristicValueChanged);
break;
case txChar:
writeCharacteristic = characteristic;
break;
}
});
})
.catch(error => {
$scope.btResult = '>> Error: ' + error;
$scope.$digest();
});
};

According to https://developers.google.com/web/updates/2015/07/interact-with-ble-devices-on-the-web#receive_gatt_notifications, it looks like you need to call characteristic.startNotifications() to let the browser know you want to receive GATT notifications:
navigator.bluetooth.requestDevice({ filters: [{ services: ['heart_rate'] }] })
.then(device => device.gatt.connect())
.then(server => server.getPrimaryService('heart_rate'))
.then(service => service.getCharacteristic('heart_rate_measurement'))
.then(characteristic => characteristic.startNotifications())
.then(characteristic => {
characteristic.addEventListener('characteristicvaluechanged',
handleCharacteristicValueChanged);
console.log('Notifications have been started.');
})
.catch(error => { console.log(error); });
function handleCharacteristicValueChanged(event) {
var value = event.target.value;
console.log('Received ' + value);
// TODO: Parse Heart Rate Measurement value.
// See https://github.com/WebBluetoothCG/demos/blob/gh-pages/heart-rate-sensor/heartRateSensor.js
}

Yes, you're absolutely right. There was 2 errors first in baud rate against my serial to BT adapter which made that I didn't send what I thought I was. And the other was that startnotifications was not called.
Thanks
Anders

Related

web-bluetooth NotSupportedError: GATT operation not permitted

I am getting error code "NotSupportedError: GATT operation not permitted."
I am trying to connect to ESP32 bluetooth. I tested using nRF Connect and messages are getting to the hardware. Next I tried to use javascript and web-bluetooth. Unfortunately I am getting error on console.error('Argh! ' + error) line of code. The error is happening on characteristic.writeValue()
The code was run on https.
The code below
$(document).ready(function(){
let bluetoothDevice = null;
let requestDeviceParams = {
filters: [
{name: ["konko"]}
],
optionalServices: ['10001001-0000-1000-8000-00805f9b34fb']
}
let name = document.querySelector('#date-input')
$("#synch-date-time").click(() => {
if (document.querySelector('#date-input').value === '') {
console.error('empty date field, please fill the fields')
return
}
asyncResultNotif();
})
async function asyncResultNotif(){
return await navigator.bluetooth.requestDevice(requestDeviceParams)
.then(device => {
bluetoothDevice = device.gatt;
return device.gatt.connect();
})
.then(server => {
if (bluetoothDevice.connected) {
return server.getPrimaryService('10001001-0000-1000-8000-00805f9b34fb');
} else {
console.log('cant connect to prime server')
}
})
.then(service => {
if (bluetoothDevice.connected) {
return service.getCharacteristic('10001111-0000-1000-8000-00805f9b34fb'); // write one value
} else {
console.log('cant connect to characteristic')
}
})
.then(characteristic => {
if (bluetoothDevice.connected) {
// const resetEnergyExpended = Uint8Array.of(1);
// return characteristic.writeValue(resetEnergyExpended);
let data = '{"ssid": "' +name.value
data +='"}'
console.log(data)
let encoder = new TextEncoder('utf-8');
let val = encoder.encode(data);
// return characteristic.writeValue(val.buffer)
return characteristic.writeValue(new Uint8Array([1]))
} else {
console.log('cant send message over BLE')
}
}). then(() => {
if (bluetoothDevice.connected) {
bluetoothDevice.disconnect();
} else {
console.log('> Bluetooth Device is already disconnected');
}
}).catch(error => {
console.error('Argh! ' + error)
});
}
It could be that the Bluetooth GATT characteristic you're writing to is not writable. Can you share your ESP32 server code as well?
It should look something like below. Note that I use BLECharacteristic::PROPERTY_WRITE.
class MyCallbacks: public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic *pCharacteristic) {
std::string value = pCharacteristic->getValue();
BLECharacteristic *pCharacteristic = pService->createCharacteristic(
CHARACTERISTIC_UUID,
BLECharacteristic::PROPERTY_READ |
BLECharacteristic::PROPERTY_WRITE
);
pCharacteristic->setCallbacks(new MyCallbacks());
pCharacteristic->setValue("Hello World");
I found the answer. The problem was with UUID of characteristic. On ESP32 it was "10001111-0000-1000-8000-00805f9b34fb" and when I changed that to "10000000-0000-1000-8000-00805f9b34fb" it starts working. I somewhere read that it may be something with 128bits in UUID. We can mark with as answered.

Loop with socket function doesn't work - WebSocket

I have a socket endpoint, that I connect to and send a message to get a user.
I used this code to do it :
import generateConnection from './generate-connection';
export async function fetchUser(id: number) {
return new Promise(function (resolve) {
const connection = generateConnection();
connection.onopen = () => {
connection.send(
JSON.stringify(
'{"msg":"connect","version":"1","support":["1","pre2","pre1"]}',
),
);
connection.send(
JSON.stringify(
`{"msg":"method","id":"1","method":"Users.getUser","params":[${id}]}`,
),
);
console.log('Connected');
};
connection.on('message', async (event) => {
const data = event.toString();
if (data[0] == 'a') {
const a = JSON.parse(JSON.parse(data.substring(1))[0]);
if (a.msg == 'result') {
if ('error' in a) {
console.log('Error' + a.error.msg);
return null;
} else {
resolve(a.result);
}
}
}
});
connection.on('error', function (error) {
console.log('Connection Error: ' + error.toString());
});
connection.on('close', function () {
console.log('echo-protocol Connection Closed');
});
});
}
const fetchAllUsers = async () => {
for (let i = 0; i < 100; i++) {
const user: any = await fetchUser(i);
console.log(user.name);
}
};
fetchAllUsers();
I get the following result :
Connected
Jack
It just give me the first user and it stop on the second.
I have no control over the socket and I want to be able to fetch all 5000 users each day to be synced.
I'm Using WebSocket for this problem.
If you have any proposition other than this method, I'm all ears :D
To explain more :
1 - I want to open a connection
2 - Send a message
3 - get Result
4 - Add to Array or db
5 - When finished, close the connection.
6 - repeat
Why would you close the connection every time? An open connection allows you to send many messages. But maybe it is easier to:
connection.close()
// right before:
resolve(a.result);
If that didn't work maybe it's time to send more then one request per connection. Try this (I'm a little rusty with promises so I hope you get the idea and improve it)
import generateConnection from './generate-connection';
export async function fetchAllUsers() {
var total = 100;
var returned = 0;
var all_results = [];
return new Promise(function(resolve) {
const connection = generateConnection();
connection.onopen = () => {
connection.send(
JSON.stringify(
'{"msg":"connect","version":"1","support":["1","pre2","pre1"]}',
),
);
for (var i = 0; i < total; i++) {
connection.send(
JSON.stringify(
`{"msg":"method","id":"1","method":"Users.getUser","params":[${i}]}`,
),
);
}
console.log('Connected');
};
connection.on('message', async(event) => {
const data = event.toString();
if (data[0] == 'a') {
const a = JSON.parse(JSON.parse(data.substring(1))[0]);
if (a.msg == 'result') {
if ('error' in a) {
console.log('Error' + a.error.msg);
return null;
} else {
console.log(a.result.name);
all_results.push(a.result);
returned++;
if (returned == total) {
resolve(all_results);
}
}
}
}
});
connection.on('error', function(error) {
console.log('Connection Error: ' + error.toString());
});
connection.on('close', function() {
console.log('echo-protocol Connection Closed');
});
});
}
fetchAllUsers();

Does anyone know why res.download is giving my download file a random name each time?

I'm using express 4.17.1. When I try to use res.download to send a csv file to the browser, the file is downloaded but the file name is something like this: 3d6a8bc1-696c-40f2-bae8-29ca69658534.csv
Then, when I attempt to download the same file again, it will send the file under this name: c1cd40ff-ea9d-4327-9389-9768fb53384a.csv
Each time it is a different random string of characters.
My code is simply this:
res.download(filePath, 'list.csv');
The filePath is this: ./downloadables/mail-list-14da.csv
I've tried using sendFile but got the same result. I recently updated from a previous version of express to see if it would automagically resolve this issue but it is still doing this.
EDIT: More Code Below as Requested
Here is the entirety of the request endpoint:
/*
* Download the list specified by the list_id with the appropriate fields as specified by the
* list_type parameter.
*/
router.get('/download/:list_type/:list_id', authCheck('list'), function(
req,
res,
next
) {
let listData = {};
Voter.aggregate(aggrPipelineList(req.params.list_type, req.params.list_id))
.allowDiskUse(true)
.exec()
.then(voterDocs => {
if (voterDocs && voterDocs.length === 0) {
res.status(404).json({
message: `list_id ${req.params.list_id} not found`
});
} else {
listData.voter_docs = voterDocs;
return req.params.list_type;
}
})
.then(listType => {
if (listType === 'mail') {
return generateMailExportFile(req.params.list_id, listData);
} else if (listType == 'phone') {
return generateCallExportFile(req.params.list_id, listData);
} else {
return generateFacebookExportFile(req.params.list_id, listData);
}
})
.then(filePath => {
console.log('FP: ' + filePath);
res.download(filePath, 'list.csv');
})
.catch(err => {
res.status(500).json({ message: err.message }); // #note: added message
});
});
Also including the generateMailExportFile function for completeness. Yes, I know I can refactor the three generate export file functions. It's on my list... I originally wrote this before I knew what the hell I was doing.
generateMailExportFile = function(listID, listData) {
let fields = [
'First Name',
'Last Name',
'Suffix',
'Street Address 1',
'Street Address 2',
'City',
'State',
'Zip'
];
let fileName = 'mail-list-' + listID.substr(listID.length - 4) + '.csv';
let voterRows = buildVoterRowsForMailList(listData.voter_docs);
let csv = json2csv({ data: voterRows, fields: fields });
let tempServerFilePath = './downloadables/' + fileName;
return new Promise((resolve, reject) => {
fs.writeFile(tempServerFilePath, csv, function(err) {
if (err) {
reject(err);
} else {
resolve(tempServerFilePath);
}
});
});
};
Here is the redux/thunk function that requests the file download:
export const downloadList = (listId, type) => {
return (dispatch, getState) => {
const rshttp = new RSHttp(getState);
rshttp
.get('/list/download/' + type + '/' + listId)
.then(response => {
let file = new Blob([response.data], { type: 'text/csv' }),
url = window.URL.createObjectURL(file);
window.open(url);
})
.catch(error => {
console.log('Error Downloading File: ' + JSON.stringify(error));
});
};
};
I hadn't before thought about the problem being on the react side. If I find an answer, I'll update this question. Any thoughts are still greatly appreciated!
The problem is you are recreating the file on your front-end. You can simply change your React code to:
export const downloadList = (listId, type) => {
return (dispatch, getState) => {
window.open('http://localhost:8080/list/download/' + type + '/' + listId', '_blank')
// Replace with your backend URL :)
};
};
and it will download the file with the correct filename.

PeerConnection create answer

I would like create WebRTC connection on 2 devices.
On my first device (Initiator), i create my Offer with this method :
createOffer() {
const { pc } = this;
pc.onnegotiationneeded = () => {
pc.createOffer()
.then(offer => pc.setLocalDescription(offer))
.then(() => {
this.setState({
offer: JSON.stringify(pc.localDescription)
});
});
};
}
And on my second device (Receiver), i create the answer :
createAnswer() {
const { pc } = this;
const { dataOffer } = this.state;
if (dataOffer) {
const sd = new RTCSessionDescription(JSON.parse(dataOffer));
pc.setRemoteDescription(sd)
.then(() => pc.createAnswer())
.then(answer => {
this.setState({
offer: JSON.stringify(answer)
});
return pc.setLocalDescription(answer);
});
}
}
But, after send Answer on first device, i've this error : PeerConnection cannot create an answer in a state other than have-remote-offer or have-local-pranswer.
When Initiator receive the answer, i run createAnswer method with answer data, That may be the problem ?
I don't really understand what method/event i need use after receive the answer :/
EDIT with new method for Initiator device :
receiveAnswer() {
const { pc } = this;
const { dataOffer } = this.state;
const sd = new RTCSessionDescription(JSON.parse(dataOffer));
pc.setRemoteDescription(sd);
}
But the connection state stay on checking :(
You can see my componentDidMount :
componentDidMount() {
const { pc } = this;
pc.oniceconnectionstatechange = () => {
this.setState({
connectionState: pc.iceConnectionState
});
};
pc.onaddstream = ({ stream }) => {
if (stream) {
this.setState({
receiverVideoURL: stream.toURL()
});
}
};
}

How to pull out handler using module exports?

I am building a node application, and trying to neatly organize my code. I wrote a serial module that imports the serial libs and handles the connection. My intention was to write a basic module and then reuse it over and over again in different projects as needed. The only part that changes per use is how the incoming serial data is handled. For this reason I would like to pull out following handler and redefine it as per the project needs. How can I use module exports to redefine only this section of the file?
I have tried added myParser to exports, but that gives me a null and I would be out of scope.
Handler to redefine/change/overload for each new project
myParser.on('data', (data) => {
console.log(data)
//DO SOMETHING WITH DATA
});
Example usage: main.js
const serial = require('./serial');
const dataParser = require('./dataParser');
const serial = require('./serial');
//call connect with CL args
serial.connect(process.argv[2], Number(process.argv[3]))
serial.myParser.on('data',(data) => {
//Do something unique with data
if (dataParser.parse(data) == 0)
serial.send('Error');
});
Full JS Module below serial.js
const SerialPort = require('serialport');
const ReadLine = require('#serialport/parser-readline');
const _d = String.fromCharCode(13); //char EOL
let myPort = null;
let myParser = null;
function connect(port, baud) {
let portName = port || `COM1`;
let baudRate = baud || 115200;
myPort = new SerialPort(portName, {baudRate: baudRate})
myParser = myPort.pipe(new ReadLine({ delimiter: '\n'}))
//Handlers
myPort.on('open', () => {
console.log(`port ${portName} open`)
});
myParser.on('data', (data) => {
console.log(data)
});
myPort.on('close', () => {
console.log(`port ${portName} closed`)
});
myPort.on('error', (err) => {
console.error('port error: ' + err)
});
}
function getPorts() {
let portlist = [];
SerialPort.list((err, ports) => {
ports.forEach(port => {
portlist.push(port.comName)
});
})
return portlist;
}
function send(data) {
myPort.write(JSON.stringify(data) + _d, function (err) {
if (err) {
return console.log('Error on write: ', err.message);
}
console.log(`${data} sent`);
});
}
function close() {
myPort.close();
}
module.exports = {
connect, getPorts, send, close
}
The problem is that a module is used where a class or a factory would be appropriate. myParser cannot exist without connect being called, so it doesn't make sense to make it available as module property, it would be unavailable by default, and multiple connect calls would override it.
It can be a factory:
module.exports = function connect(port, baud) {
let portName = port || `COM1`;
let baudRate = baud || 115200;
let myPort = new SerialPort(portName, {baudRate: baudRate})
let myParser = myPort.pipe(new ReadLine({ delimiter: '\n'}))
//Handlers
myPort.on('open', () => {
console.log(`port ${portName} open`)
});
myParser.on('data', (data) => {
console.log(data)
});
myPort.on('close', () => {
console.log(`port ${portName} closed`)
});
myPort.on('error', (err) => {
console.error('port error: ' + err)
});
function getPorts() {
let portlist = [];
SerialPort.list((err, ports) => {
ports.forEach(port => {
portlist.push(port.comName)
});
})
return portlist;
}
function send(data) {
myPort.write(JSON.stringify(data) + _d, function (err) {
if (err) {
return console.log('Error on write: ', err.message);
}
console.log(`${data} sent`);
});
}
function close() {
myPort.close();
}
return {
myParser, getPorts, send, close
};
}
So it could be used like:
const serial = require('./serial');
const connection = serial(...);
connection.myParser.on('data',(data) => {
//Do something unique with data
if (dataParser.parse(data) == 0)
connection.send('Error');
});

Categories