Fixed the default webroot directory to apache's correct webroot. Also
renamed separated outDir from webRoot and made webRoot changeable on the
config file.

Added logging the recorder and detection loops to help with debugging
and troubleshooting. Just like the video clips, max log lines were added
to control the size of the data being saved to storage.
This commit is contained in:
Maurice ONeal 2022-12-11 10:25:22 -05:00
parent ce4a326b24
commit baeaabbd55
10 changed files with 277 additions and 74 deletions

View File

@ -3,5 +3,5 @@ project( MotionWatch )
find_package( OpenCV REQUIRED ) find_package( OpenCV REQUIRED )
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -pthread") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -pthread")
include_directories( ${OpenCV_INCLUDE_DIRS} ) include_directories( ${OpenCV_INCLUDE_DIRS} )
add_executable( mow src/main.cpp src/common.cpp src/mo_detect.cpp src/web.cpp ) add_executable( mow src/main.cpp src/common.cpp src/mo_detect.cpp src/web.cpp src/logger.cpp )
target_link_libraries( mow ${OpenCV_LIBS} ) target_link_libraries( mow ${OpenCV_LIBS} )

View File

@ -114,7 +114,12 @@ vector<string> lsDirsInDir(const string &path)
{ {
if (ent->d_type & DT_DIR) if (ent->d_type & DT_DIR)
{ {
names.push_back(string(ent->d_name)); auto name = string(ent->d_name);
if ((name != "..") || (name != "."))
{
names.push_back(name);
}
} }
} }
@ -210,17 +215,22 @@ bool rdConf(shared_t *share)
share->postCmd.clear(); share->postCmd.clear();
share->buffDir.clear(); share->buffDir.clear();
share->retCode = 0;
share->frameGap = 10; share->frameGap = 10;
share->pixThresh = 30; share->pixThresh = 30;
share->imgThresh = 512; share->imgThresh = 512;
share->secs = 60; share->secs = 60;
share->maxDays = 15; share->maxDays = 15;
share->maxClips = 30; share->maxClips = 30;
share->maxLogLines = 1000;
share->camName = path(share->conf.c_str()).filename(); share->camName = path(share->conf.c_str()).filename();
share->outDir = "/var/www/hmtl"; share->webRoot = "/var/www/html";
share->vidExt = "mp4"; share->vidExt = "mp4";
share->recLoopWait = false; share->recLoopWait = false;
share->skipCmd = false; share->skipCmd = false;
share->webBg = "#485564";
share->webTxt = "#dee5ee";
share->webFont = "courier";
do do
{ {
@ -230,7 +240,10 @@ bool rdConf(shared_t *share)
{ {
rdLine("cam_name = ", line, &share->camName); rdLine("cam_name = ", line, &share->camName);
rdLine("recording_stream = ", line, &share->recordUrl); rdLine("recording_stream = ", line, &share->recordUrl);
rdLine("output_dir = ", line, &share->outDir); rdLine("web_root = ", line, &share->webRoot);
rdLine("web_text = ", line, &share->webTxt);
rdLine("web_bg = ", line, &share->webBg);
rdLine("web_font = ", line, &share->webFont);
rdLine("post_cmd = ", line, &share->postCmd); rdLine("post_cmd = ", line, &share->postCmd);
rdLine("duration = ", line, &share->secs); rdLine("duration = ", line, &share->secs);
rdLine("buff_dir = ", line, &share->buffDir); rdLine("buff_dir = ", line, &share->buffDir);
@ -239,12 +252,16 @@ bool rdConf(shared_t *share)
rdLine("img_thresh = ", line, &share->imgThresh); rdLine("img_thresh = ", line, &share->imgThresh);
rdLine("max_days = ", line, &share->maxDays); rdLine("max_days = ", line, &share->maxDays);
rdLine("max_clips = ", line, &share->maxClips); rdLine("max_clips = ", line, &share->maxClips);
rdLine("max_log_lines = ", line, &share->maxLogLines);
rdLine("vid_container = ", line, &share->vidExt); rdLine("vid_container = ", line, &share->vidExt);
} }
} while(!line.empty()); } while(!line.empty());
share->outDir = cleanDir(share->outDir) + "/" + share->camName; share->outDir = cleanDir(share->webRoot) + "/" + share->camName;
createDirTree(cleanDir(share->buffDir));
createDirTree(share->outDir);
if (share->init) if (share->init)
{ {
@ -252,11 +269,6 @@ bool rdConf(shared_t *share)
share->init = false; share->init = false;
} }
createDirTree(cleanDir(share->buffDir));
createDirTree(share->outDir);
share->retCode = 0;
} }
varFile.close(); varFile.close();

