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,28 +36,36 @@ 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
{ {
string recordUrl; vector<string> recLogLines;
string outDir; vector<string> detLogLines;
string postCmd; string recordUrl;
string conf; string outDir;
string buffDir; string postCmd;
string vidExt; string conf;
string camName; string buffDir;
bool init; string vidExt;
bool recLoopWait; string camName;
bool skipCmd; string webBg;
int frameGap; string webTxt;
int pixThresh; string webFont;
int imgThresh; string webRoot;
int secs; bool init;
int maxDays; bool recLoopWait;
int maxClips; bool logRun;
int retCode; bool skipCmd;
int frameGap;
int pixThresh;
int imgThresh;
int secs;
int maxDays;
int maxClips;
int maxLogLines;
int retCode;
}; };
string genDstFile(const string &dirOut, const char *fmt, const string &ext); string genDstFile(const string &dirOut, const char *fmt, const string &ext);

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";
htmlText += "<ul>\n";
if ((dir = opendir(outputDir.c_str())) != NULL) if (!dirNames.empty())
{ {
while ((ent = readdir(dir)) != NULL) htmlText += "<ul>\n";
for (auto &&dirName : dirNames)
{ {
if (ent->d_type & DT_REG) htmlText += " <li><a href='" + dirName + "'>" + dirName + "</a></li>\n";
{
regNames.push_back(string(ent->d_name));
}
else if (ent->d_type & DT_DIR)
{
dirNames.push_back(string(ent->d_name));
}
} }
closedir(dir); htmlText += "</ul>\n";
} }
sort(regNames.begin(), regNames.end()); for (auto &&regName : regNames)
sort(dirNames.begin(), dirNames.end());
for (auto dirName : dirNames)
{ {
htmlText += " <li><a href='" + dirName + "'>" + dirName + "</a></li>\n"; if (regName.ends_with("_log.html"))
}
for (auto regName : regNames)
{
if (regName.ends_with(".html") && !regName.ends_with("index.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";
} }
} }
htmlText += "</ul>\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 += "</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