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