View File

@ -36,11 +36,13 @@ using namespace cv;
using namespace std; using namespace std;
using namespace std::filesystem; using namespace std::filesystem;
#define APP_VER "1.5.t2" #define APP_VER "1.5.t3"
#define APP_NAME "Motion Watch" #define APP_NAME "Motion Watch"
struct shared_t struct shared_t
{ {
vector<string> recLogLines;
vector<string> detLogLines;
string recordUrl; string recordUrl;
string outDir; string outDir;
string postCmd; string postCmd;
@ -48,8 +50,13 @@ struct shared_t
string buffDir; string buffDir;
string vidExt; string vidExt;
string camName; string camName;
string webBg;
string webTxt;
string webFont;
string webRoot;
bool init; bool init;
bool recLoopWait; bool recLoopWait;
bool logRun;
bool skipCmd; bool skipCmd;
int frameGap; int frameGap;
int pixThresh; int pixThresh;
@ -57,6 +64,7 @@ struct shared_t
int secs; int secs;
int maxDays; int maxDays;
int maxClips; int maxClips;
int maxLogLines;
int retCode; int retCode;
}; };

73
src/logger.cpp Normal file
View File

@ -0,0 +1,73 @@
// This file is part of Motion Watch.
// Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include "logger.h"
void recLog(const string &line, shared_t *share)
{
share->recLogLines.push_back(genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n");
}
void detLog(const string &line, shared_t *share)
{
share->detLogLines.push_back(genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "<br>\n");
}
void enforceMaxLogLines(vector<string> &log, shared_t *share)
{
while (log.size() > share->maxLogLines)
{
log.erase(log.begin());
}
}
string combine(const vector<string> &log)
{
string ret;
for (auto &&line : log) ret += line;
return ret;
}
void logLoop(shared_t *share)
{
string htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n";
htmlText += "<head>\n";
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
htmlText += "<meta http-equiv='refresh' content='10'>";
htmlText += "</head>\n";
htmlText += "<body>\n";
htmlText += "<p>\n";
while (share->logRun && (share->retCode == 0))
{
sleep(10);
enforceMaxLogLines(share->recLogLines, share);
enforceMaxLogLines(share->detLogLines, share);
auto recLogFilePath = cleanDir(share->outDir) + "/recording_log.html";
auto detLogFilePath = cleanDir(share->outDir) + "/detection_log.html";
ofstream recFile(recLogFilePath.c_str());
ofstream detFile(detLogFilePath.c_str());
recFile << htmlText << combine(share->recLogLines) << "</p>\n</body>\n</html>";
detFile << htmlText << combine(share->detLogLines) << "</p>\n</body>\n</html>";
recFile.close();
detFile.close();
}
}

24
src/logger.h Normal file
View File

@ -0,0 +1,24 @@
#ifndef lOGGER_H
#define lOGGER_H
// This file is part of Motion Watch.
// Motion Watch is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// Motion Watch is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
#include "common.h"
void recLog(const string &line, shared_t *share);
void detLog(const string &line, shared_t *share);
void logLoop(shared_t *share);
void enforceMaxLogLines(vector<string> &log, shared_t *share);
string combine(const vector<string> &log);
#endif // lOGGER_H

View File

@ -11,9 +11,12 @@
// GNU General Public License for more details. // GNU General Public License for more details.
#include "mo_detect.h" #include "mo_detect.h"
#include "logger.h"
void detectLoop(shared_t *share) void detectLoop(shared_t *share)
{ {
detLog("detectLoop() -- start", share);
vector<string> bufFiles; vector<string> bufFiles;
do do
@ -35,10 +38,12 @@ void detectLoop(shared_t *share)
{ {
share->skipCmd = true; share->skipCmd = true;
detLog("motion detected in file: " + fullPath, share);
wrOut(fullPath, thumbNail, share); wrOut(fullPath, thumbNail, share);
} }
else else
{ {
detLog("no motion detected in file: " + fullPath + " removing it.", share);
remove(fullPath.c_str()); remove(fullPath.c_str());
} }
} }
@ -48,23 +53,29 @@ void detectLoop(shared_t *share)
} }
} }
while (!bufFiles.empty()); while (!bufFiles.empty());
detLog("detectLoop() -- finished", share);
} }
void recLoop(shared_t *share) void recLoop(shared_t *share)
{ {
while (rdConf(share)) while (rdConf(share))
{ {
recLog("recLoop() -- start", share);
if (!fileExists("/tmp/mow-lock")) if (!fileExists("/tmp/mow-lock"))
{ {
recLog("/tmp/mow-lock not found, assuming it is safe to update the webroot page.", share);
recLog("webroot page = " + cleanDir(share->webRoot) + "/index.html", share);
system("touch /tmp/mow-lock"); system("touch /tmp/mow-lock");
genCSS(share);
auto webRoot = path(share->outDir).parent_path().string(); genHTMLul(share->webRoot, string(APP_NAME) + " " + string(APP_VER), share);
genHTMLul(webRoot, string(APP_NAME) + " " + string(APP_VER));
system("rm /tmp/mow-lock"); system("rm /tmp/mow-lock");
} }
else
createDirTree(share->buffDir); {
recLog("/tmp/mow-lock pesent, skipping update of the webroot page.", share);
}
auto bufPath = cleanDir(share->buffDir) + "/%03d." + share->vidExt; auto bufPath = cleanDir(share->buffDir) + "/%03d." + share->vidExt;
auto secs = to_string(share->secs); auto secs = to_string(share->secs);
@ -73,14 +84,24 @@ void recLoop(shared_t *share)
thread th2(detectLoop, share); thread th2(detectLoop, share);
if (system(cmd.c_str()) != 0) recLog("detect_loop started in a seperate thread.", share);
recLog("ffmpeg = " + cmd, share);
auto ret = system(cmd.c_str());
if (ret == 0)
{ {
recLog("ffmpeg_return_code = " + to_string(ret), share);
share->recLoopWait = true; share->recLoopWait = true;
th2.join(); th2.join();
} }
else else
{ {
recLog("ffmpeg failed, cooling down for " + to_string(share->secs) + "secs.", share);
recLog("ffmpeg_return_code = " + to_string(ret), share);
// simulate that ffmpeg is running even after it has failed. // simulate that ffmpeg is running even after it has failed.
sleep(share->secs); sleep(share->secs);
@ -89,9 +110,19 @@ void recLoop(shared_t *share)
if (!share->skipCmd) if (!share->skipCmd)
{ {
recLog("motion not detected by detect loop.", share);
recLog("running post command = " + share->postCmd, share);
system(share->postCmd.c_str()); system(share->postCmd.c_str());
} }
else
{
recLog("motion detected by detect loop, skpping the post command.", share);
} }
recLog("recLoop() -- finished", share);
}
share->logRun = false;
} }
int main(int argc, char** argv) int main(int argc, char** argv)
@ -122,9 +153,14 @@ int main(int argc, char** argv)
sharedRes.recLoopWait = false; sharedRes.recLoopWait = false;
sharedRes.skipCmd = false; sharedRes.skipCmd = false;
sharedRes.init = true; sharedRes.init = true;
sharedRes.logRun = true;
thread th3(logLoop, &sharedRes);
recLoop(&sharedRes); recLoop(&sharedRes);
th3.join();
return sharedRes.retCode; return sharedRes.retCode;
} }

