diff --git a/README.md b/README.md index 38153d37b766f7239b930286733fd6b7fad2f971..debe2d53d0b0fa81e80aa4935c11d3de05dcd69c 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,12 @@ Below is a description of the config file's properties: * `cameraSlots`: The camera slots available for this room. Defaults to `4`. +* `notifyPath`: A path to a file which the camera server will append messages to. + This is used to notify the controller (usually PULT). + The path can either be absolute or relative to the config file but must not start with a tilde. + + If this property is emitted or an empty string is provided, the notify feature will not be used. + ## Stdin-Interface The camera server is controlled by PULT via its stdin. One could also implement the interface in any other program to manage the CVH-Camera. @@ -68,6 +74,20 @@ The current state of the camera feeds is saved in the server's memory and will b | `hide` | Hides the feed of the provided slot. <br/> Note that the feed will still be transmitted to the viewer but is just hidden. By doing that, the feed can be shown again with a very low latency. <br/><br/> **Usage**: `hide <slot>` | `show` | Shows the feed of the provided slot in case it was hidden. <br/><br/> **Usage**: `show <slot>` +## Notify-Interface + +The camera server can notify its controller (usually PULT) by writing to a file which is provided in the config. +The controller can then read the file and process the messages. + +In the case of PULT this file is a named pipe (mkfifo), which works perfectly fine. + +This is a list of all sent messages. Note that a newline character `\n` is appended to every message. + +| Message | Description +| ---------------------------------- | ----------- +| `new_feed <slot>` | Sent after a sender on a slot has started transmitting a feed. +| `remove_feed <slot>` | Sent after a sender on a slot has stopped transmitting a feed or the slot is deactivated (which also removes the feed). + ## Socket Traffic This section describes the socket traffic and the socket.io events that are used. diff --git a/camera-server/example-config.json b/camera-server/example-config.json index da5cef680e2d8f5ebc8c688afdc7201c71bfdf07..21696d58dbd4b344a42bc2451e9874d1536f77e5 100644 --- a/camera-server/example-config.json +++ b/camera-server/example-config.json @@ -1,4 +1,5 @@ { "port": 5000, - "cameraSlots": 4 + "cameraSlots": 4, + "notifyPath": "./path-relative-to-config/or-absolute-path/camera-server-output" } diff --git a/camera-server/src/config/config.ts b/camera-server/src/config/config.ts index 84ecd9df53d3936dfb407df5272e6964a17a79e9..5863a51c6053f410438ceac1384f4be875b994ee 100644 --- a/camera-server/src/config/config.ts +++ b/camera-server/src/config/config.ts @@ -4,13 +4,13 @@ import * as path from 'path'; interface Config { port: number; cameraSlots: number; + notifyPath: string; } // Required to access config with config[key] // But only an object of type Config should be returned interface IndexableConfig extends Config { - // Change type once non-number values are added - [key: string]: number; + [key: string]: number | string; } let configPath = process.env.CONFIG_PATH; @@ -22,7 +22,7 @@ if (configPath) { fileContent = fs.readFileSync(configPath).toString(); } catch (err) { console.log( - `Could not read config at ${path.resolve(configPath)}:`, + `Error: Could not read config at ${path.resolve(configPath)}:`, err ); console.log('Using default values'); @@ -33,7 +33,8 @@ if (configPath) { const indexableConfig: IndexableConfig = { port: 5000, - cameraSlots: 4 + cameraSlots: 4, + notifyPath: '' }; if (fileContent) { @@ -59,12 +60,12 @@ if (fileContent) { ); } } else { - console.log(`Unknown property ${key} in config`); + console.log(`Error: Unknown property ${key} in config`); } }); } else { console.log( - `Config at ${path.resolve( + `Error: Config at ${path.resolve( configPath! )} is malformed - using default values` ); @@ -72,6 +73,12 @@ if (fileContent) { } const config = indexableConfig as Config; +if (config.notifyPath && !path.isAbsolute(config.notifyPath) && configPath) { + config.notifyPath = path.resolve( + path.dirname(configPath), + config.notifyPath + ); +} console.log('Using config:', config); diff --git a/camera-server/src/io-interface/handlers/output-handlers.ts b/camera-server/src/io-interface/handlers/output-handlers.ts new file mode 100644 index 0000000000000000000000000000000000000000..ad81cd3374ec004ca949404efef8f950ae654b08 --- /dev/null +++ b/camera-server/src/io-interface/handlers/output-handlers.ts @@ -0,0 +1,55 @@ +import * as fs from 'fs'; + +import { config } from '../../config/config'; + +// Saves the amount of blocking append calls +// in case the named pipe is not read by PULT +let blockingAppendCalls = 0; + +const timeoutTime = 5000; + +const printTimeoutMessage = (notifyPath: string) => { + console.log( + `Error: Controller did not read file at path '${notifyPath}' for ${timeoutTime}ms` + ); +}; + +const notifyController = (message: string) => { + const { notifyPath } = config; + if (notifyPath) { + if (fs.existsSync(notifyPath)) { + const timeoutId = setTimeout( + printTimeoutMessage.bind(null, notifyPath), + timeoutTime + ); + console.log( + `Notifying controller about message '${message}' using file at path '${notifyPath}'` + ); + blockingAppendCalls += 1; + fs.appendFile(notifyPath, message + '\n', (err) => { + if (err) { + console.log( + `Error: Tried to notify controller about message '${message}' but could not write to path '${notifyPath}' - Error:`, + err + ); + } + blockingAppendCalls -= 1; + clearTimeout(timeoutId); + }); + } else { + console.log( + `Error: Tried to notify controller about message '${message}' using file at path '${notifyPath}' which does not exist` + ); + } + } +}; + +export const isBlocking = () => blockingAppendCalls !== 0; + +export const notifyNewFeed = (slot: number) => { + notifyController(`new_feed ${slot}`); +}; + +export const notifyRemoveFeed = (slot: number) => { + notifyController(`remove_feed ${slot}`); +}; diff --git a/camera-server/src/socket-io/handlers/common-handlers.ts b/camera-server/src/socket-io/handlers/common-handlers.ts index d3b56691f50c5f2502530aaa2afca3a841b9470b..39d4e5386c0ff9fb7bedf2204d272cc1a6f8490b 100644 --- a/camera-server/src/socket-io/handlers/common-handlers.ts +++ b/camera-server/src/socket-io/handlers/common-handlers.ts @@ -1,19 +1,25 @@ import { socketIO } from '../socket-io'; import { cameraSlotState } from '../../state/camera-slot-state'; import { CommandDescriptor } from '../../models/command-descriptor'; +import { + notifyNewFeed, + notifyRemoveFeed +} from '../../io-interface/handlers/output-handlers'; export const emitNewFeed = (slot: number) => { - const cameraState = cameraSlotState[slot]; + const slotState = cameraSlotState[slot]; socketIO.emit('new_feed', { slot, - feedId: cameraState.feedId, - visibility: cameraState.visibility, - geometry: cameraState.geometry + feedId: slotState.feedId, + visibility: slotState.visibility, + geometry: slotState.geometry }); + notifyNewFeed(slot); }; export const emitRemoveFeed = (slot: number) => { socketIO.emit('remove_feed', { slot }); + notifyRemoveFeed(slot); }; export const handleQueryState = (fn: Function) => { @@ -26,12 +32,12 @@ export const handleQueryState = (fn: Function) => { }; } = {}; for (let i = 0; i < cameraSlotState.length; i++) { - const cameraState = cameraSlotState[i]; - if (cameraState.feedActive) { + const slotState = cameraSlotState[i]; + if (slotState.feedActive) { response[i] = { - feedId: cameraState.feedId, - visibility: cameraState.visibility, - geometry: cameraState.geometry + feedId: slotState.feedId, + visibility: slotState.visibility, + geometry: slotState.geometry }; } } diff --git a/camera-server/src/util/cleanup.ts b/camera-server/src/util/cleanup.ts index 5cc867a4fff2a245f63c13f0d2357f968917c951..0a78da277e82507dc1291fe4700d9944b376a5ae 100644 --- a/camera-server/src/util/cleanup.ts +++ b/camera-server/src/util/cleanup.ts @@ -1,19 +1,29 @@ import { socketIO } from '../socket-io/socket-io'; +import { isBlocking } from '../io-interface/handlers/output-handlers'; interface ExitHandlerOptions { cleanup?: boolean; exit?: boolean; } -const cleanup = () => { - console.log('cleanup'); - socketIO.emit('remove_all_feeds'); -}; - -const exitHandler = (options: ExitHandlerOptions, exitCode: number) => { - if (options.cleanup) cleanup(); - if (exitCode || exitCode === 0) console.log(exitCode); - if (options.exit) process.exit(); +const exitHandler = ( + options: ExitHandlerOptions, + exitCode: string | number +) => { + if (options.cleanup) { + console.log('cleanup'); + socketIO.emit('remove_all_feeds'); + } + if (exitCode || exitCode === 0) { + console.log('Exit code:', exitCode); + if (exitCode === 0 && isBlocking()) { + console.log('Aborting process due to blocking file append'); + process.abort(); + } + } + if (options.exit) { + process.exit(); + } }; export const registerCleanupLogic = () => {