Skip to content

Commit 063f981

Browse files
feat: discover wifi devices via ip scanning (#15)
1 parent 432f81b commit 063f981

File tree

5 files changed

+116
-8
lines changed

5 files changed

+116
-8
lines changed

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,45 @@ startProbingONVIFDevices()
5656
.subscribe(console.info)
5757
```
5858

59+
```js
60+
// example probe results
61+
// two cameras discovered on the network with ONVIF WS-Discovery via UDP
62+
// This will be the last emitted value in the observable until a new camera comes online
63+
// or a camera is disconnected or otherwise fails to respond to a ping.
64+
65+
[ { name: 'Amcrest',
66+
hardware: 'IP2M-8200',
67+
location: 'china',
68+
deviceServiceUri: 'http://192.168.5.191/onvif/device_service',
69+
ip: '192.168.5.191',
70+
metadataVersion: '1',
71+
urn: 'fae40e7f-91e2-489a-afe6-66e19b667952',
72+
scopes:
73+
[ 'onvif://www.onvif.org/location/country/china',
74+
'onvif://www.onvif.org/name/Amcrest',
75+
'onvif://www.onvif.org/hardware/IP2M-8200',
76+
'onvif://www.onvif.org/Profile/Streaming',
77+
'onvif://www.onvif.org/type/Network_Video_Transmitter',
78+
'onvif://www.onvif.org/extension/unique_identifier',
79+
'onvif://www.onvif.org/Profile/G' ],
80+
profiles: [ 'Streaming', 'G' ],
81+
xaddrs: [ 'http://192.168.5.191/onvif/device_service' ] },
82+
{ name: 'IPCAM',
83+
hardware: '421FZ',
84+
location: 'china',
85+
deviceServiceUri: 'http://192.168.5.13:80/onvif/device_service',
86+
ip: '192.168.5.13',
87+
metadataVersion: '1',
88+
urn: '0cbc0d5b-a7a1-47c7-bb60-85c878bb540e',
89+
scopes:
90+
[ 'onvif://www.onvif.org/Profile/Streaming',
91+
'onvif://www.onvif.org/Model/421FZ',
92+
'onvif://www.onvif.org/Name/IPCAM',
93+
'onvif://www.onvif.org/location/country/china' ],
94+
profiles: [ 'Streaming' ],
95+
xaddrs: [ 'http://192.168.5.13:80/onvif/device_service' ] } ]
96+
```
97+
5998
If you'd like to tweak default settings feel free to override in the `.run()` method.
6099

61100
```ts
@@ -73,6 +112,7 @@ probeONVIFDevices()
73112
```ts
74113
const DEFAULT_CONFIG: IProbeConfig = {
75114
PORT: 3702,
115+
ENABLE_IP_SCANNING: true,
76116
MULTICAST_ADDRESS: '239.255.255.250',
77117
PROBE_SAMPLE_TIME_MS: 2000,
78118
PROBE_SAMPLE_START_DELAY_TIME_MS: 0,

src/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ export interface IProbeConfig {
99
*/
1010
readonly PORT: number
1111

12+
/**
13+
* Enabled IP based scanning
14+
*/
15+
readonly ENABLE_IP_SCANNING: boolean
16+
1217
/**
1318
* Multicast address.
1419
*/
@@ -48,6 +53,7 @@ export interface IProbeConfig {
4853

4954
export const DEFAULT_CONFIG: IProbeConfig = {
5055
PORT: 3702,
56+
ENABLE_IP_SCANNING: true,
5157
MULTICAST_ADDRESS: '239.255.255.250',
5258
PROBE_SAMPLE_TIME_MS: 2000,
5359
PROBE_SAMPLE_START_DELAY_TIME_MS: 0,

src/index.ts

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
import { map, distinctUntilChanged, takeUntil, mergeScan } from 'rxjs/operators'
2-
import { maybe, reader, IMaybe } from 'typescript-monads'
1+
import { map, distinctUntilChanged, takeUntil, mergeScan, flatMap } from 'rxjs/operators'
2+
import { timer, Observable, forkJoin, combineLatest } from 'rxjs'
33
import { IProbeConfig, DEFAULT_CONFIG } from './config'
4-
import { timer, Observable, forkJoin } from 'rxjs'
5-
import { parseXmlResponse } from './parse'
4+
import { maybe, reader, IMaybe } from 'typescript-monads'
5+
import { parseXmlResponse, maybeIpAddress } from './parse'
66
import { socketStream } from './socket-stream'
77
import { probePayload } from './probe-payload'
88
import { IONVIFDevice } from './device'
99
export { IProbeConfig } from './config'
1010
import { MD5 } from 'object-hash'
1111
import { ping } from 'ping-rx'
12+
import { ipscan } from './ipscan'
1213
export { IONVIFDevice }
1314
export { DEFAULT_CONFIG }
1415

@@ -42,7 +43,7 @@ export const probeONVIFDevices = () => reader<Partial<IProbeConfig>, ProbeStream
4243
buffers.forEach(b => ss.socket.send(b, 0, b.length, config.PORT, config.MULTICAST_ADDRESS))
4344
})
4445

45-
return socketMessages
46+
const onvifScan = socketMessages
4647
.pipe(
4748
map(xmlResponse => xmlResponse
4849
.map(xmlString => config.DOM_PARSER.parseFromString(xmlString, 'application/xml'))
@@ -59,6 +60,29 @@ export const probeONVIFDevices = () => reader<Partial<IProbeConfig>, ProbeStream
5960
])))
6061
}, []),
6162
distinctUntilChanged((a, b) => MD5(a) === MD5(b)))
63+
64+
const ipScan = () => timer(config.PROBE_SAMPLE_START_DELAY_TIME_MS, config.PROBE_SAMPLE_TIME_MS).pipe(flatMap(() => ipscan()))
65+
66+
return !config.ENABLE_IP_SCANNING
67+
? onvifScan
68+
: combineLatest(onvifScan, ipScan(), (onvifResults, ipscanResults) => {
69+
const ipDevicesNotInOnvifScan = ipscanResults.filter(a => !onvifResults.some(onv => onv.ip === maybeIpAddress(a).valueOrUndefined()))
70+
.map<IONVIFDevice>(deviceServiceUri => {
71+
return {
72+
deviceServiceUri,
73+
name: maybeIpAddress(deviceServiceUri).valueOr(config.NOT_FOUND_STRING),
74+
hardware: config.NOT_FOUND_STRING,
75+
location: config.NOT_FOUND_STRING,
76+
ip: config.NOT_FOUND_STRING,
77+
metadataVersion: config.NOT_FOUND_STRING,
78+
urn: config.NOT_FOUND_STRING,
79+
scopes: [],
80+
profiles: [],
81+
xaddrs: []
82+
}
83+
})
84+
return [...onvifResults, ...ipDevicesNotInOnvifScan]
85+
}).pipe(distinctUntilChanged((a, b) => MD5(a) === MD5(b)))
6286
})
6387

6488
export const startProbingONVIFDevices = () => probeONVIFDevices().run({})
@@ -69,4 +93,7 @@ export const startProbingONVIFDevicesCli = () => startProbingONVIFDevices()
6993
console.log('Watching for connected ONVIF devices...', '\n')
7094
console.log(v)
7195
})
72-
96+
97+
probeONVIFDevices().run({
98+
ENABLE_IP_SCANNING: true
99+
}).subscribe(console.log)

src/ipscan.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { flatMap, map, catchError } from 'rxjs/operators'
2+
import { RxHR as http } from '@akanass/rx-http-request'
3+
import { ok, fail, maybe } from 'typescript-monads'
4+
import { forkJoin, of } from 'rxjs'
5+
import { ping } from 'ping-rx'
6+
import { networkInterfaces, NetworkInterfaceInfo } from 'os'
7+
8+
const createNumList = (num: number) => Array.from(Array(num).keys())
9+
10+
const interfaceDict = networkInterfaces()
11+
const userLocalIpNet = maybe(Object.keys(interfaceDict)
12+
.reduce((acc: ReadonlyArray<NetworkInterfaceInfo>, curr) => [...acc, ...interfaceDict[curr]], [])
13+
.filter(a => a.family === 'IPv4' && !a.internal)
14+
.pop())
15+
.map(c => c.address.split('.').slice(0, 3).join('.'))
16+
.valueOr('192.168.1')
17+
18+
export const ipscan = () => forkJoin(createNumList(256).map(num => ping(`${userLocalIpNet}.${num}`)()())).pipe(
19+
map(res => res.filter(a => a.isOk()).map(a => `http://${a.unwrap().host}/onvif/device_service`)),
20+
flatMap(urls => forkJoin(
21+
urls.map(url => http.post(url, {
22+
headers: { 'Content-Type': 'application/soap+xml; charset=utf-8;' },
23+
body: '<Envelope xmlns="http://www.w3.org/2003/05/soap-envelope"><Body xmlns:xsd="http://www.w3.org/2001/XMLSchema"><GetSystemDateAndTime xmlns="http://www.onvif.org/ver10/device/wsdl"/></Body></Envelope>'
24+
}).pipe(
25+
map(res => {
26+
return res.response.statusCode === 200
27+
? ok<string, string>(url)
28+
: fail<string, string>('')
29+
}),
30+
catchError(err => of(fail(err)))
31+
))
32+
)),
33+
map(hosts => hosts.filter(b => b.isOk()).map(z => z.unwrap())))
34+

src/parse.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const SCHEMAS = {
77
discovery: 'http://schemas.xmlsoap.org/ws/2005/04/discovery'
88
}
99

10+
export const maybeIpAddress = (str: string) => maybe(str.match(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/)).map(a => a[0])
11+
1012
export const parseXmlResponse = (doc: Document) => (config: IProbeConfig): IONVIFDevice => {
1113
const simpleParse = (elm: Document | Element) => (ns: string) => (node: string) => maybe(elm.getElementsByTagNameNS(ns, node).item(0))
1214
const maybeRootProbeElement = maybe(doc.getElementsByTagNameNS(SCHEMAS.discovery, 'ProbeMatch').item(0))
@@ -40,8 +42,7 @@ export const parseXmlResponse = (doc: Document) => (config: IProbeConfig): IONVI
4042
.find(a => a.includes(`onvif/device_service`)))
4143
.valueOr('0.0.0.0')
4244

43-
const ip = maybe(deviceServiceUri.match(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/))
44-
.map(a => a[0]).valueOr(deviceServiceUri)
45+
const ip = maybeIpAddress(deviceServiceUri).valueOr(deviceServiceUri)
4546

4647
return {
4748
name: valueFromScope('name'),

0 commit comments

Comments
 (0)