Emergency Alert System Integration for Digital Signage
Digital signage serves as a critical communication channel during emergencies, reaching people who may not have access to phones or email. This comprehensive guide covers integration with emergency alert systems, from national warning systems to building-specific mass notification platforms.
Emergency Communication Architecture
System Overview
A robust emergency signage system integrates multiple alert sources:
┌─────────────────────────────────────────────────────────────────────────────┐
│ Emergency Alert System Architecture │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ External Alert Sources Internal Alert Sources │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ National Systems │ │ Building Systems │ │
│ │ • IPAWS/WEA │ │ • Fire Alarm Panel │ │
│ │ • NOAA Weather │ │ • Security System │ │
│ │ • Amber Alerts │ │ • Access Control │ │
│ │ • Local EMA │ │ • BMS Integration │ │
│ └─────────┬───────────┘ └─────────┬───────────┘ │
│ │ │ │
│ │ ┌─────────────────────┐ │ │
│ │ │ Mass Notification │ │ │
│ └────────▶│ Platform │◀───────┘ │
│ │ • Everbridge │ │
│ │ • AlertMedia │ │
│ │ • Rave Mobile │ │
│ │ • Singlewire │ │
│ └─────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────┐ │
│ │ Alert Processing │ │
│ │ Engine │ │
│ │ • Priority Routing │ │
│ │ • Geo-targeting │ │
│ │ • Content Transform │ │
│ └─────────┬───────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Digital Signage Network │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Lobby │ │ Hallway │ │ Cafete- │ │ Meeting │ │ Outdoor │ │ │
│ │ │ Display │ │ Display │ │ ria │ │ Rooms │ │ Signs │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Alert Priority Hierarchy
| Priority Level | Examples | Display Behavior | Override Rights |
|---|---|---|---|
| P1 - Critical | Active shooter, fire, explosion, gas leak | Full screen takeover, audio alert, strobe activation | Overrides all content |
| P2 - Urgent | Severe weather, evacuation order, lockdown | Full screen with countdown, repeated display | Overrides normal content |
| P3 - Warning | Weather watch, suspicious activity, system issue | Banner overlay or split screen | Interrupts rotation |
| P4 - Advisory | Building closure, safety reminder, drill announcement | Ticker/crawl or scheduled slot | Queued display |
| P5 - Informational | Drill complete, all clear, policy reminder | Standard content slot | Normal scheduling |
Common Alerting Protocol (CAP) Integration
Understanding CAP Format
The Common Alerting Protocol (CAP) is the international standard for emergency alerts:
<?xml version="1.0" encoding="UTF-8"?>
<alert xmlns="urn:oasis:names:tc:emergency:cap:1.2">
<identifier>NWS-IDP-PROD-4567890</identifier>
<sender>w-nws.webmaster@noaa.gov</sender>
<sent>2025-01-15T14:30:00-05:00</sent>
<status>Actual</status>
<msgType>Alert</msgType>
<scope>Public</scope>
<code>IPAWSv1.0</code>
<info>
<language>en-US</language>
<category>Met</category>
<event>Tornado Warning</event>
<responseType>Shelter</responseType>
<urgency>Immediate</urgency>
<severity>Extreme</severity>
<certainty>Observed</certainty>
<effective>2025-01-15T14:30:00-05:00</effective>
<expires>2025-01-15T15:30:00-05:00</expires>
<senderName>NWS Chicago IL</senderName>
<headline>Tornado Warning issued for Cook County</headline>
<description>
The National Weather Service in Chicago has issued a
Tornado Warning for southeastern Cook County until 3:30 PM CST.
A tornado was observed near Oak Lawn moving northeast at 35 mph.
</description>
<instruction>
TAKE COVER NOW! Move to an interior room on the lowest floor
of a sturdy building. Avoid windows.
</instruction>
<area>
<areaDesc>Southeastern Cook County</areaDesc>
<polygon>41.7,-87.7 41.7,-87.5 41.6,-87.5 41.6,-87.7 41.7,-87.7</polygon>
<geocode>
<valueName>FIPS6</valueName>
<value>017031</value>
</geocode>
</area>
</info>
</alert>
CAP Parser Implementation
const xml2js = require('xml2js');
class CAPParser {
constructor() {
this.parser = new xml2js.Parser({
explicitArray: false,
mergeAttrs: true
});
}
async parse(capXml) {
const result = await this.parser.parseStringPromise(capXml);
const alert = result.alert;
// Handle multiple info blocks (multilingual alerts)
const infos = Array.isArray(alert.info) ? alert.info : [alert.info];
return {
identifier: alert.identifier,
sender: alert.sender,
sent: new Date(alert.sent),
status: alert.status, // Actual, Exercise, System, Test, Draft
msgType: alert.msgType, // Alert, Update, Cancel, Ack, Error
scope: alert.scope, // Public, Restricted, Private
alerts: infos.map(info => ({
language: info.language || 'en-US',
category: info.category, // Geo, Met, Safety, Security, Rescue, Fire, Health, Env, Transport, Infra, CBRNE, Other
event: info.event,
responseType: info.responseType, // Shelter, Evacuate, Prepare, Execute, Avoid, Monitor, Assess, AllClear, None
urgency: info.urgency, // Immediate, Expected, Future, Past, Unknown
severity: info.severity, // Extreme, Severe, Moderate, Minor, Unknown
certainty: info.certainty, // Observed, Likely, Possible, Unlikely, Unknown
effective: info.effective ? new Date(info.effective) : null,
expires: info.expires ? new Date(info.expires) : null,
headline: info.headline,
description: info.description,
instruction: info.instruction,
area: this.parseArea(info.area)
}))
};
}
parseArea(area) {
if (!area) return null;
return {
description: area.areaDesc,
polygon: area.polygon ? this.parsePolygon(area.polygon) : null,
circle: area.circle ? this.parseCircle(area.circle) : null,
geocodes: this.parseGeocodes(area.geocode)
};
}
parsePolygon(polygonString) {
return polygonString.split(' ').map(coord => {
const [lat, lon] = coord.split(',').map(Number);
return { lat, lon };
});
}
parseCircle(circleString) {
const [center, radius] = circleString.split(' ');
const [lat, lon] = center.split(',').map(Number);
return { lat, lon, radiusKm: Number(radius) };
}
parseGeocodes(geocode) {
if (!geocode) return [];
const codes = Array.isArray(geocode) ? geocode : [geocode];
return codes.map(gc => ({
type: gc.valueName,
value: gc.value
}));
}
}
CAP Feed Monitoring
// Monitor multiple CAP feeds
class CAPFeedMonitor {
constructor(config) {
this.feeds = config.feeds;
this.pollInterval = config.pollInterval || 30000; // 30 seconds
this.processedAlerts = new Set();
this.parser = new CAPParser();
}
start() {
// Initial fetch
this.checkAllFeeds();
// Periodic polling
this.intervalId = setInterval(() => {
this.checkAllFeeds();
}, this.pollInterval);
}
async checkAllFeeds() {
for (const feed of this.feeds) {
try {
await this.checkFeed(feed);
} catch (error) {
console.error(`Error checking feed ${feed.name}:`, error);
}
}
}
async checkFeed(feed) {
const response = await fetch(feed.url, {
headers: feed.headers || {}
});
const content = await response.text();
// Handle Atom feed wrapping CAP alerts
if (feed.format === 'atom') {
await this.processAtomFeed(content, feed);
} else {
// Direct CAP XML
await this.processCapAlert(content, feed);
}
}
async processAtomFeed(atomXml, feed) {
const parser = new xml2js.Parser({ explicitArray: false });
const result = await parser.parseStringPromise(atomXml);
const entries = result.feed?.entry || [];
const entryList = Array.isArray(entries) ? entries : [entries];
for (const entry of entryList) {
const alertId = entry.id;
if (this.processedAlerts.has(alertId)) continue;
// CAP content may be embedded or linked
let capContent;
if (entry.content?.alert) {
capContent = entry.content;
} else if (entry.link?.href) {
const capResponse = await fetch(entry.link.href);
capContent = await capResponse.text();
}
if (capContent) {
await this.processCapAlert(capContent, feed, alertId);
}
}
}
async processCapAlert(capXml, feed, externalId = null) {
const alert = await this.parser.parse(capXml);
const alertId = externalId || alert.identifier;
if (this.processedAlerts.has(alertId)) return;
// Apply geographic filtering
if (feed.geoFilter && !this.matchesGeoFilter(alert, feed.geoFilter)) {
return;
}
// Apply category filtering
if (feed.categoryFilter) {
const categories = alert.alerts.map(a => a.category);
if (!categories.some(c => feed.categoryFilter.includes(c))) {
return;
}
}
this.processedAlerts.add(alertId);
// Emit for processing
this.emit('alert', {
source: feed.name,
alert,
receivedAt: new Date()
});
}
matchesGeoFilter(alert, geoFilter) {
for (const alertInfo of alert.alerts) {
if (!alertInfo.area) continue;
// Check FIPS codes
if (geoFilter.fipsCodes) {
const alertFips = alertInfo.area.geocodes
.filter(gc => gc.type === 'FIPS6')
.map(gc => gc.value);
if (alertFips.some(fips => geoFilter.fipsCodes.includes(fips))) {
return true;
}
}
// Check polygon intersection
if (geoFilter.polygon && alertInfo.area.polygon) {
if (polygonsIntersect(geoFilter.polygon, alertInfo.area.polygon)) {
return true;
}
}
// Check point within area
if (geoFilter.point && alertInfo.area.polygon) {
if (pointInPolygon(geoFilter.point, alertInfo.area.polygon)) {
return true;
}
}
}
return false;
}
}
// Usage
const monitor = new CAPFeedMonitor({
feeds: [
{
name: 'NWS Alerts',
url: 'https://alerts.weather.gov/cap/us.php?x=1',
format: 'atom',
geoFilter: {
fipsCodes: ['017031', '017043'] // Cook and DuPage County, IL
}
},
{
name: 'State EMA',
url: 'https://ema.state.gov/cap/alerts.xml',
format: 'cap',
categoryFilter: ['Safety', 'Security', 'Fire']
}
],
pollInterval: 30000
});
monitor.on('alert', async (data) => {
await processEmergencyAlert(data);
});
monitor.start();
IPAWS Integration
Understanding IPAWS
The Integrated Public Alert and Warning System (IPAWS) is FEMA's national alerting infrastructure:
IPAWS Alert Types:
- WEA (Wireless Emergency Alerts): Cell broadcast to mobile devices
- EAS (Emergency Alert System): Radio/TV broadcast
- NWEM (Non-Weather Emergency Messages): Government emergency alerts
IPAWS Lab Integration
For organizations authorized to originate IPAWS alerts:
// IPAWS OPEN (Open Platform for Emergency Networks) Integration
const IPAWSClient = {
config: {
cogId: process.env.IPAWS_COG_ID,
serverUrl: 'https://tdl.integration.aws.fema.gov', // Test environment
// Production: https://apps.fema.gov
certificatePath: '/path/to/ipaws-cert.p12',
certificatePassword: process.env.IPAWS_CERT_PASSWORD
},
// Post alert to IPAWS
postAlert: async function(capAlert) {
const https = require('https');
const fs = require('fs');
const pfx = fs.readFileSync(this.config.certificatePath);
const agent = new https.Agent({
pfx: pfx,
passphrase: this.config.certificatePassword
});
const response = await fetch(
`${this.config.serverUrl}/IPAWS_CAPService/IPAWS`,
{
method: 'POST',
headers: {
'Content-Type': 'application/xml'
},
body: capAlert,
agent: agent
}
);
return response.text();
},
// Get alerts from IPAWS
getAlerts: async function(parameters) {
const queryString = new URLSearchParams(parameters).toString();
const response = await fetch(
`${this.config.serverUrl}/IPAWS_CAPService/IPAWS?${queryString}`,
{
headers: {
'Accept': 'application/xml'
}
}
);
return response.text();
}
};
Mass Notification Platform Integration
Everbridge Integration
// Everbridge API Integration
class EverbridgeClient {
constructor(config) {
this.baseUrl = 'https://api.everbridge.net/rest';
this.organizationId = config.organizationId;
this.username = config.username;
this.password = config.password;
}
getAuthHeader() {
const credentials = Buffer.from(`${this.username}:${this.password}`).toString('base64');
return `Basic ${credentials}`;
}
// Subscribe to notifications via webhook
async createWebhook(webhookUrl, events) {
const response = await fetch(
`${this.baseUrl}/organizations/${this.organizationId}/webhooks`,
{
method: 'POST',
headers: {
'Authorization': this.getAuthHeader(),
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: webhookUrl,
events: events, // ['notification.sent', 'notification.confirmed']
active: true
})
}
);
return response.json();
}
// Get active incidents
async getActiveIncidents() {
const response = await fetch(
`${this.baseUrl}/organizations/${this.organizationId}/incidents?status=Open`,
{
headers: {
'Authorization': this.getAuthHeader()
}
}
);
return response.json();
}
// Get notification details
async getNotification(notificationId) {
const response = await fetch(
`${this.baseUrl}/organizations/${this.organizationId}/notifications/${notificationId}`,
{
headers: {
'Authorization': this.getAuthHeader()
}
}
);
return response.json();
}
}
// Webhook handler for Everbridge notifications
const handleEverbridgeWebhook = async (req, res) => {
const payload = req.body;
// Verify webhook signature
const signature = req.headers['x-everbridge-signature'];
if (!verifyWebhookSignature(payload, signature)) {
return res.status(401).send('Invalid signature');
}
const notification = payload.notification;
// Transform to signage alert format
const signageAlert = {
id: notification.notificationId,
source: 'everbridge',
priority: mapEverbridgePriority(notification.priority),
title: notification.subject,
message: notification.body,
type: notification.incidentType,
startTime: new Date(notification.createdDate),
expiresAt: notification.expirationDate ? new Date(notification.expirationDate) : null,
targetGroups: notification.contactGroups,
responseOptions: notification.responseOptions
};
await processSignageAlert(signageAlert);
res.status(200).send('OK');
};
AlertMedia Integration
// AlertMedia API Integration
class AlertMediaClient {
constructor(config) {
this.baseUrl = 'https://api.alertmedia.com/api/v1';
this.apiKey = config.apiKey;
this.apiSecret = config.apiSecret;
}
getHeaders() {
return {
'X-AlertMedia-Key': this.apiKey,
'X-AlertMedia-Secret': this.apiSecret,
'Content-Type': 'application/json'
};
}
// Get active alerts
async getActiveAlerts() {
const response = await fetch(
`${this.baseUrl}/notifications?status=active`,
{ headers: this.getHeaders() }
);
return response.json();
}
// Create signage-specific channel
async sendToSignage(alertData) {
const response = await fetch(
`${this.baseUrl}/notifications`,
{
method: 'POST',
headers: this.getHeaders(),
body: JSON.stringify({
channels: ['digital_signage'],
subject: alertData.title,
message: alertData.message,
priority: alertData.priority,
groups: alertData.targetGroups,
expires_at: alertData.expiresAt
})
}
);
return response.json();
}
}
Singlewire InformaCast Integration
// InformaCast Integration for Cisco environments
class InformaCastClient {
constructor(config) {
this.baseUrl = config.serverUrl; // On-premise or cloud
this.apiToken = config.apiToken;
}
// Subscribe to bell schedule and emergency broadcasts
async subscribeToEndpoint(endpointId, webhookUrl) {
const response = await fetch(
`${this.baseUrl}/api/v1/endpoints/${endpointId}/subscriptions`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
webhook_url: webhookUrl,
events: ['broadcast.start', 'broadcast.end', 'emergency.activate']
})
}
);
return response.json();
}
// Get current broadcast status
async getBroadcastStatus() {
const response = await fetch(
`${this.baseUrl}/api/v1/broadcasts/active`,
{
headers: { 'Authorization': `Bearer ${this.apiToken}` }
}
);
return response.json();
}
}
Building System Integration
Fire Alarm Panel Integration
Connect to fire alarm control panels for automatic emergency display:
// Fire Alarm Panel Integration (Notifier, SimplexGrinnell, etc.)
class FireAlarmIntegration {
constructor(config) {
this.panelType = config.panelType;
this.connectionType = config.connectionType; // 'serial', 'ip', 'bacnet'
this.connection = null;
}
// Serial connection to fire panel
async connectSerial(port, baudRate = 9600) {
const SerialPort = require('serialport');
this.connection = new SerialPort(port, {
baudRate: baudRate,
dataBits: 8,
parity: 'none',
stopBits: 1
});
const Readline = require('@serialport/parser-readline');
const parser = this.connection.pipe(new Readline({ delimiter: '\r\n' }));
parser.on('data', (data) => {
this.handlePanelMessage(data);
});
}
// BACnet integration
async connectBACnet(deviceId, ipAddress) {
const bacnet = require('bacstack');
this.client = new bacnet();
// Subscribe to fire alarm points
this.client.subscribeCOV(ipAddress, {
objectType: bacnet.enum.ObjectTypes.OBJECT_BINARY_VALUE,
instance: deviceId
}, false, false, 0);
this.client.on('covNotify', (data) => {
this.handleBACnetNotification(data);
});
}
handlePanelMessage(message) {
// Parse panel-specific protocol
const event = this.parseProtocol(message);
if (event.type === 'ALARM') {
this.emit('alarm', {
zone: event.zone,
device: event.device,
alarmType: event.alarmType, // 'fire', 'smoke', 'waterflow', 'manual_pull'
timestamp: new Date()
});
} else if (event.type === 'TROUBLE') {
this.emit('trouble', event);
} else if (event.type === 'SUPERVISORY') {
this.emit('supervisory', event);
}
}
parseProtocol(message) {
// Implementation varies by panel manufacturer
// Example: Notifier protocol parsing
if (this.panelType === 'notifier') {
return this.parseNotifierProtocol(message);
}
// Add other manufacturers as needed
}
}
// Usage with signage
const fireAlarm = new FireAlarmIntegration({
panelType: 'notifier',
connectionType: 'serial'
});
fireAlarm.on('alarm', async (event) => {
// Immediate full-screen takeover
await signageController.triggerEmergency({
type: 'FIRE_ALARM',
priority: 1,
zones: [event.zone],
content: {
headline: 'FIRE ALARM ACTIVATED',
instruction: `Evacuate immediately via nearest exit. Do not use elevators.`,
zone: event.zone,
color: '#FF0000'
},
audio: 'fire_alarm.mp3',
strobe: true
});
});
Access Control Integration
// Access Control System Integration
class AccessControlIntegration {
constructor(config) {
this.provider = config.provider; // 'genetec', 'lenel', 'ccure', 'brivo'
this.apiClient = this.createApiClient(config);
}
// Monitor for lockdown events
async monitorLockdownStatus() {
// Genetec Security Center example
if (this.provider === 'genetec') {
const ws = new WebSocket(this.config.websocketUrl);
ws.on('message', (data) => {
const event = JSON.parse(data);
if (event.type === 'AreaStateChange') {
if (event.state === 'Lockdown') {
this.emit('lockdown', {
area: event.areaName,
level: event.lockdownLevel,
initiatedBy: event.operator
});
} else if (event.state === 'Normal') {
this.emit('lockdown_cleared', {
area: event.areaName
});
}
}
});
}
}
// Trigger lockdown from signage (two-way integration)
async initiateLockdown(areaId, level, reason) {
// Requires appropriate authorization
const response = await this.apiClient.post('/areas/lockdown', {
areaId,
level,
reason,
initiatedBy: 'digital_signage_system'
});
return response.data;
}
}
// Integration with signage
const accessControl = new AccessControlIntegration({
provider: 'genetec',
websocketUrl: 'wss://security-center.company.com/events'
});
accessControl.on('lockdown', async (event) => {
await signageController.triggerEmergency({
type: 'LOCKDOWN',
priority: 1,
content: {
headline: 'LOCKDOWN IN EFFECT',
instruction: 'Remain in current location. Lock doors. Stay away from windows. Await further instructions.',
area: event.area
},
audio: 'lockdown_announcement.mp3'
});
});
Emergency Display Design
Alert Template System
// Emergency display template configuration
const emergencyTemplates = {
FIRE: {
backgroundColor: '#FF0000',
textColor: '#FFFFFF',
iconUrl: '/assets/icons/fire-emergency.svg',
headline: 'FIRE ALARM',
defaultInstruction: 'Evacuate immediately via nearest exit. Do not use elevators.',
audioFile: 'fire_alarm_tone.mp3',
flashInterval: 500, // ms
showEvacuationMap: true
},
TORNADO: {
backgroundColor: '#FF6600',
textColor: '#FFFFFF',
iconUrl: '/assets/icons/tornado.svg',
headline: 'TORNADO WARNING',
defaultInstruction: 'Move to interior room on lowest floor. Stay away from windows.',
audioFile: 'tornado_siren.mp3',
flashInterval: 1000,
showShelterLocations: true
},
LOCKDOWN: {
backgroundColor: '#CC0000',
textColor: '#FFFFFF',
iconUrl: '/assets/icons/lockdown.svg',
headline: 'LOCKDOWN',
defaultInstruction: 'Lock doors. Turn off lights. Hide. Remain silent.',
audioFile: 'lockdown_tone.mp3',
flashInterval: 0, // No flashing - don't draw attention
showSecureAreas: false
},
ACTIVE_THREAT: {
backgroundColor: '#990000',
textColor: '#FFFFFF',
iconUrl: '/assets/icons/warning.svg',
headline: 'ACTIVE THREAT',
defaultInstruction: 'RUN if safe path exists. HIDE if escape not possible. FIGHT as last resort.',
audioFile: null, // Silent - don't alert threat to locations
flashInterval: 0,
showSecureAreas: false
},
SHELTER_IN_PLACE: {
backgroundColor: '#0066CC',
textColor: '#FFFFFF',
iconUrl: '/assets/icons/shelter.svg',
headline: 'SHELTER IN PLACE',
defaultInstruction: 'Remain indoors. Close windows and doors. Await further instructions.',
audioFile: 'shelter_tone.mp3',
flashInterval: 2000,
showShelterLocations: true
},
EVACUATION: {
backgroundColor: '#FF9900',
textColor: '#000000',
iconUrl: '/assets/icons/evacuation.svg',
headline: 'EVACUATION ORDER',
defaultInstruction: 'Proceed to nearest exit calmly. Assist others if safe. Report to assembly area.',
audioFile: 'evacuation_tone.mp3',
flashInterval: 1000,
showEvacuationMap: true,
showAssemblyPoints: true
},
ALL_CLEAR: {
backgroundColor: '#00AA00',
textColor: '#FFFFFF',
iconUrl: '/assets/icons/checkmark.svg',
headline: 'ALL CLEAR',
defaultInstruction: 'The emergency has ended. Normal operations may resume.',
audioFile: 'all_clear_tone.mp3',
flashInterval: 0,
duration: 300000 // Display for 5 minutes then return to normal
},
MEDICAL: {
backgroundColor: '#FFFFFF',
textColor: '#FF0000',
iconUrl: '/assets/icons/medical.svg',
headline: 'MEDICAL EMERGENCY',
defaultInstruction: 'Clear the area. AED located [location]. Do not move patient unless danger exists.',
audioFile: 'medical_alert.mp3',
flashInterval: 1500
}
};
Responsive Emergency Layout
/* Emergency alert display styles */
.emergency-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 99999;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.emergency-header {
display: flex;
align-items: center;
justify-content: center;
gap: 2vw;
margin-bottom: 4vh;
}
.emergency-icon {
width: 15vmin;
height: 15vmin;
}
.emergency-headline {
font-size: clamp(48px, 12vmin, 200px);
font-weight: 900;
text-transform: uppercase;
letter-spacing: 0.05em;
text-shadow: 4px 4px 8px rgba(0, 0, 0, 0.5);
animation: pulse 1s ease-in-out infinite;
}
.emergency-instruction {
font-size: clamp(24px, 6vmin, 100px);
font-weight: 600;
text-align: center;
max-width: 90vw;
line-height: 1.4;
padding: 2vh 4vw;
background: rgba(0, 0, 0, 0.3);
border-radius: 1vmin;
}
.emergency-timestamp {
position: absolute;
bottom: 2vh;
right: 2vw;
font-size: clamp(14px, 2vmin, 32px);
opacity: 0.8;
}
/* Flashing effect */
@keyframes flash {
0%, 50% { opacity: 1; }
51%, 100% { opacity: 0.7; }
}
.emergency-flashing {
animation: flash var(--flash-interval) linear infinite;
}
/* Pulse animation for headline */
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.02); }
}
/* Evacuation map overlay */
.evacuation-map {
position: absolute;
bottom: 10vh;
left: 5vw;
width: 40vw;
height: 35vh;
background: white;
border-radius: 1vmin;
padding: 1vmin;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}
.evacuation-map img {
width: 100%;
height: 100%;
object-fit: contain;
}
/* Assembly point indicator */
.assembly-point {
position: absolute;
bottom: 10vh;
right: 5vw;
background: rgba(0, 0, 0, 0.7);
padding: 2vmin;
border-radius: 1vmin;
}
.assembly-point-label {
font-size: clamp(14px, 3vmin, 40px);
margin-bottom: 1vh;
}
.assembly-point-location {
font-size: clamp(20px, 4vmin, 60px);
font-weight: bold;
}
Emergency Display Component
// React component for emergency display
const EmergencyDisplay = ({ alert, onDismiss }) => {
const template = emergencyTemplates[alert.type] || emergencyTemplates.EVACUATION;
const [currentTime, setCurrentTime] = useState(new Date());
useEffect(() => {
// Update clock every second
const timer = setInterval(() => setCurrentTime(new Date()), 1000);
return () => clearInterval(timer);
}, []);
useEffect(() => {
// Play audio alert
if (template.audioFile) {
const audio = new Audio(`/assets/audio/${template.audioFile}`);
audio.loop = true;
audio.play();
return () => {
audio.pause();
audio.currentTime = 0;
};
}
}, [template.audioFile]);
useEffect(() => {
// Auto-dismiss for ALL_CLEAR
if (template.duration) {
const timer = setTimeout(onDismiss, template.duration);
return () => clearTimeout(timer);
}
}, [template.duration, onDismiss]);
const style = {
'--flash-interval': `${template.flashInterval}ms`,
backgroundColor: template.backgroundColor,
color: template.textColor
};
return (
<div
className={`emergency-overlay ${template.flashInterval > 0 ? 'emergency-flashing' : ''}`}
style={style}
>
<div className="emergency-header">
<img src={template.iconUrl} alt="" className="emergency-icon" />
<h1 className="emergency-headline">{alert.headline || template.headline}</h1>
</div>
<p className="emergency-instruction">
{alert.instruction || template.defaultInstruction}
</p>
{alert.additionalInfo && (
<p className="emergency-additional">{alert.additionalInfo}</p>
)}
{template.showEvacuationMap && alert.evacuationMapUrl && (
<div className="evacuation-map">
<img src={alert.evacuationMapUrl} alt="Evacuation route" />
</div>
)}
{template.showAssemblyPoints && alert.assemblyPoint && (
<div className="assembly-point">
<div className="assembly-point-label">Report to:</div>
<div className="assembly-point-location">{alert.assemblyPoint}</div>
</div>
)}
<div className="emergency-timestamp">
Alert issued: {format(alert.startTime, 'h:mm:ss a')} |
Current time: {format(currentTime, 'h:mm:ss a')}
</div>
</div>
);
};
Geo-Targeted Alert Routing
Zone-Based Display Targeting
// Zone-based alert routing
class AlertRouter {
constructor(displayRegistry) {
this.displayRegistry = displayRegistry;
}
// Route alert to appropriate displays based on zones
async routeAlert(alert) {
let targetDisplays = [];
// P1 alerts go to ALL displays
if (alert.priority === 1) {
targetDisplays = await this.displayRegistry.getAllDisplays();
}
// Zone-specific routing for lower priorities
else if (alert.targetZones?.length > 0) {
targetDisplays = await this.displayRegistry.getDisplaysByZones(alert.targetZones);
}
// Building-specific
else if (alert.targetBuildings?.length > 0) {
targetDisplays = await this.displayRegistry.getDisplaysByBuildings(alert.targetBuildings);
}
// Floor-specific
else if (alert.targetFloors?.length > 0) {
targetDisplays = await this.displayRegistry.getDisplaysByFloors(alert.targetFloors);
}
// Default: all displays in affected area
else {
targetDisplays = await this.displayRegistry.getDisplaysByGeoArea(alert.affectedArea);
}
// Apply display type filtering
if (alert.displayTypes) {
targetDisplays = targetDisplays.filter(d =>
alert.displayTypes.includes(d.type)
);
}
return targetDisplays;
}
// Send alert to targeted displays
async sendAlert(alert) {
const displays = await this.routeAlert(alert);
const results = await Promise.allSettled(
displays.map(display =>
this.sendToDisplay(display, alert)
)
);
// Log delivery status
const delivered = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
console.log(`Alert ${alert.id} delivered to ${delivered}/${displays.length} displays (${failed} failed)`);
return {
alertId: alert.id,
totalDisplays: displays.length,
delivered,
failed,
failures: results
.filter(r => r.status === 'rejected')
.map((r, i) => ({
displayId: displays[i].id,
error: r.reason.message
}))
};
}
async sendToDisplay(display, alert) {
// WebSocket push to display
const ws = await this.getDisplayConnection(display.id);
if (!ws || ws.readyState !== WebSocket.OPEN) {
throw new Error(`Display ${display.id} not connected`);
}
ws.send(JSON.stringify({
type: 'EMERGENCY_ALERT',
alert: {
id: alert.id,
type: alert.type,
priority: alert.priority,
headline: alert.headline,
instruction: alert.instruction,
startTime: alert.startTime,
expiresAt: alert.expiresAt,
additionalInfo: alert.additionalInfo,
evacuationMapUrl: this.getEvacuationMap(display),
assemblyPoint: this.getAssemblyPoint(display)
}
}));
// Wait for acknowledgment
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Acknowledgment timeout'));
}, 5000);
const handler = (message) => {
const data = JSON.parse(message);
if (data.type === 'ALERT_ACK' && data.alertId === alert.id) {
clearTimeout(timeout);
ws.off('message', handler);
resolve({ displayId: display.id, ackTime: new Date() });
}
};
ws.on('message', handler);
});
}
getEvacuationMap(display) {
// Return floor-specific evacuation map
return `/assets/evacuation-maps/${display.building}-${display.floor}.svg`;
}
getAssemblyPoint(display) {
// Return designated assembly point for display location
const assemblyPoints = {
'building-a-floor-1': 'Parking Lot A - North',
'building-a-floor-2': 'Parking Lot A - North',
'building-a-floor-3': 'Parking Lot A - South',
'building-b-floor-1': 'Parking Lot B',
// ...
};
return assemblyPoints[`${display.building}-${display.floor}`] || 'Designated Assembly Area';
}
}
Testing and Compliance
Emergency Drill Support
// Emergency drill management
class DrillManager {
constructor(alertSystem) {
this.alertSystem = alertSystem;
this.activeDrills = new Map();
}
// Schedule and execute a drill
async scheduleDrill(drillConfig) {
const drill = {
id: generateId(),
type: drillConfig.type,
scheduledTime: new Date(drillConfig.scheduledTime),
duration: drillConfig.duration || 300000, // 5 minutes default
zones: drillConfig.zones || 'all',
notifyBeforeMinutes: drillConfig.notifyBeforeMinutes || 5,
requireAcknowledgment: drillConfig.requireAcknowledgment || false,
status: 'scheduled'
};
await this.saveDrill(drill);
// Schedule pre-drill notification
if (drill.notifyBeforeMinutes > 0) {
const notifyTime = subMinutes(drill.scheduledTime, drill.notifyBeforeMinutes);
schedule.scheduleJob(notifyTime, () => this.sendPreDrillNotification(drill));
}
// Schedule drill execution
schedule.scheduleJob(drill.scheduledTime, () => this.executeDrill(drill));
return drill;
}
async sendPreDrillNotification(drill) {
await this.alertSystem.sendAlert({
type: 'DRILL_NOTICE',
priority: 4, // Advisory
headline: `UPCOMING ${drill.type.toUpperCase()} DRILL`,
instruction: `A ${drill.type} drill will begin in ${drill.notifyBeforeMinutes} minutes. This is only a drill.`,
targetZones: drill.zones,
expiresAt: drill.scheduledTime
});
}
async executeDrill(drill) {
drill.status = 'active';
drill.startedAt = new Date();
this.activeDrills.set(drill.id, drill);
// Send drill alert with DRILL indicator
await this.alertSystem.sendAlert({
type: drill.type,
priority: 2, // Urgent but marked as drill
headline: `${emergencyTemplates[drill.type].headline} - DRILL`,
instruction: `${emergencyTemplates[drill.type].defaultInstruction}\n\nTHIS IS A DRILL`,
targetZones: drill.zones,
isDrill: true,
drillId: drill.id,
expiresAt: addMilliseconds(new Date(), drill.duration)
});
// Schedule drill end
setTimeout(() => this.endDrill(drill), drill.duration);
return drill;
}
async endDrill(drill) {
drill.status = 'completed';
drill.endedAt = new Date();
this.activeDrills.delete(drill.id);
// Send all-clear with drill summary
await this.alertSystem.sendAlert({
type: 'ALL_CLEAR',
priority: 5,
headline: 'DRILL COMPLETE',
instruction: `The ${drill.type} drill has concluded. Thank you for your participation.`,
targetZones: drill.zones,
drillId: drill.id
});
// Generate drill report
const report = await this.generateDrillReport(drill);
await this.saveDrillReport(report);
return report;
}
async generateDrillReport(drill) {
const deliveryStats = await this.alertSystem.getDeliveryStats(drill.id);
const acknowledgments = await this.getAcknowledgments(drill.id);
return {
drillId: drill.id,
type: drill.type,
scheduledTime: drill.scheduledTime,
actualStartTime: drill.startedAt,
endTime: drill.endedAt,
duration: drill.endedAt - drill.startedAt,
zones: drill.zones,
displayStats: {
totalDisplays: deliveryStats.totalDisplays,
deliveredTo: deliveryStats.delivered,
failed: deliveryStats.failed,
averageDeliveryTime: deliveryStats.averageDeliveryTime
},
acknowledgments: drill.requireAcknowledgment ? {
required: acknowledgments.required,
received: acknowledgments.received,
percentage: (acknowledgments.received / acknowledgments.required * 100).toFixed(1)
} : null,
issues: deliveryStats.failures
};
}
}
Compliance Documentation
// Generate compliance reports for inspectors
const generateComplianceReport = async (dateRange) => {
const { startDate, endDate } = dateRange;
// Gather all emergency-related data
const alerts = await getAlertHistory(startDate, endDate);
const drills = await getDrillHistory(startDate, endDate);
const systemTests = await getSystemTestHistory(startDate, endDate);
const maintenanceRecords = await getMaintenanceHistory(startDate, endDate);
return {
reportPeriod: { startDate, endDate },
generatedAt: new Date(),
systemInventory: {
totalDisplays: await getDisplayCount(),
displaysByBuilding: await getDisplayCountByBuilding(),
displaysByType: await getDisplayCountByType()
},
alertCapabilities: {
supportedAlertTypes: Object.keys(emergencyTemplates),
integrations: {
capFeeds: await getConfiguredCAPFeeds(),
massNotificationPlatforms: await getConfiguredMNS(),
buildingSystems: await getConfiguredBuildingIntegrations()
}
},
alertHistory: {
totalAlerts: alerts.length,
byType: groupBy(alerts, 'type'),
byPriority: groupBy(alerts, 'priority'),
averageDeliveryTime: calculateAverageDeliveryTime(alerts),
deliverySuccessRate: calculateDeliverySuccessRate(alerts)
},
drillHistory: {
totalDrills: drills.length,
byType: groupBy(drills, 'type'),
complianceRate: calculateDrillComplianceRate(drills),
drillDetails: drills.map(d => ({
date: d.scheduledTime,
type: d.type,
duration: d.duration,
deliveryRate: d.report?.displayStats?.delivered / d.report?.displayStats?.totalDisplays
}))
},
systemTesting: {
lastFullTest: systemTests[systemTests.length - 1],
testFrequency: calculateTestFrequency(systemTests),
passRate: systemTests.filter(t => t.passed).length / systemTests.length
},
maintenance: {
scheduledMaintenance: maintenanceRecords.filter(m => m.type === 'scheduled'),
unscheduledMaintenance: maintenanceRecords.filter(m => m.type === 'unscheduled'),
averageDowntime: calculateAverageDowntime(maintenanceRecords)
},
complianceChecklist: {
annualDrillsCompleted: drills.length >= getRequiredAnnualDrills(),
monthlyTestsCompleted: systemTests.filter(t =>
differenceInMonths(new Date(), t.date) < 1
).length > 0,
maintenanceUpToDate: !maintenanceRecords.some(m => m.status === 'overdue'),
allDisplaysOperational: await checkAllDisplaysOperational()
}
};
};
Audio Alert Integration
Text-to-Speech for Dynamic Alerts
// TTS integration for emergency announcements
class EmergencyTTS {
constructor(config) {
this.provider = config.provider; // 'google', 'amazon', 'azure'
this.voice = config.voice || 'en-US-Neural2-D'; // Clear, authoritative voice
this.speakingRate = config.speakingRate || 0.9; // Slightly slower for clarity
}
async generateAudioAlert(alertText) {
// Add SSML for better emergency announcement delivery
const ssml = `
<speak>
<prosody rate="${this.speakingRate}" pitch="-2st">
<emphasis level="strong">Attention.</emphasis>
<break time="500ms"/>
${alertText}
<break time="1s"/>
<emphasis level="strong">Repeat.</emphasis>
<break time="500ms"/>
${alertText}
</prosody>
</speak>
`;
if (this.provider === 'google') {
return this.generateGoogleTTS(ssml);
} else if (this.provider === 'amazon') {
return this.generateAmazonPolly(ssml);
} else if (this.provider === 'azure') {
return this.generateAzureTTS(ssml);
}
}
async generateGoogleTTS(ssml) {
const textToSpeech = require('@google-cloud/text-to-speech');
const client = new textToSpeech.TextToSpeechClient();
const [response] = await client.synthesizeSpeech({
input: { ssml },
voice: { languageCode: 'en-US', name: this.voice },
audioConfig: { audioEncoding: 'MP3' }
});
return response.audioContent;
}
}
// Pre-generate common alert audio files
const pregenerateAlertAudio = async () => {
const tts = new EmergencyTTS({ provider: 'google' });
const commonAlerts = [
{ id: 'fire_evacuation', text: 'Fire alarm activated. Evacuate the building immediately using the nearest exit. Do not use elevators.' },
{ id: 'tornado_shelter', text: 'Tornado warning. Move immediately to the nearest interior room on the lowest floor. Stay away from windows.' },
{ id: 'lockdown', text: 'Lockdown in effect. Lock all doors. Turn off lights. Move away from windows. Remain silent and await further instructions.' },
{ id: 'all_clear', text: 'All clear. The emergency has ended. Normal operations may resume. Thank you for your cooperation.' }
];
for (const alert of commonAlerts) {
const audio = await tts.generateAudioAlert(alert.text);
await fs.writeFile(`./assets/audio/${alert.id}.mp3`, audio);
}
};
Frequently Asked Questions
Emergency alert integration transforms digital signage into critical safety infrastructure. For implementation assistance with specific emergency notification platforms or compliance requirements, contact MediaSignage support or consult with your local emergency management office.