From 39cdfaeb096bd5ff5965424affa041336913a158 Mon Sep 17 00:00:00 2001 From: Jason Hyde Date: Tue, 22 Oct 2024 21:34:06 +0200 Subject: [PATCH] [ts] lazy load sdk, plus extensive logging and documentation --- source/device.ts | 150 +++++++++++++++++------- source/lib/sdk.ts | 285 +++++++++++++++++++++++++++++++++++++++------- 2 files changed, 352 insertions(+), 83 deletions(-) diff --git a/source/device.ts b/source/device.ts index b7cd672..e99503b 100644 --- a/source/device.ts +++ b/source/device.ts @@ -1,6 +1,7 @@ import debug from 'debug' import { randomUUID } from 'node:crypto' import { existsSync, mkdirSync } from 'node:fs' +import { platform } from 'node:os' import { dirname } from 'node:path' import { auth, measure } from './decorators/index.ts' import { parseBuildDate } from './helpers/date.ts' @@ -32,8 +33,8 @@ export class Device { // @ts-expect-error deviceInfo is passed as a pointer to login function and should be initialized as an empty object #deviceInfo: DeviceInfo = {} - readonly #sdkVersion: string | undefined - readonly #sdkBuild: string | undefined + readonly #sdkVersion: string + readonly #sdkBuild: string /** * Creates a new device. @@ -41,8 +42,13 @@ export class Device { * @param ip - The IP address of the device. * @param port - The port of the device. * @param settings - The settings for the device. + * @throws {Error} If not running on Linux or initialization fails */ constructor(ip: string, port = 9008, settings?: Settings) { + if (platform() !== 'linux') { + throw new Error('This SDK is only supported on Linux platforms') + } + this.ip = validateIp(ip) this.port = validatePort(port) this.uuid = settings?.uuid ?? randomUUID() @@ -54,32 +60,26 @@ export class Device { this.#isReconnectEnabled = settings.isReconnectEnabled ?? this.#isReconnectEnabled } - if (this.init()) { - log(`${this.uuid} device initialized successfully!`) - - const sdkVersion = sdk.getSDKVersion() - const buildVersion = sdk.getSDKBuildVersion() - this.#sdkVersion = `0x${sdkVersion.toString(16)} (${sdkVersion})` - // that's a build date actually - this.#sdkBuild = `${parseBuildDate(buildVersion.toString())} (${buildVersion})` - } - } + log(`Initializing device ${this.uuid} with IP: ${this.ip}:${this.port}`) - /** - * Initializes the SDK. - * - * @returns A boolean indicating whether the initialization was successful. - * @throws An error if the initialization fails. - */ - init(): boolean { + // Initialize the SDK if ( !sdk.init() || !sdk.setConnectTimeout(this.#connectionTimeoutMs, this.#maxRetries) || !sdk.setReconnectInterval(this.#reconnectIntervalMs, this.#isReconnectEnabled) ) { - throw new Error(this.getLastError()) + const error = this.getLastError() + log(`Failed to initialize device ${this.uuid}: ${error}`) + throw new Error(error) } - return true + + // Get SDK version information + const sdkVersion = sdk.getSDKVersion() + const buildVersion = sdk.getSDKBuildVersion() + this.#sdkVersion = `0x${sdkVersion.toString(16)} (${sdkVersion})` + this.#sdkBuild = `${parseBuildDate(buildVersion.toString())} (${buildVersion})` + + log(`${this.uuid} device initialized successfully!`) } /** @@ -88,15 +88,24 @@ export class Device { * @returns A boolean indicating whether the disposal was successful. */ dispose(): boolean { - if (this.userId) { - this.logout() + log(`Disposing device ${this.uuid}...`) + + try { + if (this.userId) { + this.logout() + } + const result = sdk.cleanup() + log(`Device ${this.uuid} disposed successfully`) + return result + } catch (error) { + log(`Failed to dispose device ${this.uuid}: ${error}`) + return false } - return sdk.cleanup() } /** * This getter method returns the versions information of the device and sdk. - * If the the information is not available, it throws an error. + * If the information is not available, it throws an error. */ @auth get version(): VersionInfo { @@ -106,8 +115,8 @@ export class Device { return { sdk: { - version: this.#sdkVersion ?? 'Unknown', - build: this.#sdkBuild ?? 'Unknown' + version: this.#sdkVersion, + build: this.#sdkBuild }, device: { name: this.#deviceInfo.deviceName, @@ -141,11 +150,19 @@ export class Device { */ @measure login(user: string, pass: string): boolean { - this.userId = sdk.login(this.ip, this.port, user, pass, this.#deviceInfo) - if (this.userId === -1) { - throw new Error(this.getLastError()) + log(`Logging in to device ${this.uuid} with user: ${user}`) + + try { + this.userId = sdk.login(this.ip, this.port, user, pass, this.#deviceInfo) + if (this.userId === -1) { + throw new Error(this.getLastError()) + } + log(`Successfully logged in to device ${this.uuid}`) + return Boolean(this.userId) + } catch (error) { + log(`Failed to log in to device ${this.uuid}: ${error}`) + throw error } - return Boolean(this.userId) } /** @@ -155,7 +172,19 @@ export class Device { */ @auth logout(): boolean { - return sdk.logout(this.userId) + log(`Logging out from device ${this.uuid}`) + try { + const result = sdk.logout(this.userId) + if (result) { + log(`Successfully logged out from device ${this.uuid}`) + } else { + log(`Failed to log out from device ${this.uuid}`) + } + return result + } catch (error) { + log(`Error logging out from device ${this.uuid}: ${error}`) + return false + } } /** @@ -166,10 +195,31 @@ export class Device { */ @auth triggerAlarm(value: boolean): boolean { - // @TODO: get alarm channels from device info - const alarmChannels = [0] - const alarmValues = [value ? 1 : 0] - return sdk.triggerAlarm(this.userId, alarmChannels, alarmValues, alarmChannels.length, this.#isAlarmOpen) + log(`Triggering alarm on device ${this.uuid} with value: ${value}`) + + try { + // @TODO: get alarm channels from device info + const alarmChannels = [0] + const alarmValues = [value ? 1 : 0] + const result = sdk.triggerAlarm( + this.userId, + alarmChannels, + alarmValues, + alarmChannels.length, + this.#isAlarmOpen + ) + + if (result) { + log(`Successfully triggered alarm on device ${this.uuid}`) + } else { + log(`Failed to trigger alarm on device ${this.uuid}`) + } + + return result + } catch (error) { + log(`Error triggering alarm on device ${this.uuid}: ${error}`) + return false + } } /** @@ -181,14 +231,29 @@ export class Device { */ @auth saveSnapshot(channel: number, filePath: string): boolean { - const dirPath = dirname(filePath) + log(`Saving snapshot from device ${this.uuid} channel ${channel} to ${filePath}`) - // sdk doesn't check if path is valid so we need to do it ourselves - if (!existsSync(dirPath)) { - mkdirSync(dirPath, { recursive: true }) - } + try { + const dirPath = dirname(filePath) - return sdk.captureJPEGFile_V2(this.userId, channel, filePath) + // sdk doesn't check if path is valid so we need to do it ourselves + if (!existsSync(dirPath)) { + mkdirSync(dirPath, { recursive: true }) + } + + const result = sdk.captureJPEGFile_V2(this.userId, channel, filePath) + + if (result) { + log(`Successfully saved snapshot from device ${this.uuid}`) + } else { + log(`Failed to save snapshot from device ${this.uuid}`) + } + + return result + } catch (error) { + log(`Error saving snapshot from device ${this.uuid}: ${error}`) + return false + } } /** @@ -196,7 +261,6 @@ export class Device { * * @returns A string describing the last error. */ - getLastError(): string { return NET_SDK_ERROR[sdk.getLastError()] ?? 'Unknown error' } diff --git a/source/lib/sdk.ts b/source/lib/sdk.ts index d353635..3c57217 100644 --- a/source/lib/sdk.ts +++ b/source/lib/sdk.ts @@ -1,11 +1,9 @@ import koffi from 'koffi' +import { platform } from 'node:os' import { resolve } from 'node:path' import { LPNET_SDK_DEVICEINFO, NET_SDK_DEVICE_DISCOVERY_INFO, NET_SDK_IPC_DEVICE_INFO } from './struct/index.ts' import type { DeviceInfo, LOG_LEVEL } from './types.ts' -const path = resolve(import.meta.dirname, '../../', 'bin/linux/libdvrnetsdk.so') -const lib = koffi.load(path) - type SDK = { // ✅︎ DWORD NET_SDK_GetSDKVersion(); getSDKVersion: () => number @@ -69,41 +67,248 @@ type SDK = { captureJPEGFile_V2: (userId: number, channel: number, fileName: string) => boolean } -export const sdk: SDK = { - getSDKVersion: lib.func('NET_SDK_GetSDKVersion', 'uint32_t', []), - getSDKBuildVersion: lib.func('NET_SDK_GetSDKBuildVersion', 'uint32_t', []), - discoverDevice: lib.func('NET_SDK_DiscoverDevice', 'int', [ - koffi.out(koffi.pointer(NET_SDK_DEVICE_DISCOVERY_INFO)), - 'int', - 'int' - ]), - getDeviceInfo: lib.func('NET_SDK_GetDeviceInfo', 'bool', ['long', koffi.out(koffi.pointer(LPNET_SDK_DEVICEINFO))]), - getDeviceIPCInfo: lib.func('NET_SDK_GetDeviceIPCInfo', 'bool', [ - 'long', - koffi.out(koffi.pointer(NET_SDK_IPC_DEVICE_INFO)), - 'long', - 'long *' - ]), - init: lib.func('NET_SDK_Init', 'bool', []), - cleanup: lib.func('NET_SDK_Cleanup', 'bool', []), - setConnectTimeout: lib.func('NET_SDK_SetConnectTime', 'bool', ['uint32_t', 'uint32_t']), - setReconnectInterval: lib.func('NET_SDK_SetReconnect', 'bool', ['uint32_t', 'bool']), - login: lib.func('NET_SDK_Login', 'long', [ - 'string', - 'uint16_t', - 'string', - 'string', - koffi.out(koffi.pointer(LPNET_SDK_DEVICEINFO)) - ]), - logout: lib.func('NET_SDK_Logout', 'bool', ['long']), - setupAlarmChanel: lib.func('NET_SDK_SetupAlarmChan', 'long', ['long']), - closeAlarmChanel: lib.func('NET_SDK_CloseAlarmChan', 'bool', ['long']), - triggerAlarm: lib.func('NET_SDK_SetDeviceManualAlarm', 'bool', ['long', 'long *', 'long *', 'long', 'bool']), - getConfigFile: lib.func('NET_SDK_GetConfigFile', 'bool', ['long', 'string']), - setConfigFile: lib.func('NET_SDK_SetConfigFile', 'bool', ['long', 'string']), - getLastError: lib.func('NET_SDK_GetLastError', 'uint32_t', []), - setLogToFile: lib.func('NET_SDK_SetLogToFile', 'bool', ['bool', 'string', 'bool', 'int']), - startSavingLiveStream: lib.func('NET_SDK_SaveLiveData', 'bool', ['long', 'string']), - stopSavingLiveStream: lib.func('NET_SDK_StopSaveLiveData', 'bool', ['long']), - captureJPEGFile_V2: lib.func('NET_SDK_CaptureJPEGFile_V2', 'bool', ['long', 'long', 'string']) +const isLinux = platform() === 'linux' + +if (!isLinux) { + throw new Error('This SDK is only supported on Linux platforms') } + +/** + * Creates a lazy-loaded SDK instance for interfacing with TVT devices. + * The SDK is only loaded when first accessed and subsequent calls use the cached instance. + * This SDK is specifically designed for Linux systems and provides methods for device control and monitoring. + * + * @returns An object containing all available SDK methods + * @throws {Error} If the library cannot be loaded or if running on non-Linux systems + */ +const createSDK = (): SDK => { + let lib: ReturnType | null = null + + /** + * Gets or initializes the library instance. + * + * @returns The loaded shared library instance + */ + const getLib = () => { + if (!lib) { + const path = resolve(import.meta.dirname, '../../', 'bin/linux/libdvrnetsdk.so') + lib = koffi.load(path) + } + return lib + } + + return { + /** + * Gets the SDK version number. + * + * @returns {number} The SDK version in hex format + */ + getSDKVersion: getLib().func('NET_SDK_GetSDKVersion', 'uint32_t', []), + + /** + * Gets the SDK build version (typically represents build date). + * + * @returns {number} The SDK build version + */ + getSDKBuildVersion: getLib().func('NET_SDK_GetSDKBuildVersion', 'uint32_t', []), + + /** + * Discovers TVT devices on the network. + * + * @param {DeviceInfo} deviceInfo - Buffer to store discovered device information + * @param {number} bufNum - Size of the buffer + * @param {number} waitSeconds - Time to wait for device responses + * @returns {number} Number of devices discovered + */ + discoverDevice: getLib().func('NET_SDK_DiscoverDevice', 'int', [ + koffi.out(koffi.pointer(NET_SDK_DEVICE_DISCOVERY_INFO)), + 'int', + 'int' + ]), + + /** + * Gets detailed information about a connected device. + * + * @param {number} userId - User ID from successful login + * @param {DeviceInfo} deviceInfo - Buffer to store device information + * @returns {boolean} Success status + */ + getDeviceInfo: getLib().func('NET_SDK_GetDeviceInfo', 'bool', [ + 'long', + koffi.out(koffi.pointer(LPNET_SDK_DEVICEINFO)) + ]), + + /** + * Gets information about IPC devices connected to an NVR/DVR. + * + * @param {number} userId - User ID from successful login + * @param {DeviceInfo} deviceIPCInfo - Buffer to store IPC information + * @param {number} buffSize - Size of the buffer + * @param {number[]} ipcCount - Array to store the count of IPCs + * @returns {boolean} Success status + */ + getDeviceIPCInfo: getLib().func('NET_SDK_GetDeviceIPCInfo', 'bool', [ + 'long', + koffi.out(koffi.pointer(NET_SDK_IPC_DEVICE_INFO)), + 'long', + 'long *' + ]), + + /** + * Initializes the SDK. Must be called before using any other functions. + * + * @returns {boolean} Success status + */ + init: getLib().func('NET_SDK_Init', 'bool', []), + + /** + * Cleans up and releases SDK resources. + * + * @returns {boolean} Success status + */ + cleanup: getLib().func('NET_SDK_Cleanup', 'bool', []), + + /** + * Sets connection timeout parameters. + * + * @param {number} waitTime - Wait time in milliseconds + * @param {number} retryTimes - Number of retry attempts + * @returns {boolean} Success status + */ + setConnectTimeout: getLib().func('NET_SDK_SetConnectTime', 'bool', ['uint32_t', 'uint32_t']), + + /** + * Sets reconnection parameters. + * + * @param {number} interval - Reconnection interval in milliseconds + * @param {boolean} enableRecon - Enable/disable reconnection + * @returns {boolean} Success status + */ + setReconnectInterval: getLib().func('NET_SDK_SetReconnect', 'bool', ['uint32_t', 'bool']), + + /** + * Logs into a device. + * + * @param {string} ip - Device IP address + * @param {number} port - Device port + * @param {string} username - Login username + * @param {string} password - Login password + * @param {DeviceInfo} deviceInfo - Buffer to store device information + * @returns {number} User ID if successful, -1 if failed + */ + login: getLib().func('NET_SDK_Login', 'long', [ + 'string', + 'uint16_t', + 'string', + 'string', + koffi.out(koffi.pointer(LPNET_SDK_DEVICEINFO)) + ]), + + /** + * Logs out from a device. + * + * @param {number} userId - User ID from successful login + * @returns {boolean} Success status + */ + logout: getLib().func('NET_SDK_Logout', 'bool', ['long']), + + /** + * Sets up an alarm channel. + * + * @param {number} userId - User ID from successful login + * @returns {number} Alarm handle if successful + */ + setupAlarmChanel: getLib().func('NET_SDK_SetupAlarmChan', 'long', ['long']), + + /** + * Closes an alarm channel. + * + * @param {number} alarmHandle - Handle from setupAlarmChanel + * @returns {boolean} Success status + */ + closeAlarmChanel: getLib().func('NET_SDK_CloseAlarmChan', 'bool', ['long']), + + /** + * Triggers manual alarms on specified channels. + * + * @param {number} userId - User ID from successful login + * @param {number[]} channel - Array of channel numbers + * @param {number[]} value - Array of alarm values + * @param {number} channelCount - Number of channels + * @param {boolean} alarmOpen - Alarm open/close state + * @returns {boolean} Success status + */ + triggerAlarm: getLib().func('NET_SDK_SetDeviceManualAlarm', 'bool', [ + 'long', + 'long *', + 'long *', + 'long', + 'bool' + ]), + + /** + * Gets device configuration file. + * + * @param {number} userId - User ID from successful login + * @param {string} fileName - Path to save configuration file + * @returns {boolean} Success status + */ + getConfigFile: getLib().func('NET_SDK_GetConfigFile', 'bool', ['long', 'string']), + + /** + * Sets device configuration from file. + * + * @param {number} userId - User ID from successful login + * @param {string} fileName - Path to configuration file + * @returns {boolean} Success status + */ + setConfigFile: getLib().func('NET_SDK_SetConfigFile', 'bool', ['long', 'string']), + + /** + * Gets the last error code from the SDK. + * + * @returns {number} Error code + */ + getLastError: getLib().func('NET_SDK_GetLastError', 'uint32_t', []), + + /** + * Configures SDK logging to file. + * + * @param {boolean} logEnable - Enable/disable logging + * @param {string} logDir - Directory for log files + * @param {boolean} autoDel - Enable auto-deletion of old logs + * @param {number} logLevel - Logging level + * @returns {boolean} Success status + */ + setLogToFile: getLib().func('NET_SDK_SetLogToFile', 'bool', ['bool', 'string', 'bool', 'int']), + + /** + * Starts saving live stream to file. + * + * @param {number} liveHandle - Live stream handle + * @param {string} fileName - Path to save stream file + * @returns {boolean} Success status + */ + startSavingLiveStream: getLib().func('NET_SDK_SaveLiveData', 'bool', ['long', 'string']), + + /** + * Stops saving live stream to file. + * + * @param {number} liveHandle - Live stream handle + * @returns {boolean} Success status + */ + stopSavingLiveStream: getLib().func('NET_SDK_StopSaveLiveData', 'bool', ['long']), + + /** + * Captures a JPEG snapshot from a channel. + * + * @param {number} userId - User ID from successful login + * @param {number} channel - Video channel number + * @param {string} fileName - Path to save JPEG file + * @returns {boolean} Success status + */ + captureJPEGFile_V2: getLib().func('NET_SDK_CaptureJPEGFile_V2', 'bool', ['long', 'long', 'string']) + } +} + +export const sdk = createSDK()