Skip to content
Snippets Groups Projects
Commit e65692a0 authored by Simon Döring's avatar Simon Döring
Browse files

Split camera server code

parent bdca2962
No related branches found
No related tags found
No related merge requests found
......@@ -4,6 +4,11 @@ import * as path from 'path';
interface Config {
port: number;
cameraSlots: number;
}
// 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;
}
......@@ -26,7 +31,7 @@ if (configPath) {
console.log('Got not CONFIG_PATH environment variable');
}
const config: Config = {
const indexableConfig: IndexableConfig = {
port: 5000,
cameraSlots: 4
};
......@@ -43,11 +48,11 @@ if (fileContent) {
console.log(`Reading config at ${path.resolve(configPath!)}`);
// Overwrite default values with values of read config
Object.keys(readConfig).forEach((key) => {
if (config.hasOwnProperty(key)) {
const expectedType = typeof config[key];
if (indexableConfig.hasOwnProperty(key)) {
const expectedType = typeof indexableConfig[key];
const readType = typeof readConfig[key];
if (expectedType === readType) {
config[key] = readConfig[key];
indexableConfig[key] = readConfig[key];
} else {
console.log(
`Error: Read config propety '${key}' is of type ${readType}, but type ${expectedType} was expected`
......@@ -66,6 +71,8 @@ if (fileContent) {
}
}
const config = indexableConfig as Config;
console.log('Using config:', config);
export { config };
import { socketIO } from '../../socket-io/socket-io';
import { cameraSlotState } from '../../state/camera-slot-state';
import { emitRemoveFeed } from '../../socket-io/handlers/common-handlers';
const visibilityCommands = ['hide', 'show'];
const geometryCommands = [
'set_geometry_relative_to_window',
'set_geometry_relative_to_canvas'
];
const internalCommands = ['activate_slot', 'deactivate_slot', 'refresh_token'];
const handleInternalCommand = (
command: string,
slot: number,
params: string[]
) => {
const currentCameraState = cameraSlotState[slot];
switch (command) {
case 'activate_slot':
if (currentCameraState.slotActive) {
console.log('Error: Tried to activate active slot ' + slot);
return;
}
if (params.length === 0) {
console.log(
'Error while activating slot ' +
slot +
' - Got no token parameter'
);
return;
}
currentCameraState.token = params[0];
currentCameraState.slotActive = true;
break;
case 'deactivate_slot':
if (!currentCameraState.slotActive) {
console.log('Error: Tried to deactivate inactive slot ' + slot);
return;
}
console.log('Deactivating slot ' + slot);
emitRemoveFeed(slot);
currentCameraState.slotActive = false;
currentCameraState.token = null;
currentCameraState.feedActive = false;
currentCameraState.feedId = null;
currentCameraState.senderSocketId = null;
break;
case 'refresh_token':
if (!currentCameraState.slotActive) {
console.log(
'Error: Tried to refresh token for inactive slot ' + slot
);
return;
}
if (params.length === 0) {
console.log(
'Error while refreshing token for slot ' +
slot +
' - Got no token parameter'
);
console.log('Keeping old token');
return;
}
console.log('Refreshing token for slot ' + slot);
currentCameraState.token = params[0];
break;
default:
console.log(
'Error: handleInternalCommand got unknown command ' + command
);
break;
}
};
export const handleCommand = (line: string) => {
let emitCommand = false;
console.log('Got command from stdin:', line);
const params = line.split(' ');
const command = params.shift();
if (command == null) {
console.log('Error: Got malformed line with no command');
return;
}
const slotStr = params.shift();
if (slotStr == null) {
console.log('Error: Got no slot to apply the command on');
return;
}
const slot = parseInt(slotStr);
if (isNaN(slot)) {
console.log(
'Error: Could not parse slot ' + slotStr + ' to an integer'
);
return;
}
if (slot < 0 || slot > cameraSlotState.length - 1) {
console.log(
`Error: Got invalid slot number ${slot}. There are ${cameraSlotState.length} camera slots.`
);
return;
}
console.log('command:', command);
console.log('slot:', slot);
console.log('params:', params);
const currentCameraState = cameraSlotState[slot];
if (visibilityCommands.includes(command)) {
currentCameraState.visibility = {
command,
params
};
emitCommand = true;
} else if (geometryCommands.includes(command)) {
currentCameraState.geometry = {
command,
params
};
emitCommand = true;
} else if (internalCommands.includes(command)) {
handleInternalCommand(command, slot, params);
} else {
console.log('Command "' + command + '" is not a valid command');
return;
}
console.log('new cameraState:', currentCameraState);
if (currentCameraState.feedActive && emitCommand) {
socketIO.emit('command', {
slot,
command,
params
});
}
};
import * as readline from 'readline';
export const readlineInterface = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
export interface CommandDescriptor {
command: string;
params: string[];
}
import { Server as SocketIOServer, Socket } from 'socket.io';
import { Socket } from 'socket.io';
import { mountCleanupLogic } from './util/cleanup';
import { ValidationError } from './models/validation-error';
import { CameraSlotState, CommandDescriptor } from './models/camera-slot-state';
import { SenderSocket } from './models/sender-socket';
import { config } from './config/config';
import { socketIO } from './socket-io/socket-io';
import { handleSenderInit } from './socket-io/handlers/sender-handlers';
import { handleQueryState } from './socket-io/handlers/common-handlers';
import { readlineInterface } from './io-interface/readline-interface';
import { handleCommand } from './io-interface/handlers/input-handlers';
import { registerCleanupLogic } from './util/cleanup';
const readline = require('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false
});
const io = new SocketIOServer(config.port);
const visibilityCommands = ['hide', 'show'];
const geometryCommands = [
'set_geometry_relative_to_window',
'set_geometry_relative_to_canvas'
];
const internalCommands = ['activate_slot', 'deactivate_slot', 'refresh_token'];
let cameraSlotState: CameraSlotState[] = [];
for (let i = 0; i < config.cameraSlots; i++) {
cameraSlotState.push(new CameraSlotState());
}
const emitNewFeed = (slot: number) => {
const cameraState = cameraSlotState[slot];
io.emit('new_feed', {
slot,
feedId: cameraState.feedId,
visibility: cameraState.visibility,
geometry: cameraState.geometry
});
};
const emitRemoveFeed = (slot: number) => {
io.emit('remove_feed', { slot });
};
const handleSetFeedId = (
socket: SenderSocket,
data: null | { feedId?: string },
fn: Function
) => {
let success = true;
let message = '';
try {
const slot = socket.cameraSlot;
const currentCameraState = cameraSlotState[slot];
if (currentCameraState.token !== socket.cameraSlotToken) {
console.log(
'Error: Got set_feed_id event for slot ' +
slot +
' with an old token'
);
throw new ValidationError(
'The provided token is not valid anymore - the feed is not transmitted'
);
}
if (currentCameraState.feedActive) {
console.log(
'Error: Got set_feed_id event for slot ' +
slot +
' which already has an active feed'
);
throw new ValidationError(
'There is already somebody using this slot'
);
}
if (data == null) {
console.log(
'Error: Got set_feed_id event for slot ' +
slot +
' without data'
);
throw new ValidationError(
'Could not get feed id because no data was provided'
);
}
const feedId = data.feedId;
if (feedId == null) {
console.log(
'Error: Got set_feed_id event without a feed id on slot ' + slot
);
throw new ValidationError('No feed id was provided');
}
console.log('Setting feed id of slot ' + slot + ' to ' + feedId);
message = 'Successfully set feed id - you are now using this slot';
currentCameraState.feedActive = true;
currentCameraState.feedId = feedId;
currentCameraState.senderSocketId = socket.id;
emitNewFeed(slot);
} catch (e) {
if (e instanceof ValidationError) {
success = false;
message = e.message;
} else {
throw e;
}
}
fn({ success, message });
};
const handleSenderDisconnect = (socket: SenderSocket, _: string) => {
const slot = socket.cameraSlot;
if (slot != null) {
const currentCameraState = cameraSlotState[slot];
if (
currentCameraState.feedActive &&
socket.id === currentCameraState.senderSocketId
) {
console.log(
'Sender on slot ' + slot + ' disconnected - Clearing slot'
);
currentCameraState.feedActive = false;
currentCameraState.feedId = null;
currentCameraState.senderSocketId = null;
emitRemoveFeed(slot);
}
}
};
const registerSenderHandlers = (socket: SenderSocket) => {
socket.on('set_feed_id', handleSetFeedId.bind(null, socket));
socket.on('disconnect', handleSenderDisconnect.bind(null, socket));
};
const handleSenderInit = (
socket: SenderSocket,
data: null | { slot?: string; token?: string },
fn: Function
) => {
let success = true;
let message = '';
try {
if (data == null) {
console.log('Error: Got socket connection without data');
throw new ValidationError('No data provided');
}
const slotStr = data.slot;
if (slotStr == null) {
console.log('Error: Got socket connection without a slot');
throw new ValidationError('No slot provided');
}
const slot = parseInt(slotStr);
if (isNaN(slot)) {
console.log(
'Error: Got socket connection with slot ' +
slotStr +
' that cannot be parsed to a number'
);
throw new ValidationError(
'Slot ' + slotStr + ' cannot be parsed to number'
);
}
if (slot < 0 || slot > cameraSlotState.length - 1) {
console.log(
'Error: Got socket connection with slot ' +
slot +
' which is not in the list of slots'
);
throw new ValidationError(
'Slot ' + slot + ' is not in the list of slots'
);
}
const slotState = cameraSlotState[slot];
if (!slotState.slotActive) {
console.log(
'Error: Got socket connection for inactive slot ' + slot
);
throw new ValidationError('Slot ' + slot + ' is not active');
}
const token = data.token;
if (token == null) {
console.log('Error: Got socket connection without token');
throw new ValidationError('No token provided');
}
if (slotState.token !== token) {
console.log(
'Error: Got socket connecion with wrong token ' +
token +
' for slot ' +
slot
);
throw new ValidationError('Invalid token');
}
console.log('Got sender socket connection on slot ' + slot);
message = 'Socket authenticated';
socket.cameraSlot = slot;
socket.cameraSlotToken = token;
registerSenderHandlers(socket);
} catch (e) {
if (e instanceof ValidationError) {
success = false;
message = e.message;
} else {
throw e;
}
}
fn({ success, message });
};
const handleQueryState = (fn: Function) => {
console.log('Got state query from socket');
let response: {
[index: number]: {
feedId: string | null;
visibility: CommandDescriptor;
geometry: CommandDescriptor;
};
} = {};
for (let i = 0; i < cameraSlotState.length; i++) {
const cameraState = cameraSlotState[i];
if (cameraState.feedActive) {
response[i] = {
feedId: cameraState.feedId,
visibility: cameraState.visibility,
geometry: cameraState.geometry
};
}
}
fn(response);
};
io.on('connection', (socket: Socket) => {
socketIO.on('connection', (socket: Socket) => {
socket.on('query_state', handleQueryState);
socket.on('sender_init', handleSenderInit.bind(null, socket));
});
const handleInternalCommand = (
command: string,
slot: number,
params: string[]
) => {
const currentCameraState = cameraSlotState[slot];
switch (command) {
case 'activate_slot':
if (currentCameraState.slotActive) {
console.log('Error: Tried to activate active slot ' + slot);
return;
}
if (params.length === 0) {
console.log(
'Error while activating slot ' +
slot +
' - Got no token parameter'
);
return;
}
currentCameraState.token = params[0];
currentCameraState.slotActive = true;
break;
case 'deactivate_slot':
if (!currentCameraState.slotActive) {
console.log('Error: Tried to deactivate inactive slot ' + slot);
return;
}
console.log('Deactivating slot ' + slot);
emitRemoveFeed(slot);
currentCameraState.slotActive = false;
currentCameraState.token = null;
currentCameraState.feedActive = false;
currentCameraState.feedId = null;
currentCameraState.senderSocketId = null;
break;
case 'refresh_token':
if (!currentCameraState.slotActive) {
console.log(
'Error: Tried to refresh token for inactive slot ' + slot
);
return;
}
if (params.length === 0) {
console.log(
'Error while refreshing token for slot ' +
slot +
' - Got no token parameter'
);
console.log('Keeping old token');
return;
}
console.log('Refreshing token for slot ' + slot);
currentCameraState.token = params[0];
break;
default:
console.log(
'Error: handleInternalCommand got unknown command ' + command
);
break;
}
};
const handleCommand = (line: string) => {
let emitCommand = false;
console.log('Got command from stdin:', line);
const params = line.split(' ');
const command = params.shift();
if (command == null) {
console.log('Error: Got malformed line with no command');
return;
}
const slotStr = params.shift();
if (slotStr == null) {
console.log('Error: Got no slot to apply the command on');
return;
}
const slot = parseInt(slotStr);
if (isNaN(slot)) {
console.log(
'Error: Could not parse slot ' + slotStr + ' to an integer'
);
return;
}
if (slot < 0 || slot > cameraSlotState.length - 1) {
console.log(
`Error: Got invalid slot number ${slot}. There are ${cameraSlotState.length} camera slots.`
);
return;
}
console.log('command:', command);
console.log('slot:', slot);
console.log('params:', params);
const currentCameraState = cameraSlotState[slot];
if (visibilityCommands.includes(command)) {
currentCameraState.visibility = {
command,
params
};
emitCommand = true;
} else if (geometryCommands.includes(command)) {
currentCameraState.geometry = {
command,
params
};
emitCommand = true;
} else if (internalCommands.includes(command)) {
handleInternalCommand(command, slot, params);
} else {
console.log('Command "' + command + '" is not a valid command');
return;
}
console.log('new cameraState:', currentCameraState);
if (currentCameraState.feedActive && emitCommand) {
io.emit('command', {
slot,
command,
params
});
}
};
rl.on('line', handleCommand);
readlineInterface.on('line', handleCommand);
mountCleanupLogic(io);
registerCleanupLogic();
import { socketIO } from '../socket-io';
import { cameraSlotState } from '../../state/camera-slot-state';
import { CommandDescriptor } from '../../models/command-descriptor';
export const emitNewFeed = (slot: number) => {
const cameraState = cameraSlotState[slot];
socketIO.emit('new_feed', {
slot,
feedId: cameraState.feedId,
visibility: cameraState.visibility,
geometry: cameraState.geometry
});
};
export const emitRemoveFeed = (slot: number) => {
socketIO.emit('remove_feed', { slot });
};
export const handleQueryState = (fn: Function) => {
console.log('Got state query from socket');
let response: {
[index: number]: {
feedId: string | null;
visibility: CommandDescriptor;
geometry: CommandDescriptor;
};
} = {};
for (let i = 0; i < cameraSlotState.length; i++) {
const cameraState = cameraSlotState[i];
if (cameraState.feedActive) {
response[i] = {
feedId: cameraState.feedId,
visibility: cameraState.visibility,
geometry: cameraState.geometry
};
}
}
fn(response);
};
import { cameraSlotState } from '../../state/camera-slot-state';
import { emitNewFeed, emitRemoveFeed } from './common-handlers';
import { SenderSocket } from '../../models/sender-socket';
import { ValidationError } from '../../models/validation-error';
const handleSetFeedId = (
socket: SenderSocket,
data: null | { feedId?: string },
fn: Function
) => {
let success = true;
let message = '';
try {
const slot = socket.cameraSlot;
const currentSlotState = cameraSlotState[slot];
if (currentSlotState.token !== socket.cameraSlotToken) {
console.log(
'Error: Got set_feed_id event for slot ' +
slot +
' with an old token'
);
throw new ValidationError(
'The provided token is not valid anymore - the feed is not transmitted'
);
}
if (currentSlotState.feedActive) {
console.log(
'Error: Got set_feed_id event for slot ' +
slot +
' which already has an active feed'
);
throw new ValidationError(
'There is already somebody using this slot'
);
}
if (data == null) {
console.log(
'Error: Got set_feed_id event for slot ' +
slot +
' without data'
);
throw new ValidationError(
'Could not get feed id because no data was provided'
);
}
const feedId = data.feedId;
if (feedId == null) {
console.log(
'Error: Got set_feed_id event without a feed id on slot ' + slot
);
throw new ValidationError('No feed id was provided');
}
console.log('Setting feed id of slot ' + slot + ' to ' + feedId);
message = 'Successfully set feed id - you are now using this slot';
currentSlotState.feedActive = true;
currentSlotState.feedId = feedId;
currentSlotState.senderSocketId = socket.id;
emitNewFeed(slot);
} catch (e) {
if (e instanceof ValidationError) {
success = false;
message = e.message;
} else {
throw e;
}
}
fn({ success, message });
};
const handleSenderDisconnect = (socket: SenderSocket, _: string) => {
const slot = socket.cameraSlot;
if (slot != null) {
const currentSlotState = cameraSlotState[slot];
if (
currentSlotState.feedActive &&
socket.id === currentSlotState.senderSocketId
) {
console.log(
'Sender on slot ' + slot + ' disconnected - Clearing slot'
);
currentSlotState.feedActive = false;
currentSlotState.feedId = null;
currentSlotState.senderSocketId = null;
emitRemoveFeed(slot);
}
}
};
const registerSenderHandlers = (socket: SenderSocket) => {
socket.on('set_feed_id', handleSetFeedId.bind(null, socket));
socket.on('disconnect', handleSenderDisconnect.bind(null, socket));
};
export const handleSenderInit = (
socket: SenderSocket,
data: null | { slot?: string; token?: string },
fn: Function
) => {
let success = true;
let message = '';
try {
if (data == null) {
console.log('Error: Got socket connection without data');
throw new ValidationError('No data provided');
}
const slotStr = data.slot;
if (slotStr == null) {
console.log('Error: Got socket connection without a slot');
throw new ValidationError('No slot provided');
}
const slot = parseInt(slotStr);
if (isNaN(slot)) {
console.log(
'Error: Got socket connection with slot ' +
slotStr +
' that cannot be parsed to a number'
);
throw new ValidationError(
'Slot ' + slotStr + ' cannot be parsed to number'
);
}
if (slot < 0 || slot > cameraSlotState.length - 1) {
console.log(
'Error: Got socket connection with slot ' +
slot +
' which is not in the list of slots'
);
throw new ValidationError(
'Slot ' + slot + ' is not in the list of slots'
);
}
const slotState = cameraSlotState[slot];
if (!slotState.slotActive) {
console.log(
'Error: Got socket connection for inactive slot ' + slot
);
throw new ValidationError('Slot ' + slot + ' is not active');
}
const token = data.token;
if (token == null) {
console.log('Error: Got socket connection without token');
throw new ValidationError('No token provided');
}
if (slotState.token !== token) {
console.log(
'Error: Got socket connecion with wrong token ' +
token +
' for slot ' +
slot
);
throw new ValidationError('Invalid token');
}
console.log('Got sender socket connection on slot ' + slot);
message = 'Socket authenticated';
socket.cameraSlot = slot;
socket.cameraSlotToken = token;
registerSenderHandlers(socket);
} catch (e) {
if (e instanceof ValidationError) {
success = false;
message = e.message;
} else {
throw e;
}
}
fn({ success, message });
};
import { Server as SocketIOServer } from 'socket.io';
import { config } from '../config/config';
export const socketIO = new SocketIOServer(config.port);
export interface CommandDescriptor {
command: string;
params: string[];
}
import { config } from '../config/config';
import { CommandDescriptor } from '../models/command-descriptor';
type NullableString = string | null;
export class CameraSlotState {
class SingleCameraSlotState {
slotActive = false;
token: NullableString = null;
feedActive = false;
......@@ -20,3 +18,11 @@ export class CameraSlotState {
params: ['rb', '0', '0', '200', '200']
};
}
const cameraSlotState: SingleCameraSlotState[] = [];
for (let i = 0; i < config.cameraSlots; i++) {
cameraSlotState.push(new SingleCameraSlotState());
}
export { cameraSlotState };
import { Server as SocketIOServer } from 'socket.io';
import { socketIO } from '../socket-io/socket-io';
interface ExitHandlerOptions {
cleanup?: boolean;
exit?: boolean;
}
export const mountCleanupLogic = (io: SocketIOServer) => {
const cleanup = () => {
console.log('cleanup');
io.emit('remove_all_feeds');
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();
}
};
export const registerCleanupLogic = () => {
// do something when app is closing
process.on('exit', exitHandler.bind(null, { cleanup: true }));
......@@ -32,4 +32,4 @@ export const mountCleanupLogic = (io: SocketIOServer) => {
// catches termination
process.on('SIGTERM', exitHandler.bind(null, { exit: true }));
}
\ No newline at end of file
};
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment