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 )
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++20 -pthread")
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} )

View File

@ -114,7 +114,12 @@ vector<string> lsDirsInDir(const string &path)
{
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->buffDir.clear();
share->retCode = 0;
share->frameGap = 10;
share->pixThresh = 30;
share->imgThresh = 512;
share->secs = 60;
share->maxDays = 15;
share->maxClips = 30;
share->maxLogLines = 1000;
share->camName = path(share->conf.c_str()).filename();
share->outDir = "/var/www/hmtl";
share->webRoot = "/var/www/html";
share->vidExt = "mp4";
share->recLoopWait = false;
share->skipCmd = false;
share->webBg = "#485564";
share->webTxt = "#dee5ee";
share->webFont = "courier";
do
{
@ -230,7 +240,10 @@ bool rdConf(shared_t *share)
{
rdLine("cam_name = ", line, &share->camName);
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("duration = ", line, &share->secs);
rdLine("buff_dir = ", line, &share->buffDir);
@ -239,12 +252,16 @@ bool rdConf(shared_t *share)
rdLine("img_thresh = ", line, &share->imgThresh);
rdLine("max_days = ", line, &share->maxDays);
rdLine("max_clips = ", line, &share->maxClips);
rdLine("max_log_lines = ", line, &share->maxLogLines);
rdLine("vid_container = ", line, &share->vidExt);
}
} 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)
{
@ -252,11 +269,6 @@ bool rdConf(shared_t *share)
share->init = false;
}
createDirTree(cleanDir(share->buffDir));
createDirTree(share->outDir);
share->retCode = 0;
}
varFile.close();

View File

@ -36,28 +36,36 @@ using namespace cv;
using namespace std;
using namespace std::filesystem;
#define APP_VER "1.5.t2"
#define APP_VER "1.5.t3"
#define APP_NAME "Motion Watch"
struct shared_t
{
string recordUrl;
string outDir;
string postCmd;
string conf;
string buffDir;
string vidExt;
string camName;
bool init;
bool recLoopWait;
bool skipCmd;
int frameGap;
int pixThresh;
int imgThresh;
int secs;
int maxDays;
int maxClips;
int retCode;
vector<string> recLogLines;
vector<string> detLogLines;
string recordUrl;
string outDir;
string postCmd;
string conf;
string buffDir;
string vidExt;
string camName;
string webBg;
string webTxt;
string webFont;
string webRoot;
bool init;
bool recLoopWait;
bool logRun;
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);

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.
#include "mo_detect.h"
#include "logger.h"
void detectLoop(shared_t *share)
{
detLog("detectLoop() -- start", share);
vector<string> bufFiles;
do
@ -35,10 +38,12 @@ void detectLoop(shared_t *share)
{
share->skipCmd = true;
detLog("motion detected in file: " + fullPath, share);
wrOut(fullPath, thumbNail, share);
}
else
{
detLog("no motion detected in file: " + fullPath + " removing it.", share);
remove(fullPath.c_str());
}
}
@ -48,23 +53,29 @@ void detectLoop(shared_t *share)
}
}
while (!bufFiles.empty());
detLog("detectLoop() -- finished", share);
}
void recLoop(shared_t *share)
{
while (rdConf(share))
{
recLog("recLoop() -- start", share);
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");
auto webRoot = path(share->outDir).parent_path().string();
genHTMLul(webRoot, string(APP_NAME) + " " + string(APP_VER));
genCSS(share);
genHTMLul(share->webRoot, string(APP_NAME) + " " + string(APP_VER), share);
system("rm /tmp/mow-lock");
}
createDirTree(share->buffDir);
else
{
recLog("/tmp/mow-lock pesent, skipping update of the webroot page.", share);
}
auto bufPath = cleanDir(share->buffDir) + "/%03d." + share->vidExt;
auto secs = to_string(share->secs);
@ -73,14 +84,24 @@ void recLoop(shared_t *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;
th2.join();
}
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.
sleep(share->secs);
@ -89,9 +110,19 @@ void recLoop(shared_t *share)
if (!share->skipCmd)
{
recLog("motion not detected by detect loop.", share);
recLog("running post command = " + share->postCmd, share);
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)
@ -122,9 +153,14 @@ int main(int argc, char** argv)
sharedRes.recLoopWait = false;
sharedRes.skipCmd = false;
sharedRes.init = true;
sharedRes.logRun = true;
thread th3(logLoop, &sharedRes);
recLoop(&sharedRes);
th3.join();
return sharedRes.retCode;
}

