initial commit

Signed-off-by: ale <ale@manalejandro.com>
This commit is contained in:
ale 2024-12-01 16:36:36 +01:00
commit d9a76c9a9f
Signed by: ale
GPG Key ID: 244A9C4DAB1C0C81
7 changed files with 290 additions and 0 deletions

8
Dockerfile Normal file
View File

@ -0,0 +1,8 @@
FROM node:22-bookworm
RUN apt update && apt install -y bluetooth bluez libbluetooth-dev libudev-dev libcap2-bin && apt clean
RUN setcap cap_net_raw+eip $(eval readlink -f `which node`)
USER node
COPY --chown=node:node ./wble /wble
WORKDIR /wble
RUN yarn && yarn build
ENTRYPOINT ["yarn", "start"]

23
README.md Normal file
View File

@ -0,0 +1,23 @@
# wble
### Small program that scan BLE (Bluetooth Low Energy) devices using `NodeJS` and `Docker` with storage in `ElasticSearch`
## Config
edit environment variables with your settings
## Build
docker compose build
## Start
docker compose up
## Stop
docker compose down
## License
MIT

20
docker-compose.yml Normal file
View File

@ -0,0 +1,20 @@
services:
wble:
image: wble
build: ./
hostname: wble
container_name: wble
restart: always
environment:
- MACHINEID="Pi4"
- ELASTICINDEX="ble"
- SECONDS=60
- COUNTERCHANGEMAC=90
- WAITSTARTSCAN=3
- ELASTICSEARCHNODE="https://elasticsearch-node:9200"
cap_add:
- NET_ADMIN
- SYS_ADMIN
# volumes:
# - ./wble:/wble
network_mode: host

View File

@ -0,0 +1,85 @@
{
"mappings": {
"properties": {
"@timestamp": {
"type": "date"
},
"@version": {
"type": "text"
},
"address": {
"type": "keyword"
},
"addressType": {
"type": "keyword"
},
"advertisement": {
"properties": {
"localName": {
"type": "keyword"
},
"manufacturerData": {
"type": "keyword"
},
"serviceData": {
"properties": {
"data": {
"properties": {
"data": {
"type": "long"
},
"type": {
"type": "text"
}
}
},
"uuid": {
"type": "keyword"
}
}
},
"serviceSolicitationUuids": {
"type": "text"
},
"serviceUuids": {
"type": "text"
},
"txPowerLevel": {
"type": "long"
}
}
},
"connectable": {
"type": "boolean"
},
"id": {
"type": "keyword"
},
"machine": {
"type": "keyword"
},
"mtu": {
"type": "long"
},
"rssi": {
"type": "keyword"
},
"scannable": {
"type": "boolean"
},
"seen": {
"type": "date"
},
"services": {
"type": "keyword"
},
"state": {
"type": "keyword"
},
"uuid": {
"type": "text",
"fielddata": true
}
}
}
}

111
wble/index.ts Normal file
View File

@ -0,0 +1,111 @@
import { default as noble } from '@stoprocent/noble';
import randomMac from 'random-mac';
import { spawnSync } from 'node:child_process';
import { Client } from '@elastic/elasticsearch';
const machineId = process.env.MACHINEID || 'Pi4',
indexBle = process.env.ELASTICINDEX || 'ble',
seconds: any = process.env.SECONDS || 60,
counterChangeMac: any = process.env.COUNTERCHANGEMAC || 90,
waitStartScan: any = process.env.WAITSTARTSCAN || 3
let peers = [],
counter = 0
const changeMacBle = () => {
const newMac = randomMac('00:12:34').split(':').reverse().map(data => '0x' + data)
spawnSync('hcitool', ['-i', 'hci0', 'cmd', '0x3f', '0x001'].concat(newMac))
spawnSync('hciconfig', ['hci0', 'reset'])
},
transformObject = item => {
if (item && typeof item === 'object') {
Object.entries(item).map(([key, value]) => {
if (value && Buffer.isBuffer(value)) {
item[key] = Buffer.from(value).toString('hex')
} else {
item[key] = transformObject(value)
}
})
}
return item
},
mainBle = (client: Client) => {
noble.on('discover', peripheral => {
if (peripheral && peripheral.id && peripheral.uuid) {
const peer = transformObject({
id: peripheral.id,
uuid: peripheral.uuid,
address: peripheral.address,
addressType: peripheral.addressType,
connectable: peripheral.connectable,
advertisement: peripheral.advertisement,
rssi: peripheral.rssi,
services: peripheral.services,
mtu: peripheral.mtu,
state: peripheral.state,
seen: new Date(),
machine: machineId
})
peers.push(peer)
}
})
noble.on('scanStart', () => {
peers = []
setTimeout(() => {
noble.stopScanning()
}, seconds * 1000)
})
noble.on('scanStop', async () => {
if (counter >= counterChangeMac) {
changeMacBle()
counter = 0
} else {
counter++
}
const conn = await client.ping()
if (conn) {
client.helpers.bulk({
datasource: peers,
onDocument(doc) {
return {
index: { _index: indexBle }
}
}
})
} else {
throw new Error('disconnected')
}
setTimeout(() => {
noble.startScanning([], false, err => {
if (err) {
throw err
}
})
}, waitStartScan * 1000)
})
noble.on('stateChange', state => {
if (state === 'poweredOn') {
noble.startScanning([], false, err => {
if (err) {
throw err
}
})
}
})
}
(async () => {
try {
const client = new Client({ node: process.env.ELASTICSEARCHNODE || 'http://elasticsearch-node:9200' })
if (!(await client.indices.exists({ index: indexBle }))) {
await client.indices.create({
index: indexBle,
mappings: require('./ble-index-mapping.json')
})
}
changeMacBle()
mainBle(client)
} catch (e) {
console.error(e)
process.exit(1)
}
})()

20
wble/package.json Normal file
View File

@ -0,0 +1,20 @@
{
"name": "wble",
"version": "1.0.0",
"main": "index.js",
"author": "ale",
"license": "MIT",
"private": true,
"scripts": {
"build": "tsc --build",
"start": "node index.js"
},
"dependencies": {
"@elastic/elasticsearch": "^8.16.2",
"@stoprocent/noble": "^1.15.1",
"@types/node": "^20.17.9",
"random-mac": "^0.0.5",
"typescript": "^5.7.2"
},
"type": "module"
}

23
wble/tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2020", /* Specify ECMAScript target
version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES201
9', 'ES2020', 'ES2021', or 'ESNEXT'. */
"module": "es2020", /* Specify module code gener
ation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNex
t'. */
"strict": false, /* Enable all strict type-c
hecking options. */
"baseUrl": "./", /* Base directory to resolve no
n-absolute module names. */
"esModuleInterop": true, /* Enables emit interoperabi
lity between CommonJS and ES Modules via creation of namespace objects for all i
mports. Implies 'allowSyntheticDefaultImports'. */
"skipLibCheck": true, /* Skip type checking of dec
laration files. */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-
cased references to the same file. */
"moduleResolution": "node",
"allowSyntheticDefaultImports": true
}
}