Infinity Castle

### Changelog

#### July 2025

- Fix Sub-Account Manager
  - Populating the permission selections
- Add network scanning tool
  - Simple tool to scan for Shinobi and Central servers on network
- Update copySystemToNewServer.js
  - Create db during copy process
- Add server configuration copy script
  - Create script that copies Shinobi configurations to another server

#### June 2025

- Update createMonitorsJsonFromTxt.js
  - Ignore empty rows in processing
- Fix Videos Table UI
  - Fix dropdown menu links
- Live Grid improvements
  - Fix substream reload on scroll
  - Allow adding client-side delay in drawing detection boxes
  - Detection delay made into float (max value 10, step 0.1)
  - Disable debug logs for detection delay
- Add user logging
  - Add log when video is deleted from UI
  - Add logging for command failure to userLog
- ONVIF improvements
  - Refine ONVIF Event handler
  - Add response.ok true to toggleSubstream action stop
  - Fix syntax error in controls/onvif.js
- Add new features
  - Add Copy Tags to Monitor Settings > Copy Settings
  - Add option to use 24-hour time in Videos Table
- Server management
  - Central Management Pair Server wrapped in try script in case of failure to start
- Documentation updates
  - Add argument information about extenders
merge-requests/537/head
Moe 2025-07-16 22:04:24 +06:00
parent 2262b37523
commit ea13a4dc58
30 changed files with 1626 additions and 104 deletions

View File

@ -354,6 +354,24 @@ module.exports = function(s,config,lang){
}
]
},
{
"field": lang['Use 24 Hour Time in Videos Table'],
attribute:'localStorage="use24HourTime"',
"description": "",
"default": "0",
"example": "",
"fieldType": "select",
"possible": [
{
"name": lang.No,
"value": "0"
},
{
"name": lang.Yes,
"value": "1"
}
]
},
{
"field": lang.Themes,
"name": "detail=theme",

View File

@ -2961,6 +2961,72 @@ module.exports = (s,config,lang) => {
},
]
},
{
hidden: true,
"name": lang['Line Counter'],
"color": "orange",
id: "monSectionLineCounter",
isSection: true,
isFormGroupGroup: true,
"section-class": "h_det_input h_det_1",
"info": [
{
"name": "detail=detectorLineCounter",
"field": lang.Enabled,
"description": lang["fieldTextDetectorLineCounter"],
"default": "0",
"fieldType": "select",
"selector": "h_det_line",
"possible": yesNoPossibility
},
{
"id": "monitorSettings-lineCounter-canvas-container",
"form-group-class": "h_det_line_input h_det_line_1",
"fieldType": "div",
"style": "overflow-x: auto; width: 100%; text-align: center",
},
{
"id": "detector-line-counter-spacing",
"form-group-class": "h_det_line_input h_det_line_1",
"field": lang['Line Spacing'],
"fieldType": "number",
"numberMin": "10",
"placeholder": "40"
},
{
"name": "detail=detectorLineCounterTags",
"form-group-class": "h_det_line_input h_det_line_1",
"field": lang['Objects to Count'],
"placeholder": "person"
},
{
"id": "monitorSettings-lineCounter-canvas-container",
"class": "row",
"fieldType": "div",
"info": [
{
"id": "detector-line-counter-name-down",
"form-group-class": "h_det_line_input h_det_line_1 col-md-6",
"field": lang['Down Label'],
"placeholder": "Down"
},
{
"id": "detector-line-counter-name-up",
"form-group-class": "h_det_line_input h_det_line_1 col-md-6",
"field": lang['Up Label'],
"placeholder": "Up"
}
],
},
{
"id": "detector-line-counter-reset-daily",
"form-group-class": "h_det_line_input h_det_line_1",
"field": lang['Reset Daily'],
"fieldType": "select",
"possible": yesNoPossibility
}
]
},
{
hidden: true,
"name": lang['Event-Based Recording'],
@ -3746,6 +3812,17 @@ module.exports = (s,config,lang) => {
"form-group-class-pre-layer": "col-md-6",
"possible": yesNoPossibility
},
{
"field": lang['Copy Tags'],
"description": "",
"default": "0",
"example": "",
"fieldType": "select",
"attribute": `copy="field=tags"`,
"form-group-class": "h_copy_settings_input h_copy_settings_1",
"form-group-class-pre-layer": "col-md-6",
"possible": yesNoPossibility
},
{
"field": lang['Compress Completed Videos'],
"default": "0",

View File

@ -189,6 +189,8 @@
"Connection": "Connection",
"Video Set": "Video Set",
"Video Accessed": "Video Accessed",
"Video Deleted": "Video Deleted",
"Cloud Video Deleted": "Cloud Video Deleted",
"notEnoughFramesText1": "Not Enough Frames for compilation.",
"Allow API Trigger": "Allow API Trigger",
"When Detector is Off": "When Detector is Off",
@ -266,6 +268,7 @@
"Trigger Camera Groups": "Trigger Camera Groups",
"Motion Detection": "Motion Detection",
"Object Detection": "Object Detection",
"Detection Draw Delay": "Detection Draw Delay",
"Minimum Movement": "Minimum Movement",
"inPercent": "In Percent",
"Hide Detection on Stream": "Hide Detection on Stream",
@ -598,6 +601,7 @@
"Set New Videos Directory": "Set New Videos Directory?",
"CSS": "CSS <small>Style your dashboard.</small>",
"Don't Stretch Monitors": "Don't Stretch Monitors",
"Use 24 Hour Time in Videos Table": "Use 24 Hour Time in Videos Table",
"Force Monitors Per Row": "Force Monitors Per Row",
"Monitors per row": "Monitors per row <small>for Montage</small>",
"Browser Console Log": "Browser Console Log",
@ -775,6 +779,7 @@
"Custom": "Custom",
"Detector": "Detector",
"Detectors Selected": "Detectors Selected",
"Line Counter": "Line Counter",
"Audio Detector": "Audio Detector",
"Audio Detection": "Audio Detection",
"Minimum dB": "Minimum dB",
@ -960,6 +965,7 @@
"Output Method": "Output Method",
"Webhook": "Webhook",
"Event Webhook Error": "Event Webhook Error",
"Event Command Error": "Event Command Error",
"Webhook URL": "Webhook URL",
"Command on Trigger": "Command on Trigger",
"Frames": "Frames",
@ -1423,13 +1429,18 @@
"Batch": "Batch",
"Subdivision": "Subdivision",
"Map": "Map",
"Reset Daily": "Reset Daily",
"Up Label": "Up Label",
"Down Label": "Down Label",
"Delay for Snapshot": "Delay for Snapshot",
"Add Map": "Add Map",
"Add Input Feed": "Add Input Feed",
"Add Channel": "Add Channel",
"Automatic": "Automatic",
"Line Spacing": "Line Spacing",
"Max Latency": "Max Latency",
"Loop Stream": "Loop Stream",
"Objects to Count": "Objects to Count",
"Object Count": "Object Count",
"Object Tag": "Object Tag",
"Search Object Tags": "Search Object Tags",
@ -1445,6 +1456,7 @@
"Show Regions of Interest": "Show Regions of Interest",
"Confidence of Detection": "Confidence of Detection",
"Edit Selected": "Edit Selected",
"Copy Tags": "Copy Tags",
"Copy Stream Channels": "Copy Stream Channels",
"Copy to Selected Monitor(s)": "Copy to Selected Monitor(s)",
"Copy Settings": "Copy Settings",
@ -1870,6 +1882,8 @@
"fieldTextDetectorNotriggerCommand": "The command that will run. This is the equivalent of running a shell command from terminal.",
"fieldTextDetectorNotriggerCommandTimeout": "This value is a timer to allow the next running of your script. This value is in minutes.",
"fieldTextDetectorAudio": "Check if Audio has occured at a certiain decible. Decible reading may not be accurate to real-world measurement.",
"fieldTextDetectorLineCounter": "Add a Line Crossing Counter. When an object passes over a line a count is kept. Only works with Object Detection or plugins of similar nature.",
"fieldTextDetectorLineCounterTags": "Set the object tags that will be counted.",
"fieldTextDetectorUseDetectObject": "Create frames for sending to any connected Plugin.",
"fieldTextDetectorSendFramesObject": "Push frames to the connected plugin to be analyzed.",
"fieldTextDetectorObjCountInRegion": "Count Objects only inside Regions.",

View File

@ -4,45 +4,49 @@ const app = express();
var cors = require('cors');
var bodyParser = require('body-parser');
module.exports = (s,config,lang) => {
const { modifyConfiguration, getConfiguration } = require('../../system/utils.js')(config)
const pairPort = config.pairPort || 8091
const bindIp = config.bindip
const server = http.createServer(app);
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(cors());
try{
const { modifyConfiguration, getConfiguration } = require('../../system/utils.js')(config)
const pairPort = config.pairPort || 8091
const bindIp = config.bindip
const server = http.createServer(app);
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: true}));
app.use(cors());
server.listen(pairPort, bindIp, function(){
console.log('Management Pair Server Listening on '+pairPort);
});
server.listen(pairPort, bindIp, function(){
console.log('Management Pair Server Listening on '+pairPort);
});
const {
addManagementServer,
removeManagementServer,
connectToManagementServer,
connectAllManagementServers,
} = require('../utils.js')(s,config,lang)
const {
addManagementServer,
removeManagementServer,
connectToManagementServer,
connectAllManagementServers,
} = require('../utils.js')(s,config,lang)
/**
* API : Superuser : Save Management Server Settings
*/
app.post('/mgmt/connect', async function (req,res){
// ws://127.0.0.1:8663
let response = {ok: true};
const managementServer = req.body.managementServer;
if(!config.mgmtServers[managementServer]){
const peerConnectKey = req.body.peerConnectKey;
if(peerConnectKey){
response = await addManagementServer(managementServer, peerConnectKey)
await connectToManagementServer(managementServer, peerConnectKey)
/**
* API : Superuser : Save Management Server Settings
*/
app.post('/mgmt/connect', async function (req,res){
// ws://127.0.0.1:8663
let response = {ok: true};
const managementServer = req.body.managementServer;
if(!config.mgmtServers[managementServer]){
const peerConnectKey = req.body.peerConnectKey;
if(peerConnectKey){
response = await addManagementServer(managementServer, peerConnectKey)
await connectToManagementServer(managementServer, peerConnectKey)
}else{
response.ok = false;
response.msg = 'No P2P API Key Provided';
}
}else{
response.ok = false;
response.msg = 'No P2P API Key Provided';
response.msg = 'Already Configured';
}
}else{
response.ok = false;
response.msg = 'Already Configured';
}
s.closeJsonResponse(res,response)
})
s.closeJsonResponse(res,response)
})
}catch(err){
console.error(err)
}
}

