diff --git a/CMakeLists.txt b/CMakeLists.txt index fa46d25..04cc324 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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} ) diff --git a/src/common.cpp b/src/common.cpp index 5f30676..f590acd 100644 --- a/src/common.cpp +++ b/src/common.cpp @@ -114,7 +114,12 @@ vector 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(); diff --git a/src/common.h b/src/common.h index 675779c..729dab6 100644 --- a/src/common.h +++ b/src/common.h @@ -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 recLogLines; + vector 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); diff --git a/src/logger.cpp b/src/logger.cpp new file mode 100644 index 0000000..152c898 --- /dev/null +++ b/src/logger.cpp @@ -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 + "
\n"); +} + +void detLog(const string &line, shared_t *share) +{ + share->detLogLines.push_back(genTimeStr("[%Y-%m-%d-%H-%M-%S] ") + line + "
\n"); +} + +void enforceMaxLogLines(vector &log, shared_t *share) +{ + while (log.size() > share->maxLogLines) + { + log.erase(log.begin()); + } +} + +string combine(const vector &log) +{ + string ret; + + for (auto &&line : log) ret += line; + + return ret; +} + +void logLoop(shared_t *share) +{ + string htmlText = "\n"; + + htmlText += "\n"; + htmlText += "\n"; + htmlText += "\n"; + htmlText += ""; + htmlText += "\n"; + htmlText += "\n"; + htmlText += "

\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) << "

\n\n"; + detFile << htmlText << combine(share->detLogLines) << "

\n\n"; + + recFile.close(); + detFile.close(); + } +} diff --git a/src/logger.h b/src/logger.h new file mode 100644 index 0000000..d4be579 --- /dev/null +++ b/src/logger.h @@ -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 &log, shared_t *share); +string combine(const vector &log); + +#endif // lOGGER_H diff --git a/src/main.cpp b/src/main.cpp index 2870b6c..f5965a8 100755 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 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; } diff --git a/src/mo_detect.cpp b/src/mo_detect.cpp index 9d23270..15a5434 100644 --- a/src/mo_detect.cpp +++ b/src/mo_detect.cpp @@ -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; diff --git a/src/mo_detect.h b/src/mo_detect.h index 8799101..459f26c 100644 --- a/src/mo_detect.h +++ b/src/mo_detect.h @@ -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); diff --git a/src/web.cpp b/src/web.cpp index 7208781..f6c165e 100644 --- a/src/web.cpp +++ b/src/web.cpp @@ -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 regNames; - vector dirNames; + vector logNames; + vector regNames = lsFilesInDir(outputDir); + vector dirNames = lsDirsInDir(outputDir); string htmlText = "\n"; htmlText += "\n"; + htmlText += "\n"; + htmlText += "\n"; + htmlText += "\n"; htmlText += "\n"; - htmlText += "

" + title + "

\n"; - htmlText += "\n"; + if (!logNames.empty()) + { + htmlText += "

Logs

\n"; + htmlText += "
\n"; + } + htmlText += "\n"; htmlText += ""; @@ -79,8 +89,11 @@ void genHTMLvid(const string &outputVid, shared_t *share) filename = replaceAll(filename, share->vidExt, "html"); htmlText += "\n"; + htmlText += "\n"; + htmlText += "\n"; + htmlText += "\n"; htmlText += "\n"; - htmlText += "