View File

@ -12,14 +12,16 @@
#include "mo_detect.h" #include "mo_detect.h"
bool imgDiff(const Mat &prev, const Mat &next, shared_t *share) bool imgDiff(const Mat &prev, const Mat &next, int &diffScore, shared_t *share)
{ {
Mat diff; Mat diff;
absdiff(prev, next, diff); absdiff(prev, next, diff);
threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY); threshold(diff, diff, share->pixThresh, 255, THRESH_BINARY);
return countNonZero(diff) >= share->imgThresh; diffScore = countNonZero(diff);
return diffScore >= share->imgThresh;
} }
Mat frameFF(VideoCapture *cap, int gap) Mat frameFF(VideoCapture *cap, int gap)
@ -57,6 +59,10 @@ void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share)
auto vidOut = genDstFile(outDir, timStr.c_str(), "." + share->vidExt); auto vidOut = genDstFile(outDir, timStr.c_str(), "." + share->vidExt);
auto imgOut = genDstFile(outDir, timStr.c_str(), ".jpg"); auto imgOut = genDstFile(outDir, timStr.c_str(), ".jpg");
detLog("write_out_vid: " + vidOut, share);
detLog("write_out_img: " + imgOut, share);
detLog("remove_file: " + buffFile, share);
enforceMaxClips(outDir, share); enforceMaxClips(outDir, share);
copy_file(buffFile.c_str(), vidOut.c_str()); copy_file(buffFile.c_str(), vidOut.c_str());
@ -65,8 +71,8 @@ void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share)
imwrite(imgOut.c_str(), vidThumb); imwrite(imgOut.c_str(), vidThumb);
genHTMLvid(vidOut, share); genHTMLvid(vidOut, share);
genHTMLul(outDir, share->camName + ": " + dayStr); genHTMLul(outDir, share->camName + ": " + dayStr, share);
genHTMLul(share->outDir, share->camName); genHTMLul(share->outDir, share->camName, share);
} }
bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share) bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
@ -79,6 +85,9 @@ bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
{ {
Mat prev; Mat prev;
Mat next; Mat next;
int diff = 0;
int maxDiff = 0;
int frameGaps = 0;
do do
{ {
@ -87,19 +96,29 @@ bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
next = frameFF(&capture, share->frameGap); next = frameFF(&capture, share->frameGap);
if (imgDiff(prev, next, share)) if (imgDiff(prev, next, diff, share))
{ {
resize(next, vidThumb, Size(360, 202), INTER_LINEAR); resize(next, vidThumb, Size(1280, 720), INTER_LINEAR);
if (diff > maxDiff)
{
maxDiff = diff;
}
mod = true; break; mod = true; break;
} }
frameGaps++;
} }
while (!prev.empty() && !next.empty()); while (!prev.empty() && !next.empty());
detLog("scanned_buff_file = " + buffFile + " max_score = " + to_string(maxDiff) + " frame_gaps = " + to_string(frameGaps), share);
} }
else else
{ {
cerr << "err: Could not open buff file: " << buffFile << " for reading. check formatting/permissions." << endl; detLog("failed to open buff file: " + buffFile + " for reading.", share);
cerr << " Also check if opencv was compiled with FFMPEG encoding enabled." << endl; detLog("check formatting/permissions.", share);
detLog("also check if opencv was compiled with FFMPEG encoding enabled.", share);
} }
return mod; return mod;

