├── .gitignore
├── package.json
├── UNLICENSE
├── README.md
└── foscam.js
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | package-lock.json
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": {
3 | "name": "Franklin",
4 | "email": "fr@nkl.in",
5 | "url": "https://frankl.in"
6 | },
7 | "name": "foscam",
8 | "description": "Remote control, view and config a Foscam/Tenvis IP camera",
9 | "version": "0.2.2",
10 | "repository": {
11 | "type": "git",
12 | "url": "git://github.com/fvdm/nodejs-foscam.git"
13 | },
14 | "bugs": {
15 | "url": "https://github.com/fvdm/nodejs-foscam/issues"
16 | },
17 | "main": "foscam.js",
18 | "dependencies": {},
19 | "devDependencies": {
20 | "dotest": "^2"
21 | },
22 | "engines": {
23 | "node": ">=18"
24 | },
25 | "keywords": [
26 | "camera",
27 | "foscam",
28 | "ipcam",
29 | "tenvis",
30 | "webcam"
31 | ],
32 | "license": "Unlicense"
33 | }
34 |
--------------------------------------------------------------------------------
/UNLICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # foscam
2 |
3 | Remote control, view and config a Foscam/Tenvis IP camera.
4 |
5 | All included methods are based on Foscam's (fragmented) API documentation.
6 | Some features may not be supported by non-pan/tilt, older cameras or old firmware.
7 | So make sure you keep a backup of your camera settings, just in case.
8 |
9 |
10 | ## Requirements
11 |
12 | - Node.js 18 or newer (uses native `fetch()`)
13 |
14 |
15 | ## Usage
16 |
17 | All methods support both **Promises** and **callbacks**.
18 | When no callback is provided, a Promise is returned.
19 |
20 | ### Promise style (async/await)
21 |
22 | ```js
23 | const cam = require( 'foscam' );
24 |
25 | cam.setup( {
26 | host: 'mycamera.lan',
27 | port: 81,
28 | user: 'admin',
29 | pass: '',
30 | } );
31 |
32 | // Using async/await
33 | const status = await cam.status();
34 | console.log( status );
35 |
36 | // Take a snapshot
37 | await cam.control.decoder( 'left' );
38 | await cam.control.decoder( 'stop left' );
39 | const filepath = await cam.snapshot( '/path/to/save.jpg' );
40 | console.log( filepath );
41 | ```
42 |
43 | ### Callback style
44 |
45 | ```js
46 | const cam = require( 'foscam' );
47 |
48 | cam.setup( {
49 | host: 'mycamera.lan',
50 | port: 81,
51 | user: 'admin',
52 | pass: '',
53 | } );
54 |
55 | // start rotating left
56 | cam.control.decoder( 'left', () => {
57 |
58 | // stop rotation
59 | cam.control.decoder( 'stop left', () => {
60 |
61 | // take a picture and store it on your computer
62 | cam.snapshot( '/path/to/save.jpg', console.log );
63 |
64 | } );
65 | } );
66 | ```
67 |
68 |
69 | ## Installation
70 |
71 | Stable: `npm install foscam`
72 |
73 | Develop: `npm install fvdm/nodejs-foscam#develop`
74 |
75 |
76 | ## Methods
77 |
78 | Every method takes an optional `callback` function as last parameter.
79 | When no callback is provided, the method returns a **Promise**.
80 |
81 | **NOTE:** Some methods require a certain access-level, i.e. *admins* can do everything, but a *visitor* can only view.
82 |
83 |
84 | ### Basic
85 |
86 | ### setup
87 | #### ( properties, [callback] )
88 |
89 | In order to connect to the camera you first need to provide its access details. You can either do this by setting the properties below directly in `cam.settings`, but better is to use `cam.setup()`. When the `callback` function is provided, `setup()` will attempt to connect to the camera and retrieve its status, returned as object to the callback. When it fails the callback gets **false**.
90 |
91 |
92 | name | type | default | description
93 | --------|--------|---------------|----------------------
94 | host | string | 192.168.1.239 | Camera IP or hostname
95 | port | number | 81 | Camera port number
96 | user | string | admin | Username
97 | pass | string | | Password
98 | timeout | number | 5000 | Request timeout in ms
99 |
100 |
101 | ```js
102 | // Promise style
103 | const status = await cam.setup( {
104 | host: 'mycamera.lan',
105 | port: 81,
106 | user: 'admin',
107 | pass: '',
108 | } );
109 |
110 | if ( !status ) {
111 | console.error( 'ERROR: can\'t connect' );
112 | }
113 | else {
114 | console.log( status );
115 | }
116 | ```
117 |
118 | ```js
119 | // Callback style
120 | cam.setup(
121 | {
122 | host: 'mycamera.lan',
123 | port: 81,
124 | user: 'admin',
125 | pass: '',
126 | },
127 | ( status ) => {
128 | if ( !status ) {
129 | console.error( 'ERROR: can\'t connect' );
130 | }
131 | else {
132 | console.log( status );
133 | }
134 | }
135 | );
136 | ```
137 |
138 | ### status
139 | #### ( [callback] )
140 |
141 | **Permission: everyone**
142 |
143 | Get basic details from the camera.
144 |
145 | ```js
146 | // Promise
147 | const result = await cam.status();
148 | console.log( result );
149 |
150 | // Callback
151 | cam.status( console.log );
152 | ```
153 |
154 | ```js
155 | { id: '001A11A00A0B',
156 | sys_ver: '0.37.2.36',
157 | app_ver: '3.2.2.18',
158 | alias: 'Cam1',
159 | now: '1343304558',
160 | tz: '-3600',
161 | alarm_status: '0',
162 | ddns_status: '0',
163 | ddns_host: '',
164 | oray_type: '0',
165 | upnp_status: '0',
166 | p2p_status: '0',
167 | p2p_local_port: '23505',
168 | msn_status: '0',
169 | alarm_status_str: 'no alarm',
170 | ddns_status_str: 'No Action',
171 | upnp_status_str: 'No Action' }
172 | ```
173 |
174 | ### camera_params
175 | #### ( [callback] )
176 |
177 | **Permission: visitor**
178 |
179 | Get camera sensor settings.
180 |
181 | ```js
182 | // Promise
183 | const params = await cam.camera_params();
184 | console.log( params );
185 |
186 | // Callback
187 | cam.camera_params( console.log );
188 | ```
189 |
190 | ```js
191 | { resolution: 32,
192 | brightness: 96,
193 | contrast: 4,
194 | mode: 1,
195 | flip: 0,
196 | fps: 0 }
197 | ```
198 |
199 | ### Camera
200 |
201 | ### snapshot
202 | #### ( [filename], [callback] )
203 |
204 | Take a snapshot. Either receive the **binary JPEG** in the `callback` or specify a `filename` to store it on your computer.
205 |
206 | When a `filename` is provided the callback will return either the *filename* on success or *false* on failure.
207 |
208 | ```js
209 | // Promise - custom processing
210 | const jpeg = await cam.snapshot();
211 | // add binary processing here
212 |
213 | // Promise - store locally
214 | const filepath = await cam.snapshot( './my_view.jpg' );
215 | console.log( filepath );
216 |
217 | // Callback - custom processing
218 | cam.snapshot( ( jpeg ) => {
219 | // add binary processing here
220 | } );
221 |
222 | // Callback - store locally
223 | cam.snapshot( './my_view.jpg', console.log );
224 | ```
225 |
226 |
227 | ### preset.set
228 | #### ( id, [cb] )
229 |
230 | Save current camera position in preset #`id`. You can set presets 1 to 16.
231 |
232 | ```js
233 | // Promise
234 | await cam.preset.set( 3 );
235 |
236 | // Callback
237 | cam.preset.set( 3, console.log );
238 | ```
239 |
240 |
241 | ### preset.go
242 | #### ( id, [cb] )
243 |
244 | Move camera to the position as stored in preset #`id`. You can use presets 1 to 16.
245 |
246 | ```js
247 | // Promise
248 | await cam.preset.go( 3 );
249 |
250 | // Callback
251 | cam.preset.go( 3, console.log );
252 | ```
253 |
254 |
255 | ### control.decoder
256 | #### ( command, [callback] )
257 |
258 | Control camera movement, like pan and tilt.
259 |
260 | The `command` to execute can be a string or number.
261 |
262 |
263 | command | description
264 | -----------------------|------------------
265 | up | start moving up
266 | stop up | stop moving up
267 | down | start moving down
268 | stop down | stop moving down
269 | left | start moving left
270 | stop left | stop moving left
271 | right | start moving right
272 | stop right | stop moving right
273 | center | move to center
274 | vertical patrol | start moving y-axis
275 | stop vertical patrol | stop moving y-axis
276 | horizontal patrol | start moving x-axis
277 | stop horizontal patrol | stop moving x-axis
278 | io output high | iR on _(some cameras)_
279 | io output low | iR off _(some camera)_
280 |
281 |
282 | ```js
283 | // Promise
284 | await cam.control.decoder( 'horizontal patrol' );
285 | console.log( 'Camera moving left-right' );
286 |
287 | // Callback
288 | cam.control.decoder( 'horizontal patrol', () => {
289 | console.log( 'Camera moving left-right' );
290 | } );
291 | ```
292 |
293 |
294 | ### control.camera
295 | #### ( name, value, [callback] )
296 |
297 | Change a camera (sensor) setting.
298 |
299 |
300 | name | value
301 | -----------|-----------------------------------
302 | resolution | `240` (320x240) or `480` (640x480)
303 | brightness | `0` to `255`
304 | contrast | `0` to `6`
305 | mode | `50` Hz, `60` Hz or `outdoor`
306 | flipmirror | `default`, `flip`, `mirror` or `flipmirror`
307 |
308 |
309 | ```js
310 | // Promise
311 | await cam.control.camera( 'resolution', 640 );
312 | console.log( 'Resolution changed to 640x480' );
313 |
314 | // Callback
315 | cam.control.camera( 'resolution', 640, () => {
316 | console.log( 'Resolution changed to 640x480' );
317 | } );
318 | ```
319 |
320 |
321 | ### System
322 |
323 | ### reboot
324 | #### ( [callback] )
325 |
326 | Reboot the device
327 |
328 | ```js
329 | // Promise
330 | await cam.reboot();
331 | console.log( 'Rebooting camera' );
332 |
333 | // Callback
334 | cam.reboot( () => {
335 | console.log( 'Rebooting camera' );
336 | } );
337 | ```
338 |
339 |
340 | ### restore_factory
341 | #### ( [callback] )
342 |
343 | Reset all settings back to their factory values.
344 |
345 | ```js
346 | // Promise
347 | await cam.restore_factory();
348 | console.log( 'Resetting camera settings to factory defaults' );
349 |
350 | // Callback
351 | cam.restore_factory( () => {
352 | console.log( 'Resetting camera settings to factory defaults' );
353 | } );
354 | ```
355 |
356 |
357 | ### talk
358 | #### ( propsObject )
359 |
360 | Directly communicate with the device.
361 |
362 |
363 | property | type | required | value
364 | ---------|----------|----------|----------------------
365 | path | string | yes | i.e. `get_params.cgi`
366 | fields | object | no | i.e. `{ ntp_enable: 1, ntp_svr: 'ntp.xs4all.nl' }`
367 | encoding | string | no | `binary` or `utf8` (default)
368 | callback | function | no | i.e. `( response ) => {}`
369 | timeout | number | no | Request timeout in ms (default: 5000)
370 |
371 |
372 | ```js
373 | // Promise
374 | const response = await cam.talk( {
375 | path: 'set_datetime.cgi',
376 | fields: {
377 | ntp_enable: 1,
378 | ntp_svr: 'ntp.xs4all.nl',
379 | tz: -3600,
380 | },
381 | } );
382 | console.log( response );
383 |
384 | // Callback
385 | cam.talk( {
386 | path: 'set_datetime.cgi',
387 | fields: {
388 | ntp_enable: 1,
389 | ntp_svr: 'ntp.xs4all.nl',
390 | tz: -3600,
391 | },
392 | callback: ( response ) => {
393 | console.log( response );
394 | },
395 | } );
396 | ```
397 |
398 |
399 | ## Unlicense
400 |
401 | This is free and unencumbered software released into the public domain.
402 |
403 | Anyone is free to copy, modify, publish, use, compile, sell, or
404 | distribute this software, either in source code form or as a compiled
405 | binary, for any purpose, commercial or non-commercial, and by any
406 | means.
407 |
408 | In jurisdictions that recognize copyright laws, the author or authors
409 | of this software dedicate any and all copyright interest in the
410 | software to the public domain. We make this dedication for the benefit
411 | of the public at large and to the detriment of our heirs and
412 | successors. We intend this dedication to be an overt act of
413 | relinquishment in perpetuity of all present and future rights to this
414 | software under copyright law.
415 |
416 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
417 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
418 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
419 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
420 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
421 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
422 | OTHER DEALINGS IN THE SOFTWARE.
423 |
424 | For more information, please refer to
425 |
426 |
427 | ## Author
428 |
429 | Franklin van de Meent
430 | | [Website](https://frankl.in)
431 | | [Github](https://github.com/fvdm)
432 |
--------------------------------------------------------------------------------
/foscam.js:
--------------------------------------------------------------------------------
1 | /*
2 | Name: foscam
3 | Description: Remote control a Foscam/Tenvis IP camera
4 | Framework: node.js
5 | Author: Franklin van de Meent (https://frankl.in)
6 | Source & docs: https://github.com/fvdm/nodejs-foscam
7 | Feedback: https://github.com/fvdm/nodejs-foscam/issues
8 | License: Unlicense (public domain) - see LICENSE file
9 | */
10 |
11 | const fs = require( 'fs' );
12 | const { EventEmitter } = require( 'events' );
13 | const app = new EventEmitter();
14 |
15 | // defaults
16 | app.settings = {
17 | host: '192.168.1.239',
18 | port: 81,
19 | user: 'admin',
20 | pass: '',
21 | timeout: 5000,
22 | };
23 |
24 | // overrides
25 | app.setup = function( props, cb ) {
26 | for ( const key in props ) {
27 | app.settings[key] = props[key];
28 | }
29 |
30 | if ( typeof cb === 'function' ) {
31 | return app.status( cb );
32 | }
33 | };
34 |
35 |
36 | // status
37 | app.status = function( cb ) {
38 | const processData = ( data ) => {
39 | const result = {};
40 | const lines = data.split( '\n' );
41 |
42 | for ( const line of lines ) {
43 | if ( line !== '' ) {
44 | const parts = line.split( 'var ' );
45 | const parsed = String( parts[1] ).split( '=' );
46 | parsed[1] = String( parsed[1] ).replace( /;$/, '' );
47 | result[parsed[0]] = parsed[1].substring( 0, 1 ) === '\'' ? parsed[1].substring( 1, parsed[1].length - 2 ) : parsed[1];
48 | }
49 | }
50 |
51 | if ( result.alarm_status ) {
52 | switch ( result.alarm_status ) {
53 | case '0': result.alarm_status_str = 'no alarm'; break;
54 | case '1': result.alarm_status_str = 'motion alarm'; break;
55 | case '2': result.alarm_status_str = 'input alarm'; break;
56 | }
57 | }
58 |
59 | if ( result.ddns_status ) {
60 | switch ( result.ddns_status ) {
61 | case '0': result.ddns_status_str = 'No Action'; break;
62 | case '1': result.ddns_status_str = 'It\'s connecting...'; break;
63 | case '2': result.ddns_status_str = 'Can\'t connect to the Server'; break;
64 | case '3': result.ddns_status_str = 'Dyndns Succeed'; break;
65 | case '4': result.ddns_status_str = 'DynDns Failed: Dyndns.org Server Error'; break;
66 | case '5': result.ddns_status_str = 'DynDns Failed: Incorrect User or Password'; break;
67 | case '6': result.ddns_status_str = 'DynDns Failed: Need Credited User'; break;
68 | case '7': result.ddns_status_str = 'DynDns Failed: Illegal Host Format'; break;
69 | case '8': result.ddns_status_str = 'DynDns Failed: The Host Does not Exist'; break;
70 | case '9': result.ddns_status_str = 'DynDns Failed: The Host Does not Belong to You'; break;
71 | case '10': result.ddns_status_str = 'DynDns Failed: Too Many or Too Few Hosts'; break;
72 | case '11': result.ddns_status_str = 'DynDns Failed: The Host is Blocked for Abusing'; break;
73 | case '12': result.ddns_status_str = 'DynDns Failed: Bad Reply from Server'; break;
74 | case '13': result.ddns_status_str = 'DynDns Failed: Bad Reply from Server'; break;
75 | case '14': result.ddns_status_str = 'Oray Failed: Bad Reply from Server'; break;
76 | case '15': result.ddns_status_str = 'Oray Failed: Incorrect User or Password'; break;
77 | case '16': result.ddns_status_str = 'Oray Failed: Incorrect Hostname'; break;
78 | case '17': result.ddns_status_str = 'Oray Succeed'; break;
79 | case '18': result.ddns_status_str = 'Reserved'; break;
80 | case '19': result.ddns_status_str = 'Reserved'; break;
81 | case '20': result.ddns_status_str = 'Reserved'; break;
82 | case '21': result.ddns_status_str = 'Reserved'; break;
83 | }
84 | }
85 |
86 | if ( result.upnp_status ) {
87 | switch ( result.upnp_status ) {
88 | case '0': result.upnp_status_str = 'No Action'; break;
89 | case '1': result.upnp_status_str = 'Succeed'; break;
90 | case '2': result.upnp_status_str = 'Device System Error'; break;
91 | case '3': result.upnp_status_str = 'Errors in Network Communication'; break;
92 | case '4': result.upnp_status_str = 'Errors in Chat with UPnP Device'; break;
93 | case '5': result.upnp_status_str = 'Rejected by UPnP Device, Maybe Port Conflict'; break;
94 | }
95 | }
96 |
97 | return result;
98 | };
99 |
100 | if ( typeof cb === 'function' ) {
101 | app.talk( {
102 | path: 'get_status.cgi',
103 | callback: ( data ) => cb( processData( data ) )
104 | } );
105 | return;
106 | }
107 |
108 | return app.talk( { path: 'get_status.cgi' } ).then( processData );
109 | };
110 |
111 |
112 | // camera params
113 | app.camera_params = function( cb ) {
114 | const processData = ( data ) => {
115 | const result = {};
116 | data.replace( /var ([^=]+)=([^;]+);/g, ( str, key, value ) => {
117 | const parsed = parseInt( value, 10 );
118 | result[key] = isNaN( parsed ) ? 0 : parsed;
119 | } );
120 | return result;
121 | };
122 |
123 | if ( typeof cb === 'function' ) {
124 | app.talk( {
125 | path: 'get_camera_params.cgi',
126 | callback: ( data ) => cb( processData( data ) )
127 | } );
128 | return;
129 | }
130 |
131 | return app.talk( { path: 'get_camera_params.cgi' } ).then( processData );
132 | };
133 |
134 |
135 | // Presets
136 | app.preset = {
137 | id2cmd: function( action, id ) {
138 | const cmds = {
139 | set: [30, 32, 34, 36, 38, 40, 42, 44, 46, 48, 50, 52, 54, 56, 58, 60],
140 | go: [31, 33, 35, 37, 39, 41, 43, 45, 47, 49, 51, 53, 55, 57, 59, 61]
141 | };
142 | return cmds[action][id - 1];
143 | },
144 |
145 | set: function( id, cb ) {
146 | return app.control.decoder( app.preset.id2cmd( 'set', id ), cb );
147 | },
148 |
149 | go: function( id, cb ) {
150 | return app.control.decoder( app.preset.id2cmd( 'go', id ), cb );
151 | }
152 | };
153 |
154 |
155 | // control
156 | app.control = {
157 |
158 | // pan/tilt
159 | decoder: function( cmd, cb ) {
160 | let command = cmd;
161 |
162 | if ( typeof command === 'string' && !command.match( /^[0-9]+$/ ) ) {
163 | switch ( command ) {
164 | case 'up': command = 0; break;
165 | case 'stop up': command = 1; break;
166 | case 'down': command = 2; break;
167 | case 'stop down': command = 3; break;
168 | case 'left': command = 4; break;
169 | case 'stop left': command = 5; break;
170 | case 'right': command = 6; break;
171 | case 'stop right': command = 7; break;
172 | case 'center': command = 25; break;
173 | case 'vertical patrol': command = 26; break;
174 | case 'stop vertical patrol': command = 27; break;
175 | case 'horizontal patrol': command = 28; break;
176 | case 'stop horizontal patrol': command = 29; break;
177 | case 'io output high': command = 94; break;
178 | case 'io output low': command = 95; break;
179 | }
180 | }
181 |
182 | if ( typeof cb === 'function' ) {
183 | app.talk( {
184 | path: 'decoder_control.cgi',
185 | fields: { command },
186 | callback: cb
187 | } );
188 | return;
189 | }
190 |
191 | return app.talk( {
192 | path: 'decoder_control.cgi',
193 | fields: { command }
194 | } );
195 | },
196 |
197 | // camera settings
198 | camera: function( param, value, cb ) {
199 | let paramVal = param;
200 | let valueVal = value;
201 |
202 | // fix param
203 | if ( typeof paramVal === 'string' && !paramVal.match( /^[0-9]+$/ ) ) {
204 | switch ( paramVal ) {
205 |
206 | case 'brightness': paramVal = 1; break;
207 | case 'contrast': paramVal = 2; break;
208 |
209 | // resolution
210 | case 'resolution':
211 | paramVal = 0;
212 | if ( typeof valueVal === 'string' && !valueVal.match( /^[0-9]{1,2}$/ ) ) {
213 | switch ( valueVal ) {
214 | case '320':
215 | case '320x240':
216 | case '320*240':
217 | valueVal = 8;
218 | break;
219 |
220 | case '640':
221 | case '640x480':
222 | case '640*480':
223 | valueVal = 32;
224 | break;
225 | }
226 | }
227 | break;
228 |
229 | case 'mode':
230 | paramVal = 3;
231 | if ( typeof valueVal === 'string' && !valueVal.match( /^[0-9]$/ ) ) {
232 | switch ( valueVal.toLowerCase() ) {
233 | case '50':
234 | case '50hz':
235 | case '50 hz':
236 | valueVal = 0;
237 | break;
238 |
239 | case '60':
240 | case '60hz':
241 | case '60 hz':
242 | valueVal = 1;
243 | break;
244 |
245 | case 'outdoor':
246 | case 'outside':
247 | valueVal = 2;
248 | break;
249 | }
250 | }
251 | break;
252 |
253 | case 'flipmirror':
254 | paramVal = 5;
255 | if ( typeof valueVal === 'string' && !valueVal.match( /^[0-9]$/ ) ) {
256 | switch ( valueVal.toLowerCase() ) {
257 | case 'default':
258 | valueVal = 0;
259 | break;
260 |
261 | case 'flip':
262 | valueVal = 1;
263 | break;
264 |
265 | case 'mirror':
266 | valueVal = 2;
267 | break;
268 |
269 | case 'flipmirror':
270 | case 'flip&mirror':
271 | case 'flip+mirror':
272 | case 'flip + mirror':
273 | case 'flip & mirror':
274 | valueVal = 3;
275 | break;
276 | }
277 | }
278 | break;
279 | }
280 | }
281 |
282 | // send it
283 | if ( typeof cb === 'function' ) {
284 | app.talk( {
285 | path: 'camera_control.cgi',
286 | fields: {
287 | param: paramVal,
288 | value: valueVal
289 | },
290 | callback: cb
291 | } );
292 | return;
293 | }
294 |
295 | return app.talk( {
296 | path: 'camera_control.cgi',
297 | fields: {
298 | param: paramVal,
299 | value: valueVal
300 | }
301 | } );
302 | }
303 | };
304 |
305 |
306 | // reboot
307 | app.reboot = function( cb ) {
308 | if ( typeof cb === 'function' ) {
309 | app.talk( {
310 | path: 'reboot.cgi',
311 | callback: cb
312 | } );
313 | return;
314 | }
315 |
316 | return app.talk( { path: 'reboot.cgi' } );
317 | };
318 |
319 |
320 | // restore factory
321 | app.restore_factory = function( cb ) {
322 | if ( typeof cb === 'function' ) {
323 | app.talk( {
324 | path: 'restore_factory.cgi',
325 | callback: cb
326 | } );
327 | return;
328 | }
329 |
330 | return app.talk( { path: 'restore_factory.cgi' } );
331 | };
332 |
333 |
334 | // params
335 | app.params = function( cb ) {
336 | if ( typeof cb === 'function' ) {
337 | app.talk( {
338 | path: 'get_params.cgi',
339 | callback: cb
340 | } );
341 | return;
342 | }
343 |
344 | return app.talk( { path: 'get_params.cgi' } );
345 | };
346 |
347 |
348 | // set
349 | app.set = {
350 |
351 | // alias
352 | alias: function( alias, cb ) {
353 | if ( typeof cb === 'function' ) {
354 | app.talk( {
355 | path: 'set_alias.cgi',
356 | fields: { alias },
357 | callback: cb
358 | } );
359 | return;
360 | }
361 |
362 | return app.talk( {
363 | path: 'set_alias.cgi',
364 | fields: { alias }
365 | } );
366 | },
367 |
368 | // datetime
369 | datetime: function( props, cb ) {
370 | if ( typeof cb === 'function' ) {
371 | app.talk( {
372 | path: 'set_datetime.cgi',
373 | fields: props,
374 | callback: cb
375 | } );
376 | return;
377 | }
378 |
379 | return app.talk( {
380 | path: 'set_datetime.cgi',
381 | fields: props
382 | } );
383 | }
384 | };
385 |
386 |
387 | // snapshot
388 | app.snapshot = function( filepath, cb ) {
389 | let callback = cb;
390 | let savePath = filepath;
391 |
392 | if ( !callback && typeof filepath === 'function' ) {
393 | callback = filepath;
394 | savePath = false;
395 | }
396 |
397 | const processData = async ( bin ) => {
398 | if ( savePath ) {
399 | await fs.promises.writeFile( savePath, bin );
400 | return savePath;
401 | }
402 | return bin;
403 | };
404 |
405 | if ( typeof callback === 'function' ) {
406 | app.talk( {
407 | path: 'snapshot.cgi',
408 | encoding: 'binary',
409 | callback: async ( bin ) => {
410 | try {
411 | const result = await processData( bin );
412 | callback( result );
413 | }
414 | catch ( err ) {
415 | app.emit( 'connection-error', err );
416 | callback( false );
417 | }
418 | }
419 | } );
420 | return;
421 | }
422 |
423 | return app.talk( {
424 | path: 'snapshot.cgi',
425 | encoding: 'binary'
426 | } ).then( processData );
427 | };
428 |
429 |
430 | // communicate
431 | app.talk = function( {
432 |
433 | path,
434 | fields = {},
435 | encoding = '',
436 | callback = false,
437 | timeout = app.settings.timeout,
438 |
439 | } ) {
440 |
441 | fields.user = app.settings.user;
442 | fields.pwd = app.settings.pass;
443 |
444 | const queryParams = new URLSearchParams( fields ).toString();
445 | const url = `http://${app.settings.host}:${app.settings.port}/${path}?${queryParams}`;
446 |
447 | const options = {
448 | timeout: AbortSignal.timeout( parseInt( timeout, 10 ) ),
449 | };
450 |
451 | const fetchData = async () => {
452 | try {
453 | const response = await fetch( url, options );
454 |
455 | if ( !response.ok ) {
456 | throw new Error( `HTTP error! status: ${response.status}` );
457 | }
458 |
459 | let data;
460 | if ( encoding === 'binary' ) {
461 | const buffer = await response.arrayBuffer();
462 | data = Buffer.from( buffer );
463 | }
464 | else {
465 | data = await response.text();
466 | data = data.trim();
467 | }
468 |
469 | return data;
470 | }
471 | catch ( err ) {
472 | app.emit( 'connection-error', err );
473 | throw err;
474 | }
475 | };
476 |
477 | if ( typeof callback === 'function' ) {
478 | fetchData().then( callback ).catch( () => {} );
479 | return;
480 | }
481 |
482 | return fetchData();
483 |
484 | };
485 |
486 | // ready
487 | module.exports = app;
488 |
--------------------------------------------------------------------------------