View File

@ -12,14 +12,16 @@
#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;
absdiff(prev, next, diff);
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)
@ -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 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);
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);
genHTMLvid(vidOut, share);
genHTMLul(outDir, share->camName + ": " + dayStr);
genHTMLul(share->outDir, share->camName);
genHTMLul(outDir, share->camName + ": " + dayStr, share);
genHTMLul(share->outDir, share->camName, 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 next;
int diff = 0;
int maxDiff = 0;
int frameGaps = 0;
do
{
@ -87,19 +96,29 @@ bool moDetect(const string &buffFile, Mat &vidThumb, shared_t *share)
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;
}
frameGaps++;
}
while (!prev.empty() && !next.empty());
detLog("scanned_buff_file = " + buffFile + " max_score = " + to_string(maxDiff) + " frame_gaps = " + to_string(frameGaps), share);
}
else
{
cerr << "err: Could not open buff file: " << buffFile << " for reading. check formatting/permissions." << endl;
cerr << " Also check if opencv was compiled with FFMPEG encoding enabled." << endl;
detLog("failed to open buff file: " + buffFile + " for reading.", share);
detLog("check formatting/permissions.", share);
detLog("also check if opencv was compiled with FFMPEG encoding enabled.", share);
}
return mod;

View File

@ -15,8 +15,9 @@
#include "common.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);
void wrOut(const string &buffFile, const Mat &vidThumb, shared_t *share);
Mat frameFF(VideoCapture *cap, int gap);

View File

@ -12,54 +12,64 @@
#include "web.h"
void genHTMLul(const string &outputDir, const string &title)
void genHTMLul(const string &outputDir, const string &title, shared_t *share)
{
DIR *dir;
struct dirent *ent;
vector<string> regNames;
vector<string> dirNames;
vector<string> logNames;
vector<string> regNames = lsFilesInDir(outputDir);
vector<string> dirNames = lsDirsInDir(outputDir);
string htmlText = "<!DOCTYPE html>\n";
htmlText += "<html>\n";
htmlText += "<head>\n";
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
htmlText += "</head>\n";
htmlText += "<body>\n";
htmlText += "<h2>" + title + "</h2>\n";
htmlText += "<ul>\n";
htmlText += "<h3>" + title + "</h3>\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)
{
regNames.push_back(string(ent->d_name));
}
else if (ent->d_type & DT_DIR)
{
dirNames.push_back(string(ent->d_name));
}
htmlText += " <li><a href='" + dirName + "'>" + dirName + "</a></li>\n";
}
closedir(dir);
htmlText += "</ul>\n";
}
sort(regNames.begin(), regNames.end());
sort(dirNames.begin(), dirNames.end());
for (auto dirName : dirNames)
for (auto &&regName : regNames)
{
htmlText += " <li><a href='" + dirName + "'>" + dirName + "</a></li>\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";
}
}
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 += "</html>";
@ -79,8 +89,11 @@ void genHTMLvid(const string &outputVid, shared_t *share)
filename = replaceAll(filename, share->vidExt, "html");
htmlText += "<html>\n";
htmlText += "<head>\n";
htmlText += "<link rel='stylesheet' href='/theme.css'>\n";
htmlText += "</head>\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 += "</video>\n";
htmlText += "</body>\n";
@ -92,3 +105,19 @@ void genHTMLvid(const string &outputVid, shared_t *share)
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"
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 genCSS(shared_t *share);
#endif // WEB_H