View File

@ -124,7 +124,7 @@ module.exports = function(s,config,lang,app,io){
if(onvifOptions.mid && onvifOptions.ke){
const groupKey = onvifOptions.ke
const monitorId = onvifOptions.mid
const theDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection || (await s.createOnvifDevice({ id: e.mid, ke: e.ke })).device;
const theDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection || (await s.createOnvifDevice({ id: monitorId, ke: groupKey })).device;
if(theDevice){
theUrl = addCredentialsToUrl({
username: theDevice.user,

View File

@ -530,22 +530,26 @@ function beginProcessing(){
//events - alarms
const deleteOldAlarms = function(v){
return new Promise((resolve,reject) => {
const daysOldForDeletion = v.d.event_days && !isNaN(v.d.event_days) ? parseFloat(v.d.event_days) : 10
if(config.cron.deleteEvents === true && daysOldForDeletion !== 0){
knexQuery({
action: "delete",
table: "Alarms",
where: [
['ke','=',v.ke],
['time','<', sqlDate(daysOldForDeletion + ' DAY')],
]
},(err,rrr) => {
if(config.alarmManagement){
const daysOldForDeletion = v.d.event_days && !isNaN(v.d.event_days) ? parseFloat(v.d.event_days) : 10
if(config.cron.deleteEvents === true && daysOldForDeletion !== 0){
knexQuery({
action: "delete",
table: "Alarms",
where: [
['ke','=',v.ke],
['time','<', sqlDate(daysOldForDeletion + ' DAY')],
]
},(err,rrr) => {
resolve()
if(err)return errorLog(err);
if(rrr && rrr > 0 || config.debugLog === true){
postMessage({f:'deleteEvents',msg:rrr + ' SQL rows older than ' + daysOldForDeletion + ' days deleted',ke:v.ke,time:'moment()'})
}
})
}else{
resolve()
if(err)return errorLog(err);
if(rrr && rrr > 0 || config.debugLog === true){
postMessage({f:'deleteEvents',msg:rrr + ' SQL rows older than ' + daysOldForDeletion + ' days deleted',ke:v.ke,time:'moment()'})
}
})
}
}else{
resolve()
}

View File

@ -0,0 +1,9 @@
module.exports = async function(s,config){
s.debugLog('Updating database to 2025-04-13')
const {
addColumn,
} = require('../utils.js')(s,config)
await addColumn('Events Counts',[
{name: 'name', length: 255, type: 'string'},
])
}

View File

@ -111,6 +111,7 @@ module.exports = function(s,config){
{name: 'mid', length: 100, type: 'string'},
{name: 'tag', length: 30, type: 'string'},
{name: 'details', type: 'text'},
{name: 'name', length: 255, type: 'string'},
{name: 'count', type: 'integer', length: 10, defaultTo: 1},
{name: 'time', type: 'timestamp', defaultTo: currentTimestamp()},
{name: 'end', type: 'timestamp', defaultTo: currentTimestamp()},
@ -214,6 +215,7 @@ module.exports = function(s,config){
await require('./migrate/2022-12-18.js')(s,config)
await require('./migrate/2023-03-11.js')(s,config)
await require('./migrate/2025-03-05.js')(s,config)
await require('./migrate/2025-04-13.js')(s,config)
delete(s.preQueries)
}
}

View File

@ -1,6 +1,7 @@
module.exports = function(s,config,lang){
require('./events/onvif.js')(s,config,lang)
require('./events/noEventsDetector.js')(s,config,lang)
require('./events/lineCrossCounter.js')(s,config,lang);
const { bindTagLegendForMonitors } = require('./events/utils.js')(s,config,lang)
s.onAccountSave(function(theGroup,formDetails,user){
const groupKey = user.ke

View File

@ -0,0 +1,268 @@
class LineCrossCounter {
constructor(imageWidth, imageHeight, lines = [], tags = ['person']) {
this.imageWidth = imageWidth;
this.imageHeight = imageHeight;
// Line configuration (default: two horizontal lines)
this.lines = lines.length >= 2 ? lines : [
{ start: { x: 0, y: 194 }, end: { x: imageWidth, y: 194 } },
{ start: { x: 0, y: 220 }, end: { x: imageWidth, y: 220 } }
];
this.tags = Array.isArray(tags) ? tags : [tags];
this.offset = 15;
// Tracking state
this.resetCounters();
this.frameCount = 0;
this.startTime = new Date();
this.lastFrameTime = null;
this.autoResetEnabled = false;
this.resetHour = 0; // Midnight (12:00 AM)
this.resetMinute = 0;
this.lastResetCheck = null;
this.calculateLineEquations();
this.trackingHistory = new Map();
this.historyDuration = 10000;
}
enableDailyReset(hour = 0, minute = 0) {
this.autoResetEnabled = true;
this.resetHour = hour;
this.resetMinute = minute;
this.lastResetCheck = new Date();
return `Daily reset enabled at ${hour}:${minute.toString().padStart(2, '0')}`;
}
disableDailyReset() {
this.autoResetEnabled = false;
return "Daily reset disabled";
}
checkForDailyReset() {
if (!this.autoResetEnabled) return false;
const now = new Date();
this.lastResetCheck = now;
// Check if we've crossed the reset time
if (now.getHours() === this.resetHour &&
now.getMinutes() === this.resetMinute) {
// Don't reset multiple times in the same minute
const lastReset = this.lastDailyReset || new Date(0);
if (now.getTime() - lastReset.getTime() > 60000) {
this.resetCounters();
this.lastDailyReset = now;
return true;
}
}
return false;
}
resetCounters() {
this.counts = {
total: { down: 0, up: 0 },
byTag: this.createEmptyTagCounts()
};
this.currentNearLine1 = new Set(); // Format: `${id}:${tag}`
this.currentNearLine2 = new Set();
}
createEmptyTagCounts() {
return this.tags.reduce((acc, tag) => {
acc[tag] = { down: 0, up: 0 };
return acc;
}, {});
}
calculateLineEquations() {
this.lineEqs = this.lines.map(line => {
const { start, end } = line;
const A = end.y - start.y;
const B = start.x - end.x;
const C = (end.x * start.y) - (start.x * end.y);
return {
A, B, C,
minX: Math.min(start.x, end.x),
maxX: Math.max(start.x, end.x),
minY: Math.min(start.y, end.y),
maxY: Math.max(start.y, end.y)
};
});
}
isPointNearLine(x, y, lineIndex) {
const line = this.lineEqs[lineIndex];
if (x < line.minX - this.offset || x > line.maxX + this.offset ||
y < line.minY - this.offset || y > line.maxY + this.offset) {
return false;
}
const distance = Math.abs(line.A * x + line.B * y + line.C) /
Math.sqrt(line.A * line.A + line.B * line.B);
console.log('distance',distance, 'line : ' + lineIndex,', pass :',distance <= this.offset)
return distance <= this.offset;
}
processDetections(detections) {
this.checkForDailyReset();
this.lastFrameTime = new Date();
this.frameCount++;
// Clean up old history entries
const now = Date.now();
for (const [id, entry] of this.trackingHistory.entries()) {
if (now - entry.lastSeen > this.historyDuration) {
this.trackingHistory.delete(id);
}
}
if (this.frameCount % 3 !== 0) {
return {
frameResult: this.getCounts(),
changedCount: {
total: { down: 0, up: 0 },
byTag: {}
}
};
}
const filtered = detections.filter(d => d.tag && this.tags.includes(d.tag));
const newNearLine1 = new Set();
const newNearLine2 = new Set();
const changedCount = {
total: { down: 0, up: 0 },
byTag: this.createEmptyTagCounts()
};
// Process each detection
filtered.forEach(detection => {
const { x, y, width, height, id, tag } = detection;
const cx = Math.floor(x + width / 2);
const cy = Math.floor(y + height / 2);
const idTag = `${id}:${tag}`;
// Update tracking history
if (!this.trackingHistory.has(id)) {
this.trackingHistory.set(id, {
lastSeen: now,
positions: [],
tag: tag
});
}
const history = this.trackingHistory.get(id);
history.lastSeen = now;
history.positions.push({ x: cx, y: cy, timestamp: now });
// Keep only recent positions
history.positions = history.positions.filter(
pos => now - pos.timestamp <= this.historyDuration
);
// Check line proximity
const nearLine1 = this.isPointNearLine(cx, cy, 0);
const nearLine2 = this.isPointNearLine(cx, cy, 1);
if (nearLine1) newNearLine1.add(idTag);
if (nearLine2) newNearLine2.add(idTag);
// Check historical positions for crossings
if (history.positions.length > 1) {
const crossedLine1To2 = this.checkHistoricalCrossing(history.positions, 0, 1);
const crossedLine2To1 = this.checkHistoricalCrossing(history.positions, 1, 0);
if (crossedLine1To2 && !this.countedCrossings.has(`${id}:down`)) {
this.counts.total.down++;
this.counts.byTag[tag].down++;
changedCount.total.down++;
changedCount.byTag[tag].down++;
this.countedCrossings.add(`${id}:down`);
}
if (crossedLine2To1 && !this.countedCrossings.has(`${id}:up`)) {
this.counts.total.up++;
this.counts.byTag[tag].up++;
changedCount.total.up++;
changedCount.byTag[tag].up++;
this.countedCrossings.add(`${id}:up`);
}
}
});
this.currentNearLine1 = newNearLine1;
this.currentNearLine2 = newNearLine2;
const filteredChangedTags = Object.fromEntries(
Object.entries(changedCount.byTag).filter(
([_, counts]) => counts.down !== 0 || counts.up !== 0
)
);
return {
frameResult: this.getCounts(),
changedCount: {
total: changedCount.total,
byTag: filteredChangedTags
}
};
}
checkHistoricalCrossing(positions, fromLineIndex, toLineIndex) {
// Check if object crossed from one line to another in its position history
let wasNearFromLine = false;
for (const pos of positions) {
const nearFromLine = this.isPointNearLine(pos.x, pos.y, fromLineIndex);
const nearToLine = this.isPointNearLine(pos.x, pos.y, toLineIndex);
if (nearFromLine) wasNearFromLine = true;
if (wasNearFromLine && nearToLine) return true;
}
return false;
}
getCounts() {
return {
total: this.counts.total,
byTag: this.counts.byTag,
lines: this.lines,
frameCount: this.frameCount,
activeTags: this.tags,
timestamps: {
start: this.startTime.toISOString(),
lastFrame: this.lastFrameTime?.toISOString() || null
}
};
}
updateLines(newLines) {
if (newLines.length >= 2) {
this.lines = newLines;
this.calculateLineEquations();
return true;
}
return false;
}
updateTags(newTags) {
this.tags = Array.isArray(newTags) ? newTags : [newTags];
// Initialize counts for new tags
this.tags.forEach(tag => {
if (!this.counts.byTag[tag]) {
this.counts.byTag[tag] = { down: 0, up: 0 };
}
});
// Remove old tags
Object.keys(this.counts.byTag).forEach(tag => {
if (!this.tags.includes(tag)) {
delete this.counts.byTag[tag];
}
});
}
}
module.exports = LineCrossCounter;

View File

@ -0,0 +1,45 @@
class Tracker {
constructor() {
this.center_points = {};
this.id_count = 0;
}
update(objects_rect) {
const objects_bbs_ids = [];
for (const rect of objects_rect) {
const [x, y, w, h] = rect;
const cx = Math.floor((x + x + w) / 2);
const cy = Math.floor((y + y + h) / 2);
let same_object_detected = false;
for (const [id, pt] of Object.entries(this.center_points)) {
const dist = Math.hypot(cx - pt[0], cy - pt[1]);
if (dist < 35) {
this.center_points[id] = [cx, cy];
objects_bbs_ids.push([x, y, w, h, parseInt(id)]);
same_object_detected = true;
break;
}
}
if (!same_object_detected) {
this.center_points[this.id_count] = [cx, cy];
objects_bbs_ids.push([x, y, w, h, this.id_count]);
this.id_count += 1;
}
}
const new_center_points = {};
for (const obj_bb_id of objects_bbs_ids) {
const object_id = obj_bb_id[4];
new_center_points[object_id] = this.center_points[object_id];
}
this.center_points = new_center_points;
return objects_bbs_ids;
}
}
module.exports = Tracker;

View File

@ -0,0 +1,107 @@
const LineCrossCounter = require('./libs/lineCrossCounter.js');
module.exports = (s,config,lang) => {
function setupLineCounter(monitorId, groupKey){
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId]
const monitorDetails = monitorConfig.details;
const lineCounterEnabled = monitorDetails.detectorLineCounter === '1';
if(lineCounterEnabled){
if(!activeMonitor.lineCounter){
const lineCounterSettings = monitorDetails.detectorLineCounterSettings;
const downName = lineCounterSettings.downName || 'Down';
const upName = lineCounterSettings.upName || 'Up';
const resetDaily = lineCounterSettings.resetDaily;
const lineCounterTags = monitorDetails.detectorLineCounterTags.split(',');
const imageWidth = parseInt(monitorDetails.detector_scale_x_object) || 1280
const imageHeight = parseInt(monitorDetails.detector_scale_y_object) || 720
if(lineCounterTags.length === 0)lineCounterTags.push('person');
// console.log('lineCounterSettings.lines',lineCounterSettings.lines)
const counter = new LineCrossCounter(imageWidth, imageHeight, lineCounterSettings.lines, lineCounterTags);
counter.name = {
down: downName,
up: upName,
};
if(resetDaily){
counter.enableDailyReset()
}
activeMonitor.lineCounter = counter;
}
}else{
delete(activeMonitor.lineCounter)
}
}
function destroyLineCounter(monitorId, groupKey){
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
delete(activeMonitor.lineCounter)
}
function resetLineCounter(monitorId, groupKey){
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
activeMonitor.lineCounter.resetCounters()
}
async function saveEventCount(monitorId, groupKey, changedCount, frameResult, eventTime){
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
const lineCounter = activeMonitor.lineCounter;
const byTag = changedCount.byTag;
for(tag in byTag){
const tagCounts = byTag[tag]
for(direction in tagCounts){
const newCount = tagCounts[direction];
if(newCount > 0){
// console.log(JSON.stringify(frameResult,null,3))
// console.log(JSON.stringify(byTag,null,3))
const theCount = frameResult.byTag[tag][direction];
// console.log({
// action: "insert",
// table: "Events Counts",
// insert: {
// ke: groupKey,
// mid: monitorId,
// tag: tag,
// name: lineCounter.name[direction],
// count: theCount,
// time: eventTime,
// end: eventTime,
// details: '{}'
// }
// })
const insertResponse = await s.knexQueryPromise({
action: "insert",
table: "Events Counts",
insert: {
ke: groupKey,
mid: monitorId,
tag: tag,
name: lineCounter.name[direction],
count: theCount,
time: eventTime,
end: eventTime,
details: '{}'
}
})
}
}
}
}
async function processDetectionWithLineCounter(monitorId, groupKey, matrices = [], eventTime){
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
if(activeMonitor.lineCounter && matrices.length > 0){
const { frameResult, changedCount } = activeMonitor.lineCounter.processDetections(matrices);
await saveEventCount(monitorId, groupKey, changedCount, frameResult, eventTime)
// console.log(frameResult)
}
}
s.onMonitorStart(function(monitorConfig){
setupLineCounter(monitorConfig.mid, monitorConfig.ke)
})
s.onMonitorSave(function(monitorConfig){
destroyLineCounter(monitorConfig.mid, monitorConfig.ke)
})
s.onEventTrigger(function(d,filter,eventTime){
const monitorId = d.mid || d.id;
const groupKey = d.ke;
const eventDetails = d.details;
if(eventDetails.reason !== 'motion'){
processDetectionWithLineCounter(monitorId, groupKey, eventDetails.matrices, eventTime)
}
})
}

View File

@ -3,30 +3,59 @@ function hasOnvifEventsEnabled(monitorConfig) {
}
module.exports = function (s, config, lang) {
const {Cam} = require("onvif");
const { Cam } = require("onvif");
const {
triggerEvent,
} = require('./utils.js')(s, config, lang)
function handleEvent(event, monitorConfig, onvifEventLog) {
const eventValue = event.message?.message?.data?.simpleItem?.$?.Value;
if (eventValue === false) {
onvifEventLog(`ONVIF Event Stopped`, `topic ${event.topic?._}`)
return
}
onvifEventLog(`ONVIF Event Detected!`, `topic ${event.topic?._}`)
triggerEvent({
f: 'trigger',
id: monitorConfig.mid,
ke: monitorConfig.ke,
details: {
plug: 'onvifEvent',
name: 'onvifEvent',
reason: event.topic?._,
confidence: 100,
[event.message?.message?.data?.simpleItem?.$?.Name]: eventValue
function handleEvent(event, monitorConfig, onvifEventLog, callback = () => {}) {
try{
const monitorId = monitorConfig.mid;
const groupKey = monitorConfig.ke;
s.runExtensionsForArray('onOnvifEventTrigger', null, [event, groupKey, monitorId]);
const message = event.message && event.message.message ? event.message.message : event.message;
if(!message){
console.error(`Missing Message in ONVIF Event from ${monitorConfig.name}`)
console.error(err)
console.error(JSON.stringify(event,null,3))
return
}
})
if(!message.source){
// ignore event with no source
return;
}
const topicInternalName = event.topic?._;
const sourceSimpleItem = message.source?.simpleItem;
const sourceSelected = (sourceSimpleItem instanceof Array ? sourceSimpleItem[sourceSimpleItem.length - 1] : sourceSimpleItem).$;
const data = message.data?.simpleItem?.$;
const topicShortName = sourceSelected.Value;
const eventValue = data.Value;
if(topicShortName === "Processor_Usage"){
// ignore camera CPU stats
return
}else if (eventValue === false) {
onvifEventLog(`ONVIF Event Stopped`, `topic ${topicInternalName}`)
return
}
onvifEventLog(`ONVIF Event Detected!`, `topic ${topicInternalName}`)
callback({
f: 'trigger',
id: monitorId,
ke: groupKey,
details: {
plug: 'onvifEvent',
name: 'onvifEvent',
reason: topicInternalName,
confidence: 100,
token: topicShortName,
[data.Name]: eventValue
}
})
}catch(err){
console.error(`Failure to parse ONVIF Event from ${monitorConfig.name}`)
console.error(err)
console.error(JSON.stringify(event,null,3))
}
}
function configureOnvif(monitorConfig, onvifEventLog) {
@ -46,8 +75,13 @@ module.exports = function (s, config, lang) {
onvifEventLog(`ONVIF Event Error`,error)
return
}
this.on('event', function (event) {
this.on('event', config.noEventTriggerForOnvifEvent ? function (event) {
handleEvent(event, monitorConfig, onvifEventLog);
} : function (event) {
handleEvent(event, monitorConfig, onvifEventLog, triggerEvent);
})
this.on('eventsError', function (e) {
onvifEventLog(`ONVIF Event Error`,e)
})
this.on('eventsError', function (e) {
onvifEventLog(`ONVIF Event Error`,e)

View File

@ -480,7 +480,10 @@ module.exports = (s,config,lang) => {
var detector_command = addEventDetailsToString(d,monitorDetails.detector_command)
if(detector_command === '')return
exec(detector_command,{detached: true},function(err){
if(err)s.debugLog(err)
if(err){
s.userLog(d, {type:lang["Event Command Error"],msg:{error:err,cmd:detector_command}})
s.debugLog(d.ke, monitorId, detector_command, err)
}
})
}
@ -883,23 +886,31 @@ module.exports = (s,config,lang) => {
if(!passedMotionLock)return
}
const thisHasMatrices = hasMatrices(eventDetails)
if(thisHasMatrices && monitorDetails.detector_obj_region === '1'){
var regions = s.group[monitorConfig.ke].activeMonitors[monitorConfig.mid].parsedObjects.cordsForObjectDetection
var matricesInRegions = isAtleastOneMatrixInRegion(regions,eventDetails.matrices)
eventDetails.matrices = matricesInRegions
if(matricesInRegions.length === 0)return;
if(filter.countObjects && monitorDetails.detector_obj_count === '1' && monitorDetails.detector_obj_count_in_region === '1' && !didCountingAlready){
countObjects(eventDetails.matrices)
if(thisHasMatrices){
if(monitorDetails.detector_obj_region === '1'){
var regions = s.group[monitorConfig.ke].activeMonitors[monitorConfig.mid].parsedObjects.cordsForObjectDetection
var matricesInRegions = isAtleastOneMatrixInRegion(regions,eventDetails.matrices)
eventDetails.matrices = matricesInRegions
if(matricesInRegions.length === 0)return;
if(filter.countObjects && monitorDetails.detector_obj_count === '1' && monitorDetails.detector_obj_count_in_region === '1' && !didCountingAlready){
countObjects(eventDetails.matrices)
}
}
if(monitorDetails.detector_object_ignore_not_move === '1'){
const trackerId = `${groupKey}${monitorId}`
trackObjectWithTimeout(trackerId,eventDetails.matrices)
const trackedObjects = getTracked(trackerId)
const objectsThatMoved = getAllMatricesThatMoved(monitorConfig,trackedObjects)
setLastTracked(trackerId, trackedObjects)
if(objectsThatMoved.length === 0)return;
eventDetails.matrices = objectsThatMoved
}else if(activeMonitor.lineCounter){
const trackerId = `${groupKey}${monitorId}`
trackObjectWithTimeout(trackerId,eventDetails.matrices)
const trackedObjects = getTracked(trackerId)
setLastTracked(trackerId, trackedObjects)
eventDetails.matrices = trackedObjects
}
}
if(thisHasMatrices && monitorDetails.detector_object_ignore_not_move === '1'){
const trackerId = `${groupKey}${monitorId}`
trackObjectWithTimeout(trackerId,eventDetails.matrices)
const trackedObjects = getTracked(trackerId)
const objectsThatMoved = getAllMatricesThatMoved(monitorConfig,trackedObjects)
setLastTracked(trackerId, trackedObjects)
if(objectsThatMoved.length === 0)return;
eventDetails.matrices = objectsThatMoved
}
//
d.doObjectDetection = (

View File

@ -43,65 +43,191 @@ module.exports = function(s,config){
await extender(...args)
}
}
////// INFO //////
// Arguments for callback noted below each Extension.
// Example use of arguments : s.onSocketAuthentication((userDatabaseRow,socketIoConnection,initiateData,sendDataToClient) => { console.log(userDatabaseRow) })
////// USER //////
createExtension(`onSocketAuthentication`)
// [0] userDatabaseRow : Object of User database row
// [1] socketIoConnection : Socket.IO Connection Handler
// [2] initiateData : Data that was used to initiate the socket authentication
// [3] sendDataToClient : function to send data to authenticated connection
createExtension(`onUserLog`)
// [0] logEvent : the databse row being inserted
createExtension(`loadGroupExtender`,`loadGroupExtensions`)
// [0] userDatabaseRow : Object of User database row. This will always be an Admin user.
createExtension(`loadGroupAppExtender`,`loadGroupAppExtensions`)
// [0] userDatabaseRow : Object of User database row. This will always be an Admin user.
createExtension(`unloadGroupAppExtender`,`unloadGroupAppExtensions`)
// [0] userDatabaseRow : Object of User database row. This will always be an Admin user.
createExtension(`onAccountSave`)
// [0] groupLoadedInMemory : Object of group initiated in memory.
// [1] userDetails : Additional data about the user. If Admin user it will have group data.
// [2] userDatabaseRow : Object of User database row.
createExtension(`beforeAccountSave`)
// [0] infoObject : form (complete post of user data to be update), formDetails (form posted details), userDetails (details in database before save)
createExtension(`onTwoFactorAuthCodeNotification`)
// [0] userDatabaseRow : Object of User database row
createExtension(`onStalePurgeLock`)
createExtension(`onVideoAccess`)
// [0] groupKey : The ID of the group.
// [1] usedSpace : Currently used space (mb).
// [2] sizeLimit : Maximum Usable Space (mb).
createExtension(`onLogout`)
// [0] userDatabaseRow : Object of User database row.
// [1] groupKey : The ID of the group.
// [2] userId : The ID of the user.
// [3] clientIp : IP Address of the user logging out.
////// EVENTS //////
createExtension(`onEventTrigger`)
// [0] eventData : The object that triggered the Event.
// [1] filter : The current state of the filters being used for this event.
// [2] eventTime : Current Date and Time of the event.
createExtension(`onEventTriggerBeforeFilter`)
// [0] eventData : The object that triggered the Event.
// [1] filter : The state of the filters being modified for this event.
createExtension(`onFilterEvent`)
// unused
createExtension(`onOnvifEventTrigger`)
// [0] onvifEventData : The ONVIF Event object that triggered.
// [1] groupKey : The ID of the group.
// [2] monitorId : The ID of the Monitor.
////// MONITOR //////
createExtension(`onMonitorInit`)
// [0] initiateData : Data that was used to initiate the Monitor into memory. Is usually database row of Monitor.
createExtension(`onMonitorStart`)
// [0] monitorConfig : Copy of Monitor Configuration loaded into memory.
// [1] initiateData : Data that was used to initiate the action.
createExtension(`onMonitorStop`)
// [0] monitorConfig : Copy of Monitor Configuration loaded into memory.
// [1] initiateData : Data that was used to initiate the action.
createExtension(`onMonitorSave`)
// [0] monitorConfig : Copy of Monitor Configuration loaded into memory.
// [1] formData : Data that was used to update the monitor.
// [2] endData : Response for the update.
createExtension(`onMonitorUnexpectedExit`)
// [0] monitorConfig : Copy of Monitor Configuration loaded into memory.
// [1] initiateData : Data that was used to initiate the monitor.
createExtension(`onDetectorNoTriggerTimeout`)
// [0] initiateData : Data that was used to initiate the monitor.
createExtension(`onFfmpegCameraStringCreation`)
// [0] initiateData : Data that was used to initiate the monitor.
// [1] ffmpegCommand : The final FFmpeg command used for the main process of the monitor.
createExtension(`onFfmpegBuildMainStream`)
// [0] streamType : The Stream Type that was chosen for the main stream.
// [1] streamFlags : The complete set of flags used for this output.
// [2] initiateData : Data that was used to initiate the monitor.
createExtension(`onFfmpegBuildStreamChannel`)
// [0] streamType : The Stream Type that was chosen for this output.
// [1] streamFlags : The complete set of flags used for this output.
// [2] number : The number of this output.
// [3] initiateData : Data that was used to initiate the monitor.
createExtension(`onMonitorPingFailed`)
// [0] monitorConfig : Copy of Monitor Configuration loaded into memory.
// [1] initiateData : Data that was used to initiate the monitor.
createExtension(`onMonitorDied`)
// [0] monitorConfig : Copy of Monitor Configuration loaded into memory.
// [1] initiateData : Data that was used to initiate the monitor.
createExtension(`onMonitorCreateStreamPipe`)
// This extension should be used wisely. It can be used to add new stream types.
// [0] streamType : The Stream Type that was chosen for this output.
// [1] initiateData : Data that was used to initiate the monitor.
// [2] resetStreamCheck : Function to reset the timer that indicates if the output is stale.
///////// SYSTEM ////////
createExtension(`onProcessReady`)
// [0] ready : This is always true.
createExtension(`onProcessExit`)
// no arguments
createExtension(`onLoadedUsersAtStartup`)
// no arguments
createExtension(`onBeforeDatabaseLoad`)
// [0] config : The Configuration object used to initiate the Shinobi core process.
createExtension(`onFFmpegLoaded`)
// no arguments
createExtension(`beforeMonitorsLoadedOnStartup`)
// no arguments
createExtension(`onWebSocketConnection`)
// [0] socketIoConnection : Socket.IO Connection Handler
// [1] validatedAndBindAuthenticationToSocketConnection : N/A
// [2] createStreamEmitter : For creating a handler on a stream output
createExtension(`onWebSocketDisconnection`)
// [0] socketIoConnection : Socket.IO Connection Handler
createExtension(`onWebsocketMessageSend`)
// [0] socketData : The Data being sent over Socket.IO
// [1] socketId : The identifier of where the Socket.IO data is sent to.
// [2] originSocket : This is not always set. The socketId that is sending the data to another socketId.
createExtension(`onOtherWebSocketMessages`)
// [0] socketData : The Data being sent over Socket.IO to the server from the client.
// [1] socketIoConnection : Socket.IO Connection Handler of the sender (client)
// [2] sendDataToClient : function to send data back to the socketIoConnection.
createExtension(`onGetCpuUsage`)
// [0] cpuUsagePercent : the percent of CPU Usage.
createExtension(`onGetRamUsage`)
// [0] ramUsagePercent : the percent of RAM Usage.
createExtension(`onSubscriptionCheck`)
// unused
createExtension(`onDataPortMessage`)
// [0] dataPortObject : Data sent back to server on Data Port channel.
createExtension(`onHttpRequestUpgrade`,null,true)
// [0] request : Request to http.createServer(app) on Upgrade.
// [1] socket : Socket of http.createServer(app) on Upgrade.
// [2] head : Header sent to http.createServer(app) on Upgrade.
createExtension(`onPluginConnected`)
// [0] request : Request to http.createServer(app) on Upgrade.
createExtension(`onPluginDisconnected`)
// [0] pluginName : The internal name of the plugin.
// [1] newDetector : Detector information loaded into memory.
/////// CRON ////////
createExtension(`onCronGroupProcessed`)
// [0] userDatabaseRow : Object of User database row. This will always be an Admin user.
createExtension(`onCronGroupProcessedAwaited`)
// [0] userDatabaseRow : Object of User database row. This will always be an Admin user.
createExtension(`onCronGroupBeforeProcessed`)
// [0] userDatabaseRow : Object of User database row. This will always be an Admin user.
createExtension(`onCronGroupBeforeProcessedAwaited`)
// [0] userDatabaseRow : Object of User database row. This will always be an Admin user.
/////// VIDEOS ////////
createExtension(`insertCompletedVideoExtender`,`insertCompletedVideoExtensions`)
// [0] monitorObject : Active Monitor loaded into memory.
// [1] rawInfoAboutVideo : Currently processing information about video.
// [2] insertQuery : The insert query used to save the video into the database.
createExtension(`onEventBasedRecordingComplete`)
// [0] endData : Response for the process.
// [1] monitorConfig : Copy of Monitor Configuration loaded into memory.
createExtension(`onEventBasedRecordingStart`)
// [0] monitorConfig : Copy of Monitor Configuration loaded into memory.
// [1] filename : Filename of video saved.
createExtension(`onBeforeInsertCompletedVideo`)
// [0] monitorObject : Active Monitor loaded into memory.
// [1] rawInfoAboutVideo : Currently processing information about video.
createExtension(`onVideoAccess`)
// [0] videoDatabaseRow : Object of Video database row
// [1] userDatabaseRow : Object of User database row. The one accessing the data.
// [2] groupKey : The ID of the group.
// [3] monitorId : The ID of the Monitor.
// [4] clientIp : IP Address of the user accessing the data.
createExtension(`onVideoDeleteByUser`)
// [0] videoDatabaseRow : Object of Video database row
// [1] userDatabaseRow : Object of User database row. The one accessing the data.
// [2] groupKey : The ID of the group.
// [3] monitorId : The ID of the Monitor.
// [4] clientIp : IP Address of the user deleting the data.
createExtension(`onCloudVideoDeleteByUser`)
// [0] videoDatabaseRow : Object of Video database row
// [1] userDatabaseRow : Object of User database row. The one accessing the data.
// [2] groupKey : The ID of the group.
// [3] monitorId : The ID of the Monitor.
// [4] clientIp : IP Address of the user accessing the data.
createExtension(`onCloudVideoUploaded`)
// [0] insertQuery : The insert query used to save the cloud video info into the database.
/////// TIMELAPSE ////////
createExtension(`onInsertTimelapseFrame`)
// [0] initiateData : Data that was used to initiate the monitor.
// [1] insertQuery : The insert query used to save the Timelapse frame into the database.
// [2] filePath : The filesystem path of the file that was saved.
}

View File

@ -854,7 +854,7 @@ module.exports = function(s,config,lang){
apiKeyPermissions: {},
userPermissions: {},
}
const permissions = user.permissions
const permissions = user.permissions || {}
const details = user.details;
[
'auth_socket',

View File

@ -15,6 +15,38 @@ module.exports = function(s,config,lang){
}
})
})
s.onVideoDeleteByUser((videoRow,user,groupKey,monitorId,ip) => {
s.userLog({
ke: groupKey,
mid: '$USER',
},{
type: lang['Video Deleted'],
msg: {
user: {
mail: user.mail,
uid: user.uid,
ip,
},
video: videoRow
}
})
})
s.onCloudVideoDeleteByUser((videoRow,user,groupKey,monitorId,ip) => {
s.userLog({
ke: groupKey,
mid: '$USER',
},{
type: lang['Cloud Video Deleted'],
msg: {
user: {
mail: user.mail,
uid: user.uid,
ip,
},
video: videoRow
}
})
})
s.onLogout((user,groupKey,userId,ip) => {
s.userLog({
ke: groupKey,

View File

@ -735,6 +735,7 @@ module.exports = function(s,config,lang,app,io){
response.channel = activeMonitor.subStreamChannel;
break;
case'stop':
response.ok = true
activeMonitor.allowDestroySubstream = true
await destroySubstreamProcess(activeMonitor)
break;
@ -1816,11 +1817,14 @@ module.exports = function(s,config,lang,app,io){
break;
case'delete':
response.ok = true;
const clientIp = s.getClientIp(req)
switch(videoParam){
case'cloudVideos':
s.runExtensionsForArray('onCloudVideoDeleteByUser', null, [r, user, groupKey, monitorId, clientIp])
s.deleteVideoFromCloud(r,details.type || r.type || 's3')
break;
default:
s.runExtensionsForArray('onVideoDeleteByUser', null, [r, user, groupKey, monitorId, clientIp])
s.deleteVideo(r)
break;
}

View File

@ -0,0 +1,225 @@
const path = require('path');
const fs = require('fs').promises;
const { spawn } = require('child_process');
const { Client } = require('ssh2');
const os = require('os');
// ── Shinobi helpers ───────────────────────────────────────────────────────────
const configRaw = require('../conf.json'); // <- will be copied
const s = require('../libs/process.js')(process);
const config = require('../libs/config.js')(s);
const s2 = { mainDirectory: process.cwd() };
// ── CLI parsing ───────────────────────────────────────────────────────────────
const [
, , host,
sshUsername,
sshPassword,
sqlUsername = 'majesticflame',
sqlPassword = '',
sqlDatabase = 'ccio',
sqlPort = 3306,
shinobiPath = '/home/Shinobi', // <─ NEW default
] = process.argv.map(String);
if (!host || !sshUsername || !sshPassword || !sqlUsername) {
console.log(`** Invalid parameters provided!`)
if(!host)console.log('Missing Host!')
if(!sshUsername)console.log('Missing SSH Username!')
if(!sshPassword)console.log('Missing SSH Password!')
console.log(`## Example Usage :`)
console.log(`=============`)
console.log('node tools/copySystemToNewServer.js HOST SSH_USER SSH_PASS SQL_USER SQL_PASS SQL_DB SQL_PORT SHINOBI_PATH');
console.log(`=============`)
console.log(`## Usage Parameters :`)
console.log(`=============`)
console.log(`# HOST : The Server IP Address or Domain Name. Required.`)
console.log(`# SSH_USER : The username to login to SSH. Required.`)
console.log(`# SSH_PASS : The password to login to SSH. To use RSA passwordless login do "__RSA:/path/to/keyfile". Required.`)
console.log(`# SQL_USER : The username for the MySQL (MariaDB) DATABASE. Default is "majesticflame". Optional.`)
console.log(`# SQL_PASS : The password for the MySQL (MariaDB) DATABASE. Default is blank. Optional.`)
console.log(`# SQL_DB : The database name. Default is "ccio". Optional.`)
console.log(`# SQL_PORT : The database port. Default is "3306". Optional.`)
console.log(`# SHINOBI_PATH : The path where Shinobi is installed. Default is "/home/Shinobi" Optional.`)
console.log(`=============`)
process.exit(1);
}
// ── Source-side DB info (conf.json) ───────────────────────────────────────────
const localDbConf = (configRaw.db || {
host : '127.0.0.1',
user : 'majesticflame',
password: '',
database: 'ccio',
port : 3306,
});
// ── Paths ─────────────────────────────────────────────────────────────────────
const dumpName = `shinobi_dump_${Date.now()}.sql`;
const dumpPath = path.join(os.tmpdir(), dumpName);
const remoteDump = `/tmp/${dumpName}`;
const localTmpConf = path.join(os.tmpdir(), `shinobi_conf_${Date.now()}.json`);
const remoteConf = `${shinobiPath.replace(/\/+$/, '')}/conf.json`;
// ── Helper — promisified spawn ------------------------------------------------
function run(cmd, args, opts = {}) {
return new Promise((resolve, reject) => {
const p = spawn(cmd, args, { stdio: 'inherit', ...opts });
p.on('close', code => (code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`))));
});
}
// ── 1. Dump the local database ───────────────────────────────────────────────
async function dumpLocalDb() {
console.log(`\n[1/5] Dumping local DB ⇒ ${dumpPath}`);
const args = [
`-h${localDbConf.host}`,
`-P${localDbConf.port}`,
`-u${localDbConf.user}`,
`-p${localDbConf.password}`,
'--single-transaction', '--routines', '--triggers',
localDbConf.database,
];
await run('mysqldump', args, {
stdio: ['ignore', await fs.open(dumpPath, 'w'), 'inherit'],
});
console.log(' ✓ Dump complete');
}
// ── SSH helpers ───────────────────────────────────────────────────────────────
function connectSSH() {
return new Promise((resolve, reject) => {
const conn = new Client();
const opts = { host, port: 22, username: sshUsername };
if (sshPassword.startsWith('__RSA:')) {
opts.privateKey = require('fs').readFileSync(sshPassword.slice(6));
} else {
opts.password = sshPassword;
}
conn.on('ready', () => resolve(conn))
.on('error', reject)
.connect(opts);
});
}
function getSftp(conn) {
return new Promise((res, rej) => conn.sftp((e, s) => (e ? rej(e) : res(s))));
}
// ── 2. Copy dump to target ────────────────────────────────────────────────────
async function uploadDump(conn, sftp) {
console.log(`[2/5] Uploading dump ⇒ ${host}:${remoteDump}`);
await new Promise((res, rej) =>
sftp.fastPut(dumpPath, remoteDump, {}, err => (err ? rej(err) : res())),
);
console.log(' ✓ Upload complete');
}
// ── 3. Copy conf.json to target ───────────────────────────────────────────────
async function uploadConf(conn, sftp) {
console.log(`[3/5] Uploading conf.json ⇒ ${host}:${remoteConf}`);
// 3a. make sure remote Shinobi dir exists
await new Promise((resolve, reject) => {
conn.exec(`mkdir -p '${shinobiPath.replace(/'/g, `'\\''`)}'`, (err, stream) => {
if (err) return reject(err);
stream.on('close', code => (code === 0 ? resolve() : reject(new Error(`mkdir exited ${code}`))));
});
});
// 3b. write local temp conf then push
await fs.writeFile(localTmpConf, JSON.stringify(configRaw, null, 2));
await new Promise((res, rej) =>
sftp.fastPut(localTmpConf, remoteConf, {}, err => (err ? rej(err) : res())),
);
console.log(' ✓ conf.json uploaded');
}
// ── 4. Ensure DB / privileges on target ──────────────────────────────────────
async function ensureDatabase(conn) {
console.log('[4/6] Ensuring database & privileges …');
const pwdClause = sqlPassword
? `IDENTIFIED BY '${sqlPassword.replace(/'/g, `'\\''`)}'`
: '';
const sql = `
CREATE DATABASE IF NOT EXISTS \\\`${sqlDatabase}\\\`;
GRANT ALL PRIVILEGES ON \\\`${sqlDatabase}\\\`.* TO '${sqlUsername}'@'127.0.0.1' ${pwdClause};
FLUSH PRIVILEGES;
`;
const cmd = [
'mysql',
`-u${sqlUsername}`,
sqlPassword ? `-p'${sqlPassword.replace(/'/g, `'\\''`)}'` : '',
`-P${sqlPort}`,
`-e "${sql.replace(/\n/g, ' ')}"`,
].join(' ');
await new Promise((resolve, reject) => {
conn.exec(cmd, (err, stream) => {
if (err) return reject(err);
stream.on('close', code => (code === 0 ? resolve() : reject(new Error(`mysql exited ${code}`))))
.stderr.pipe(process.stderr);
stream.pipe(process.stdout);
});
});
console.log(' ✓ Database ready');
}
// ── 5. Restore DB on target ───────────────────────────────────────────────────
async function importOnTarget(conn) {
console.log('[5/6] Importing dump on target …');
const restoreCmd = [
`mysql`,
`-u${sqlUsername}`,
sqlPassword ? `-p'${sqlPassword.replace(/'/g, `'\\''`)}'` : '',
`-P${sqlPort}`,
`${sqlDatabase} < ${remoteDump}`,
].join(' ');
await new Promise((resolve, reject) => {
conn.exec(restoreCmd, (err, stream) => {
if (err) return reject(err);
stream.on('close', code => (code === 0 ? resolve() : reject(new Error(`mysql exited ${code}`))))
.stderr.pipe(process.stderr);
stream.pipe(process.stdout);
});
});
console.log(' ✓ Import complete');
}
// ── 6. Cleanup ────────────────────────────────────────────────────────────────
async function cleanup(conn) {
console.log('[6/6] Cleaning up …');
await fs.unlink(dumpPath).catch(() => {});
await fs.unlink(localTmpConf).catch(() => {});
await new Promise(res => {
conn.exec(`rm -f '${remoteDump}'`, () => res()); // ignore errors
});
conn.end();
console.log(' ✓ All done!');
}
// ── Main orchestrator ─────────────────────────────────────────────────────────
(async () => {
try {
await dumpLocalDb();
const conn = await connectSSH();
const sftp = await getSftp(conn);
await uploadDump(conn, sftp);
await uploadConf(conn, sftp);
await ensureDatabase(conn);
await importOnTarget(conn);
await cleanup(conn);
} catch (err) {
console.error('‼ Error:', err.message);
process.exit(1);
}
})();

View File

@ -43,6 +43,7 @@ function makeConfig(streamUrl){
// streamUrl = 'rtsp://1.1.1.1:554/'
const copyOfBaseConfig = Object.assign({},monitorBase)
const urlParts = getUrlParts(streamUrl)
console.log(streamUrl)
copyOfBaseConfig.mid = generateId()
copyOfBaseConfig.name = urlParts.hostname
copyOfBaseConfig.host = urlParts.hostname
@ -58,7 +59,7 @@ function run(){
const newMonitorsList = []
const fileName = `${importFilePath}.json`
importList.forEach((streamUrl) => {
newMonitorsList.push(makeConfig(streamUrl))
if(streamUrl)newMonitorsList.push(makeConfig(streamUrl))
})
console.log(`New JSON written to ${fileName}`)
fs.writeFileSync(fileName,JSON.stringify(newMonitorsList));

View File

@ -0,0 +1,86 @@
const net = require('net');
const os = require('os');
// Function to check if a port is open on a given IP
async function checkPort(ip, port, timeout = 1000) {
return new Promise((resolve) => {
const socket = new net.Socket();
let status = 'closed';
socket.setTimeout(timeout);
socket.on('connect', () => {
status = 'open';
socket.destroy();
resolve(status);
});
socket.on('timeout', () => {
socket.destroy();
resolve(status);
});
socket.on('error', () => {
socket.destroy();
resolve(status);
});
socket.connect(port, ip);
});
}
// Function to get all local network interfaces
function getLocalIPRanges() {
const interfaces = os.networkInterfaces();
const ranges = [];
for (const name of Object.keys(interfaces)) {
for (const iface of interfaces[name]) {
// Skip internal and non-IPv4 addresses
if (iface.internal || iface.family !== 'IPv4') continue;
// Get network range (simple /24 assumption)
const ipParts = iface.address.split('.').slice(0, 3);
ranges.push(ipParts.join('.') + '.0/24');
}
}
return ranges;
}
// Function to generate all IPs in a range
function* generateIPsFromRange(ipRange) {
const [base, mask] = ipRange.split('/');
const baseParts = base.split('.').map(Number);
// Simple handling for /24 ranges
if (mask === '24') {
for (let i = 1; i <= 254; i++) {
yield `${baseParts[0]}.${baseParts[1]}.${baseParts[2]}.${i}`;
}
}
}
// Main scanning function
async function scanNetwork() {
const ipRanges = getLocalIPRanges();
const portsToCheck = [8080, 8005];
const portNamesToCheck = {"8080": "CORE", "8005": "CENTRAL"};
console.log(`Starting scan of local network ranges: ${ipRanges.join(', ')}`);
console.log(`Checking ports: ${portsToCheck.join(', ')}`);
for (const range of ipRanges) {
console.log(`\nScanning range: ${range}`);
for (const ip of generateIPsFromRange(range)) {
for (const port of portsToCheck) {
checkPort(ip, port).then((status) => {
if(status === 'open')console.log(`${ip}, Port: ${port} - ${status}`, portNamesToCheck[port]);
})
}
}
}
}
// Run the scan
scanNetwork().catch(console.error);

View File

@ -9,6 +9,7 @@ var liveGridOpenCount = 0
var liveGridPauseScrollTimeout = null;
var liveGridPlayingNow = {};
var currentPtzPresetPosition = {};
var detectionDrawDelays = {};
//
var onLiveStreamInitiateExtensions = []
function onLiveStreamInitiate(callback){
@ -169,6 +170,7 @@ function buildLiveGridBlock(monitor){
var streamBlockInfo = definitions['Monitor Stream Window']
var wasLiveGridLogStreamOpenBefore = isLiveGridLogStreamOpenBefore(monitorId)
if(!loadedLiveGrids[monitor.mid])loadedLiveGrids[monitor.mid] = {}
var detectionDrawDelay = detectionDrawDelays[monitorId];
var quickLinkHtml = ''
$.each(streamBlockInfo.quickLinks,function(n,button){
if(button.eval && !eval(button.eval))return;
@ -189,6 +191,11 @@ function buildLiveGridBlock(monitor){
${streamBlockInfo.streamBlockHudHtml || ''}
<div class="controls">
${streamBlockInfo.streamBlockHudControlsHtml || ''}
<div class="text-start">
<div class="slider-container">
<input type="range" class="slider detection-delay" step="0.1" min="0" max="10" title="${lang['Detection Draw Delay']} : ${detectionDrawDelay}" value="${detectionDrawDelay || 0}">
</div>
</div>
</div>
</div>
${streamElement}
@ -978,7 +985,8 @@ function pauseMonitorItem(monitorId){
function resumeMonitorItem(monitorId){
// needs to know about substream
liveGridPlayingNow[monitorId] = true
resetMonitorCanvas(monitorId,true,null)
var monitor = loadedMonitors[monitorId];
resetMonitorCanvas(monitorId,true,monitor.subStreamChannel)
}
function isScrolledIntoView(elem){
var el = $(elem)
@ -1066,12 +1074,29 @@ function showHideSubstreamActiveIcon(monitorId, show){
}
}
function setDetectionDrawDelay(monitorId, value){
var theValue = parseFloat(value) || 0
if(theValue == 0){
delete(detectionDrawDelays[monitorId])
}else{
detectionDrawDelays[monitorId] = theValue
}
dashboardOptions('detectionDrawDelays', detectionDrawDelays)
}
$(document).ready(function(e){
liveGrid
.on('dblclick','.stream-block',function(){
var monitorItem = $(this).parents('[data-mid]');
fullScreenLiveGridStream(monitorItem)
})
.on('change','.detection-delay',function(){
var el = $(this);
var monitorId = el.parents('[data-mid]').attr('data-mid');
var value = el.val()
el.attr('title', `${lang['Detection Draw Delay']} : ${value}`)
// console.log('setDetectionDrawDelay',monitorId, value)
setDetectionDrawDelay(monitorId, value)
})
$('body')
.resize(function(){
resetAllLiveGridDimensionsInMemory()
@ -1352,7 +1377,7 @@ $(document).ready(function(e){
onToggleSideBarMenuHide(function (isHidden){
setTimeout(updateAllLiveGridElementsHeightWidth,2000)
})
onWebSocketEvent(function (d){
onWebSocketEvent(async function (d){
switch(d.f){
case'control_ptz_preset_changed':
currentPtzPresetPosition[d.mid] = d.profileToken;
@ -1446,7 +1471,7 @@ $(document).ready(function(e){
monitorElement.removeClass('doObjectDetection')
}
if(matrices && matrices.length > 0){
drawMatrices(d,{
await drawMatrices(d,{
theContainer: liveGridElement.eventObjects,
height: liveGridElement.height,
width: liveGridElement.width,
@ -1556,4 +1581,5 @@ $(document).ready(function(e){
window.monitorsWatchOnLiveGrid = monitorsWatchOnLiveGrid;
window.closeAllLiveGridPlayers = closeAllLiveGridPlayers;
window.closeLiveGridPlayers = closeLiveGridPlayers;
detectionDrawDelays = dashboardOptions().detectionDrawDelays || {}
})

View File

@ -454,6 +454,7 @@ window.getMonitorEditFormFields = function(){
monitorConfig.details.input_map_choices = getInputMapDesignations()
monitorConfig.details.stream_channels = getStreamChannelConnectionInfo()
monitorConfig.details.triggerMonitorsPtzTargets = getSelectedEventBasedPtzPresets()
monitorConfig.details.detectorLineCounterSettings = getLineCounterState()
// if(monitorConfig.protocol=='rtsp'){monitorConfig.ext='mp4',monitorConfig.type='rtsp'}
if(errorsFound.length > 0){
@ -724,7 +725,14 @@ async function importIntoMonitorEditor(options){
}
}
});
//
if(monitorDetails.detectorLineCounter == '1'){
setTimeout(function(){
drawLineCounterCanvas(Object.assign({}, monitorConfig, { details: monitorDetails }))
},500)
}else{
clearLineCounterCanvas()
}
//
await getPluginsList(monitorConfig)
//

View File

@ -0,0 +1,164 @@
$(document).ready(function(){
const lineCounterCanvas = $('#monitorSettings-lineCounter-canvas-container')
const monitorEditorWindow = $('#tab-monitorSettings')
const lineCounterSpacingField = $('#detector-line-counter-spacing')
const lineCounterUpName = $('#detector-line-counter-name-up')
const lineCounterDownName = $('#detector-line-counter-name-down')
const lineCounterResetDailyField = $('#detector-line-counter-reset-daily')
const lineCounterToggle = monitorEditorWindow.find('[detail=detectorLineCounter]')
const scaleXObject = monitorEditorWindow.find('[detail=detector_scale_x_object]')
const scaleYObject = monitorEditorWindow.find('[detail=detector_scale_y_object]')
let currentLineCounterCanvas = null;
let lastEmbedUrl = '';
function loadLineCounterForMonitorSettings(monitor){
drawLineCounterCanvas(monitor)
}
function clearLineCounterCanvas(){
if(currentLineCounterCanvas){
currentLineCounterCanvas.destroy()
currentLineCounterCanvas = null;
lineCounterCanvas.empty()
lastEmbedUrl = null;
}
}
function getLineCounterSettings(monitor){
const lineCounterSettings = monitor.details.detectorLineCounterSettings || {};
const imageWidth = parseInt(monitor.details.detector_scale_x_object) || 1280
const imageHeight = parseInt(monitor.details.detector_scale_y_object) || 720
const {
lineSpacing = 40,
refreshRate = 10,
lines,
} = lineCounterSettings;
return {
lineSpacing,
refreshRate,
lines,
imageWidth,
imageHeight,
}
}
function drawLineCounterCanvas(monitor){
clearLineCounterCanvas()
const {
lineSpacing,
refreshRate,
lines,
imageWidth,
imageHeight,
} = getLineCounterSettings(monitor)
const containerWidth = lineCounterCanvas.width()
const ratio = containerWidth / imageWidth / 2
const newWidth = imageWidth * ratio
const newHeight = imageHeight * ratio
lineCounterCanvas.html(`<div style="position:relative;display:inline-block;height:${newHeight}px;width:${newWidth}px;text-align: initial;">
<iframe style="position:absolute" width="${newWidth}" height="${newHeight}"></iframe>
<canvas id="monitorSettings-lineCounter-canvas" width="${newWidth}" height="${newHeight}" style="position:absolute"></canvas>
</div>`)
currentLineCounterCanvas = new ParallelLineDrawer('monitorSettings-lineCounter-canvas', {
lineSpacing,
refreshRate,
originalWidth: imageWidth,
originalHeight: imageHeight,
});
currentLineCounterCanvas.scaleCanvas(ratio);
if(monitor){
currentLineCounterCanvas.loadState({
lines
});
}
loadLineCounterTagsField(monitor)
loadLineCounterLiveStream(monitor)
lineCounterSpacingField.val(lineSpacing)
}
function getLineCounterState(){
const {
lineSpacing,
refreshRate,
lines,
} = currentLineCounterCanvas ? currentLineCounterCanvas.getState() : {};
const upName = lineCounterUpName.val().trim()
const downName = lineCounterDownName.val().trim()
const resetDaily = lineCounterResetDailyField.val() === '1'
return {
lineSpacing,
refreshRate,
lines,
upName,
downName,
resetDaily,
}
}
function resetLineCounterState(){
if(currentLineCounterCanvas){
currentLineCounterCanvas.reset();
}
}
function loadLineCounterTagsField(monitorConfig){
const monitorEditorWindow = $('#tab-monitorSettings')
const lineCounterTagsInput = monitorEditorWindow.find('[detail=detectorLineCounterTags]')
const monitorTags = monitorConfig && monitorConfig.details.detectorLineCounterTags ? monitorConfig.details.detectorLineCounterTags.split(',') : []
lineCounterTagsInput.tagsinput('removeAll');
monitorTags.forEach((tag) => {
lineCounterTagsInput.tagsinput('add',tag);
});
}
function loadLineCounterLiveStream(monitor){
const monitorId = monitor.mid
const liveElement = lineCounterCanvas.find('iframe')
const imageWidth = parseInt(monitor.details.detector_scale_x_object) || 1280
const imageHeight = parseInt(monitor.details.detector_scale_y_object) || 720
if(monitor.mode === 'stop'){
var apiUrl = placeholder.getData(placeholder.plcimg({
bgcolor: '#000',
text: lang[`Cannot watch a monitor that isn't running.`],
size: imageWidth+'x'+imageHeight
}))
liveElement.attr('src',apiUrl)
}else{
var apiUrl = `${getApiPrefix('embed')}/${monitorId}/fullscreen|jquery|gui|relative?host=${location.pathname}`
liveElement.attr('src',apiUrl)
lastEmbedUrl = apiUrl
}
}
function setIframeSource(embedUrl){
lineCounterCanvas.find('iframe').attr('src', embedUrl || 'about:blank')
}
function isLineCounterEnabled(){
lineCounterToggle.val() === '1'
}
lineCounterToggle.change(function(e){
if($(this).val() === '1'){
drawLineCounterCanvas(monitorEditorSelectedMonitor)
}else{
clearLineCounterCanvas()
}
});
scaleXObject.change(function(e){
if(lineCounterToggle.val() === '1'){
drawLineCounterCanvas(monitorEditorSelectedMonitor)
}
});
scaleYObject.change(function(e){
if(lineCounterToggle.val() === '1'){
drawLineCounterCanvas(monitorEditorSelectedMonitor)
}
});
lineCounterSpacingField.change(function(e){
var value = parseInt($(this).val()) || 40;
currentLineCounterCanvas.setLineSpacing(value)
});
addOnTabAway('monitorSettings', function(){
setIframeSource()
})
addOnTabOpen('monitorSettings', function(){
})
addOnTabReopen('monitorSettings', function(){
if(lastEmbedUrl)setIframeSource(lastEmbedUrl)
})
window.drawLineCounterCanvas = drawLineCounterCanvas
window.clearLineCounterCanvas = clearLineCounterCanvas
window.getLineCounterState = getLineCounterState
window.isLineCounterEnabled = isLineCounterEnabled
})

View File

@ -722,7 +722,7 @@ function buildPosePoints(bodyParts, x, y){
}
return theArray;
}
function drawMatrices(event, options, autoRemoveTimeout, drawTrails){
async function drawMatrices(event, options, autoRemoveTimeout, drawTrails){
var theContainer = options.theContainer
var height = options.height
var width = options.width
@ -786,6 +786,15 @@ function drawMatrices(event, options, autoRemoveTimeout, drawTrails){
}
$.each(event.details.matrices, processMatrix);
$.each(moreMatrices, processMatrix);
var detectionDrawDelay = detectionDrawDelays[monitorId];
if(detectionDrawDelay){
await (new Promise((resolve) => {
setTimeout(() => {
resolve()
}, detectionDrawDelay * 1000)
}))
// console.log(`Delayed Draw by ${detectionDrawDelay} seconds`)
}
var addedEls = theContainer.append(html)
if(autoRemoveTimeout){
addedEls = addedEls.find('.fresh-detected-object').removeClass('fresh-detected-object')

View File

@ -75,6 +75,7 @@ $(document).ready(function(){
notifyColor = 'success'
if(data.user){
var account = data.user
account.details = safeJsonParse(account.details)
loadedSubAccounts[account.uid] = account;
drawSubAccountRow(account)
theWindowForm.find('[name="uid"]').val(account.uid)
@ -105,6 +106,7 @@ $(document).ready(function(){
$.each(form,function(n,v){
account[n] = v
});
account.details = safeJsonParse(account.details);
accountTable.find(`[uid="${uid}"] .mail`).text(form.mail)
new PNotify({
title : lang['Account Edited'],
@ -152,7 +154,12 @@ $(document).ready(function(){
label: lang['Can Delete Videos and Events'],
},
];
var drawSelectableForPermissionForm = function(account){
var drawSelectableForPermissionForm = async function(account){
var details = account ? safeJsonParse(account.details) : {}
if(account){
await loadPermissions()
details = Object.assign({}, details, loadedPermissions[details.permissionSet] || {})
}
var html = `
<thead class="text-center">
<tr>
@ -167,7 +174,7 @@ $(document).ready(function(){
html += `<td class="text-start">${monitor.name} (${monitor.mid})</td>`
html += `<td>${(monitor.tags || '').split(',').map(item => `<span class="label label-primary">${item}</span>`)}</td>`
$.each(permissionTypeNames,function(n,permissionType){
const isChecked = account && (account.details[permissionType.name] || []).indexOf(monitor.mid) > -1;
const isChecked = account && (details[permissionType.name] || []).indexOf(monitor.mid) > -1;
html += `<td><input class="form-check-input" type="checkbox" data-monitor="${monitor.mid}" value="${permissionType.name}" ${isChecked ? 'checked' : ''}></td>`
})
html += `</tr>`
@ -191,8 +198,9 @@ $(document).ready(function(){
}
var setPermissionSelectionsToFields = async function(uid){
var account = loadedSubAccounts[uid]
var details = account.details
var details = safeJsonParse(account.details)
await loadPermissions()
details = Object.assign({}, details, loadedPermissions[details.permissionSet] || {})
// load values to Account Information : email, password, etc.
$.each(account,function(n,v){
theWindowForm.find('[name="'+n+'"]').val(v)
@ -207,7 +215,7 @@ $(document).ready(function(){
}
var openSubAccountEditor = async function(uid){
var account = loadedSubAccounts[uid]
drawSelectableForPermissionForm(account)
await drawSelectableForPermissionForm(account)
await setPermissionSelectionsToFields(uid)
theWindowForm.find('[name="pass"],[name="password_again"]').val('')
permissionsSection.show()
@ -273,7 +281,7 @@ $(document).ready(function(){
var defaultValue = el.attr('data-default')
el.val(defaultValue)
})
drawSelectableForPermissionForm()
await drawSelectableForPermissionForm()
setSubmitButtonState(lang['Add New'],'plus')
theWindowForm.find('input').val('')
}

View File

@ -712,7 +712,7 @@ async function unarchiveVideos(videos){
}
function buildDefaultVideoMenuItems(file,options){
var isLocalVideo = !file.videoSet || file.videoSet === 'videos'
var href = getApiHost() + file.href + `${!isLocalVideo ? `?type=${file.type}` : ''}`
var href = file.href + `${!isLocalVideo ? `?type=${file.type}` : ''}`
options = options ? options : {play: true}
return `
<li><a class="dropdown-item" href="${href}" download>${lang.Download}</a></li>

View File

@ -79,6 +79,7 @@ $(document).ready(function(e){
pageNumber = pageNumber || 1;
pageSize = pageSize || 10;
var use24HourTime = dashboardOptions().use24HourTime == '1';
var dateRange = getSelectedTime(dateSelector);
var searchQuery = objectTagSearchField.val() || null;
var andOnly = objectTagSearchFieldAndOnly.val() || '0';
@ -179,8 +180,8 @@ $(document).ready(function(e){
mid: file.mid,
time: `
<div>${timeAgo(file.time)}</div>
<div><small><b>${lang.Start} :</b> ${formattedTime(file.time, 'DD-MM-YYYY hh:mm:ss AA')}</small></div>
<div><small><b>${lang.End} :</b> ${formattedTime(file.end, 'DD-MM-YYYY hh:mm:ss AA')}</small></div>`,
<div><small><b>${lang.Start} :</b> ${formattedTime(file.time, !use24HourTime)}</small></div>
<div><small><b>${lang.End} :</b> ${formattedTime(file.end, !use24HourTime)}</small></div>`,
objects: `<div style="word-break: break-word;max-width:125px;">${file.objects}</div>`,
tags: `
${file.ext ? `<span class="badge badge-${file.ext ==='webm' ? `primary` : 'danger'}">${file.ext}</span>` : ''}

236
web/assets/vendor/lineDrawForCanvas.js vendored Normal file
View File

@ -0,0 +1,236 @@
class ParallelLineDrawer {
constructor(canvasId, options = {}) {
// Configuration with defaults
this.lineSpacing = options.lineSpacing || 40;
this.refreshRate = options.refreshRate || 10;
this.onLinesDrawn = options.onLinesDrawn || null;
// Ratio handling
this.originalWidth = options.originalWidth || null;
this.originalHeight = options.originalHeight || null;
this.currentRatio = 1; // Default ratio (no scaling)
// Canvas setup
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext("2d");
// Set initial canvas size if original dimensions provided
if (this.originalWidth && this.originalHeight) {
this.resizeCanvas(this.originalWidth, this.originalHeight);
}
// State variables
this.cursorX = 0;
this.cursorY = 0;
this.firstClick = [0, 0];
this.intervalLoop = null;
this.isActive = false;
this.currentLines = null;
// Bind event handlers
this.handleMouseMove = this.handleMouseMove.bind(this);
this.startDragLine = this.startDragLine.bind(this);
this.stopDragLine = this.stopDragLine.bind(this);
this.initialize();
}
resizeCanvas(width, height) {
this.originalWidth = width;
this.originalHeight = height;
this.canvas.width = width;
this.canvas.height = height;
this.currentRatio = 1;
this.redrawLines();
}
scaleCanvas(scaleFactor) {
if (!this.originalWidth || !this.originalHeight) return;
this.currentRatio = scaleFactor;
this.canvas.width = this.originalWidth * scaleFactor;
this.canvas.height = this.originalHeight * scaleFactor;
this.redrawLines();
}
initialize() {
document.addEventListener('mousemove', this.handleMouseMove);
this.canvas.addEventListener('mousedown', this.startDragLine);
this.canvas.addEventListener('mouseup', this.stopDragLine);
this.canvas.addEventListener('mouseleave', this.stopDragLine);
this.isActive = true;
}
destroy() {
this.stopDragLine();
document.removeEventListener('mousemove', this.handleMouseMove);
this.canvas.removeEventListener('mousedown', this.startDragLine);
this.canvas.removeEventListener('mouseup', this.stopDragLine);
this.canvas.removeEventListener('mouseleave', this.stopDragLine);
this.isActive = false;
}
// Load previous state (lines, spacing, refresh rate)
loadState(state) {
if (!state) return;
// Restore configuration
if (state.lineSpacing) this.lineSpacing = state.lineSpacing;
if (state.refreshRate) this.refreshRate = state.refreshRate;
if (state.originalWidth && state.originalHeight) {
this.originalWidth = state.originalWidth;
this.originalHeight = state.originalHeight;
this.scaleCanvas(state.currentRatio || 1);
}
// Restore lines with ratio adjustment
if (state.lines && Array.isArray(state.lines) && state.lines.length === 2) {
this.currentLines = state.lines.map(line => ({
start: {
x: line.start.x * this.currentRatio,
y: line.start.y * this.currentRatio
},
end: {
x: line.end.x * this.currentRatio,
y: line.end.y * this.currentRatio
}
}));
this.redrawLines();
}
}
// Redraw existing lines
redrawLines() {
if (!this.currentLines) return;
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.drawLine(this.currentLines[0], '#FF0000'); // Red
this.drawLine(this.currentLines[1], '#0000FF'); // Blue
}
handleMouseMove(e) {
const rect = this.canvas.getBoundingClientRect();
this.cursorX = e.clientX - rect.left;
this.cursorY = e.clientY - rect.top;
}
startDragLine(e) {
const rect = this.canvas.getBoundingClientRect();
this.firstClick = [
e.clientX - rect.left,
e.clientY - rect.top
];
this.intervalLoop = setInterval(() => {
// Check if mouse is at edge of screen
if (this.cursorX <= 0 || this.cursorY <= 0 ||
this.cursorX >= this.canvas.width || this.cursorY >= this.canvas.height) {
this.stopDragLine();
return;
}
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
// Calculate offsets for parallel lines
const angle = Math.atan2(
this.cursorY - this.firstClick[1],
this.cursorX - this.firstClick[0]
);
const perpendicularAngle = angle + Math.PI/2;
const offsetX = Math.cos(perpendicularAngle) * (this.lineSpacing/2);
const offsetY = Math.sin(perpendicularAngle) * (this.lineSpacing/2);
// Calculate line coordinates
this.currentLines = [
{
start: {
x: this.firstClick[0] - offsetX,
y: this.firstClick[1] - offsetY
},
end: {
x: this.cursorX - offsetX,
y: this.cursorY - offsetY
}
},
{
start: {
x: this.firstClick[0] + offsetX,
y: this.firstClick[1] + offsetY
},
end: {
x: this.cursorX + offsetX,
y: this.cursorY + offsetY
}
}
];
// Draw lines with colors
this.drawLine(this.currentLines[0], '#FF0000'); // Red
this.drawLine(this.currentLines[1], '#0000FF'); // Blue
}, this.refreshRate);
}
drawLine(coords, color) {
this.ctx.beginPath();
this.ctx.moveTo(coords.start.x, coords.start.y);
this.ctx.lineTo(coords.end.x, coords.end.y);
this.ctx.strokeStyle = color;
this.ctx.lineWidth = 2;
this.ctx.stroke();
}
stopDragLine() {
if (this.intervalLoop) {
clearInterval(this.intervalLoop);
this.intervalLoop = null;
if (this.onLinesDrawn && this.currentLines) {
this.onLinesDrawn(this.getCurrentLines());
}
}
}
// Returns current state (lines + configuration)
getState() {
return {
lines: this.getCurrentLines(),
lineSpacing: this.lineSpacing,
refreshRate: this.refreshRate,
originalWidth: this.originalWidth,
originalHeight: this.originalHeight,
currentRatio: this.currentRatio
};
}
getCurrentLines() {
if (!this.currentLines) return null;
// Return lines in original coordinates (divide by current ratio)
const newLines = this.currentLines.map(line => ({
start: {
x: line.start.x / this.currentRatio,
y: line.start.y / this.currentRatio
},
end: {
x: line.end.x / this.currentRatio,
y: line.end.y / this.currentRatio
}
}));
console.log(newLines,this.currentLines[0].start.x,this.currentRatio)
return newLines
}
// Configuration setters
setLineSpacing(spacing) {
this.lineSpacing = spacing;
}
setRefreshRate(rate) {
this.refreshRate = rate;
}
setCallback(callback) {
this.onLinesDrawn = callback;
}
}

View File

@ -11,6 +11,7 @@
<script src="<%-window.libURL%>assets/vendor/bootstrap5/js/bootstrap.bundle.min.js"></script>
<script src="<%-window.libURL%>assets/vendor/bootstrap-table/bootstrap-table.min.js"></script>
<script src="<%-window.libURL%>assets/vendor/bootstrap5-tags/bootstrap-tagsinput.min.js"></script>
<script src="<%-window.libURL%>assets/vendor/lineDrawForCanvas.js"></script>
<!-- <script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script> -->
<script src="<%-window.libURL%>assets/vendor/js/daterangepicker.js"></script>
@ -34,6 +35,7 @@
<script src="<%-window.libURL%>assets/js/bs5.subAccountManager.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.apiKeys.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.monitorSettings.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.monitorSettings.lineCounter.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.monitorsUtils.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.monitorStates.js"></script>
<script src="<%-window.libURL%>assets/js/bs5.schedules.js"></script>