initial commit
Signed-off-by: ale <ale@manalejandro.com>
This commit is contained in:
commit
d9a76c9a9f
8
Dockerfile
Normal file
8
Dockerfile
Normal 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
23
README.md
Normal 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
20
docker-compose.yml
Normal 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
|
85
wble/ble-index-mapping.json
Normal file
85
wble/ble-index-mapping.json
Normal 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
111
wble/index.ts
Normal 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
20
wble/package.json
Normal 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
23
wble/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user