View File

@ -15,8 +15,9 @@
#include "common.h" #include "common.h"
#include "web.h" #include "web.h"
#include "logger.h"
bool imgDiff(const Mat &prev, const Mat &next, shared_t *share); bool imgDiff(const Mat &prev, const Mat &next, int &diffScore, shared_t *share);
bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share); bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share);
void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share); void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share);
Mat frameFF(VideoCapture *cap, int gap); Mat frameFF(VideoCapture *cap, int gap);

View File

@ -12,54 +12,64 @@
#include "web.h" #include "web.h"
void genHTMLul(const string &outputDir, const string &title) void genHTMLul(const string &outputDir, const string &title, shared_t *share)
{ {
DIR *dir; DIR *dir;
struct dirent *ent; struct dirent *ent;
vector<string> regNames; vector<string> logNames;
vector<string> dirNames; vector<string> regNames = lsFilesInDir(outputDir);
vector<string> dirNames = lsDirsInDir(outputDir);
string htmlText = "<!DOCTYPE html>\n"; string htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n"; htmlText += "<html>\n";
htmlText += "<head>\n";
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
htmlText += "</head>\n";
htmlText += "<body>\n"; htmlText += "<body>\n";
htmlText += "<h2>" + title + "</h2>\n"; htmlText += "<h3>" + title + "</h3>\n";
if (!dirNames.empty())
{
htmlText += "<ul>\n"; htmlText += "<ul>\n";
if ((dir = opendir(outputDir.c_str())) != NULL) for (auto &&dirName : dirNames)
{
while ((ent = readdir(dir)) != NULL)
{
if (ent->d_type & DT_REG)
{
regNames.push_back(string(ent->d_name));
}
else if (ent->d_type & DT_DIR)
{
dirNames.push_back(string(ent->d_name));
}
}
closedir(dir);
}
sort(regNames.begin(), regNames.end());
sort(dirNames.begin(), dirNames.end());
for (auto dirName : dirNames)
{ {
htmlText += " <li><a href='" + dirName + "'>" + dirName + "</a></li>\n"; htmlText += " <li><a href='" + dirName + "'>" + dirName + "</a></li>\n";
} }
for (auto regName : regNames) htmlText += "</ul>\n";
}
for (auto &&regName : regNames)
{ {
if (regName.ends_with(".html") && !regName.ends_with("index.html")) if (regName.ends_with("_log.html"))
{ {
htmlText += " <li><a href='" + regName + "'> <img src='" + replaceAll(regName, ".html", ".jpg") + "' </a></li>\n"; logNames.push_back(regName);
}
else if (regName.ends_with(".html") && !regName.ends_with("index.html"))
{
// regName.substr(0, regName.size() - 5) removes .html
auto name = regName.substr(0, regName.size() - 5);
htmlText += "<a href='" + regName + "'><img src='" + name + ".jpg" + "' style='width:25%;height:25%;'</a>\n";
} }
} }
if (!logNames.empty())
{
htmlText += "<h4>Logs</h4>\n";
htmlText += "<ul>\n";
for (auto &&name : logNames)
{
// name.substr(0, name.size() - 9) removes _log.html
htmlText += " <li><a href='" + name + "'>" + name.substr(0, name.size() - 9) + "</a></li>\n";
}
htmlText += "</ul>\n"; htmlText += "</ul>\n";
}
htmlText += "</body>\n"; htmlText += "</body>\n";
htmlText += "</html>"; htmlText += "</html>";
@ -79,8 +89,11 @@ void genHTMLvid(const string &outputVid, shared_t *share)
filename = replaceAll(filename, share->vidExt, "html"); filename = replaceAll(filename, share->vidExt, "html");
htmlText += "<html>\n"; htmlText += "<html>\n";
htmlText += "<head>\n";
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
htmlText += "</head>\n";
htmlText += "<body>\n"; htmlText += "<body>\n";
htmlText += "<video width='420' height='320' controls autoplay>\n"; htmlText += "<video width=100% height=100% controls autoplay>\n";
htmlText += " <source src='" + filename + "' type='video/" + share->vidExt + "'>\n"; htmlText += " <source src='" + filename + "' type='video/" + share->vidExt + "'>\n";
htmlText += "</video>\n"; htmlText += "</video>\n";
htmlText += "</body>\n"; htmlText += "</body>\n";
@ -92,3 +105,19 @@ void genHTMLvid(const string &outputVid, shared_t *share)
file.close(); file.close();
} }
void genCSS(shared_t *share)
{
string cssText = "body {\n";
cssText += " background-color: " + share->webBg + ";\n";
cssText += " color: " + share->webTxt + ";\n";
cssText += " font-family: " + share->webFont + ";\n";
cssText += "}";
ofstream file(string(cleanDir(share->webRoot) + "/theme.css").c_str());
file << cssText << endl;
file.close();
}

View File

@ -15,7 +15,8 @@
#include "common.h" #include "common.h"
void genHTMLul(const string &outputDir, const string &title); void genHTMLul(const string &outputDir, const string &title, shared_t *share);
void genHTMLvid(const string &outputVid, shared_t *share); void genHTMLvid(const string &outputVid, shared_t *share);
void genCSS(shared_t *share);
#endif // WEB_H #endif // WEB_H