LCOV - code coverage report
Current view: top level - src - history_image.cxx (source / functions) Coverage Total Hit
Test: coverage.info Lines: 0.0 % 222 0
Test Date: 2025-11-11 10:26:08 Functions: 0.0 % 8 0

            Line data    Source code
       1              : /********************************************************************\
       2              : 
       3              :   Name:         history_image.cxx
       4              :   Created by:   Stefan Ritt
       5              : 
       6              :   Contents:     Logger module saving images from webcams through
       7              :                 network HTTP link into subdirectories. These images
       8              :                 can then be retrived in the history page.
       9              : 
      10              : \********************************************************************/
      11              : 
      12              : #include <string>
      13              : #include <exception>
      14              : #include <sstream>
      15              : #include <iomanip>
      16              : #include <thread>
      17              : #include <algorithm>
      18              : 
      19              : #include "midas.h"
      20              : #include "msystem.h"
      21              : #include "odbxx.h"
      22              : 
      23              : 
      24              : #ifdef HAVE_OPENCV
      25              : #include <opencv2/opencv.hpp>
      26              : #endif
      27              : 
      28            0 : std::string history_dir() {
      29            0 :    static std::string dir;
      30              : 
      31            0 :    if (dir.empty()) {
      32              :       midas::odb o = {
      33              :          {"History dir", ""}
      34            0 :       };
      35            0 :       o.connect("/Logger/History/IMAGE");
      36              : 
      37            0 :       if (o["History dir"] != std::string(""))
      38            0 :          dir = o["History dir"];
      39              :       else {
      40            0 :          midas::odb l("/Logger");
      41            0 :          if (l.is_subkey("History dir")) {
      42            0 :             dir = l["History dir"];
      43            0 :             if (dir == "")
      44            0 :                dir = l["Data dir"];
      45              :          } else
      46            0 :             dir = l["Data dir"];
      47            0 :       }
      48              : 
      49            0 :       if (dir.empty())
      50            0 :          dir = cm_get_path();
      51              : 
      52            0 :       if (dir.back() != '/')
      53            0 :          dir += "/";
      54            0 :    }
      55            0 :    return dir;
      56            0 : }
      57              : 
      58              : #ifdef HAVE_CURL
      59              : #include <curl/curl.h>
      60              : 
      61            0 : static size_t write_data(void *ptr, size_t size, size_t nmemb, void *stream)
      62              : {
      63            0 :    size_t written = fwrite(ptr, size, nmemb, (FILE *)stream);
      64            0 :    return written;
      65              : }
      66              : 
      67              : static std::vector<std::thread> _image_threads;
      68              : static bool stop_all_threads = false;
      69              : 
      70            0 : int mkpath(std::string dir, mode_t mode)
      71              : {
      72            0 :    if (dir.back() == DIR_SEPARATOR)
      73            0 :       dir.pop_back();
      74              : 
      75              :    struct stat sb;
      76            0 :    if (!stat(dir.c_str(), &sb))
      77            0 :       return 0;
      78              : 
      79            0 :    std::string p = dir;
      80            0 :    if (p.find_last_of(DIR_SEPARATOR) != std::string::npos) {
      81            0 :       p = p.substr(0, p.find_last_of(DIR_SEPARATOR));
      82            0 :       mkpath(p.c_str(), mode);
      83              :    }
      84              : 
      85            0 :    return mkdir(dir.c_str(), mode);
      86            0 : }
      87              : 
      88              : 
      89            0 : void image_thread(std::string name) {
      90            0 :    DWORD last_check_delete = 0;
      91            0 :    midas::odb o(("/History/Images/"+name).c_str());
      92              : #ifdef HAVE_OPENCV
      93              :    cv::VideoCapture* cap = new cv::VideoCapture();
      94              :    //Track the number of times we have failed to connect to prevent error spam
      95              :    int failed_connections = 0;
      96              :    cv::Mat frame;
      97              : #endif
      98              : 
      99              :    do {
     100            0 :       std::this_thread::sleep_for(std::chrono::milliseconds(20));
     101              : 
     102            0 :       if (stop_all_threads)
     103            0 :          break;
     104              : 
     105              :       // check for old files
     106            0 :       if (ss_time() > last_check_delete + 60 && o["Storage hours"] > 0) {
     107              : 
     108            0 :          std::string path = history_dir();
     109            0 :          path += name;
     110              : 
     111            0 :          STRING_LIST flist;
     112              :          //printf("scan [%s]\n", path.c_str());
     113            0 :          ss_file_find(path.c_str(), "??????_??????.*", &flist);
     114              : 
     115            0 :          for (unsigned i=0 ; i<flist.size() ; i++) {
     116            0 :             std::string filename = flist[i];
     117            0 :             struct tm ti{};
     118            0 :             sscanf(filename.c_str(), "%2d%2d%2d_%2d%2d%2d", &ti.tm_year, &ti.tm_mon,
     119              :                    &ti.tm_mday, &ti.tm_hour, &ti.tm_min, &ti.tm_sec);
     120            0 :             ti.tm_year += 100;
     121            0 :             ti.tm_mon -= 1;
     122            0 :             ti.tm_isdst = -1;
     123            0 :             time_t ft = ss_mktime(&ti);
     124            0 :             double age = (ss_time() - ft)/3600.0;
     125            0 :             if (age >= o["Storage hours"]) {
     126            0 :                std::string pathname = (path+"/"+filename);
     127            0 :                int error = remove(pathname.c_str());
     128              :                // suppress cases with ENOENT happening on systems with very many files
     129            0 :                if (error && errno != ENOENT)
     130            0 :                   cm_msg(MERROR, "image_thread", "Cannot remove file \"%s\", remove() errno %d (%s)", pathname.c_str(), errno, strerror(errno));
     131            0 :             }
     132              : 
     133            0 :          }
     134              : 
     135            0 :          last_check_delete = ss_time();
     136            0 :       }
     137              : 
     138            0 :       if (!o["Enabled"])
     139            0 :          continue;
     140              : 
     141            0 :       if (ss_time() >= o["Last fetch"] + o["Period"]) {
     142            0 :          o["Last fetch"] = ss_time();
     143            0 :          std::string url = o["URL"];
     144            0 :          bool is_rtsp = false;
     145              :          // Check if the URL is rtsp protocol 
     146            0 :          if (url.compare(0,7,"rtsp://") == 0) {
     147            0 :             is_rtsp = true;
     148              : #ifdef HAVE_OPENCV
     149              :             if (!cap->isOpened() ) {
     150              :                cm_msg(MINFO,"image_history_rtsp","Opening camera %s",name.c_str());
     151              :                if (!cap->open(url.c_str())) {
     152              :                   std::cout << "Unable to open video capture\n";
     153              :                   failed_connections++;
     154              :                   std::string error = "Cannot connect to camera \"" + name + "\" at " + url + ", please check camera power and URL";
     155              :                   cm_msg(MERROR,"image_history_rtsp","%s",error.c_str());
     156              :                   if (failed_connections > 10)
     157              :                      cm_msg(MERROR,"image_history_rtsp","More than 10 failed connections, I will stop reporting this error");
     158              :                   continue;
     159              :                } else {
     160              :                   //Connection success! Reset failure counter
     161              :                   failed_connections = 0;
     162              :                }
     163              :             }
     164              : 
     165              : #else // We have not built with OpenCV and tried to connect to a rtsp camera
     166            0 :             std::string error = "Cannot connect to camera \"" + name + "\". mlogger not build with rtsp support (OpenCV)";
     167            0 :             cm_msg(MERROR,"image_history_rtsp","%s",error.c_str());
     168              : #endif
     169            0 :          }
     170            0 :          std::string filename = history_dir() + name;
     171            0 :          std::string dotname = filename;
     172            0 :          int status = mkpath(filename, 0755);
     173            0 :          if (status)
     174            0 :             cm_msg(MERROR, "image_thread", "Cannot create directory \"%s\": mkpath() errno %d (%s)", filename.c_str(), errno, strerror(errno));
     175              : 
     176            0 :          ss_tzset(); // required by localtime_r()
     177              : 
     178            0 :          time_t now = time(nullptr);
     179              :          struct tm ltm;
     180            0 :          localtime_r(&now, &ltm);
     181            0 :          std::stringstream s;
     182              :          s <<
     183            0 :            std::setfill('0') << std::setw(2) << ltm.tm_year - 100 <<
     184            0 :            std::setfill('0') << std::setw(2) << ltm.tm_mon + 1 <<
     185            0 :            std::setfill('0') << std::setw(2) << ltm.tm_mday <<
     186              :            "_" <<
     187            0 :            std::setfill('0') << std::setw(2) << ltm.tm_hour <<
     188            0 :            std::setfill('0') << std::setw(2) << ltm.tm_min <<
     189            0 :            std::setfill('0') << std::setw(2) << ltm.tm_sec;
     190            0 :          filename += "/" + s.str();
     191            0 :          dotname += "/." + s.str();
     192              : 
     193            0 :          if (o["Extension"] == std::string("")) {
     194            0 :             filename += url.substr(url.find_last_of('.'));
     195            0 :             dotname += url.substr(url.find_last_of('.'));
     196              :          } else {
     197            0 :             filename += o["Extension"];
     198            0 :             dotname +=  o["Extension"];
     199              :          }
     200            0 :          if (is_rtsp)
     201              :          {
     202              : #ifdef HAVE_OPENCV
     203              :             // If the system has OpenCV but not the full gstreamer install, or the system is missing video codecs, the mlogger can hang here. 
     204              :             bool OK = cap->grab();
     205              :             if (OK == false) {
     206              :                std::string error = "Cannot grab from camera \"" + name + "\" at " + url + ", please check camera power and URL";
     207              :                cm_msg(MERROR, "log_image_history", "%s", error.c_str());
     208              :                cap->release();
     209              :                delete cap;
     210              :                cap = new cv::VideoCapture();
     211              :                continue;
     212              :             }
     213              : 
     214              :             *cap >> frame;
     215              :             if (frame.empty()) {
     216              :                std::string error = "Recieved empty frame from camera \"" + name;
     217              :                cm_msg(MERROR, "log_image_history", "%s", error.c_str());
     218              :                cap->release();
     219              :                delete cap;
     220              :                cap = new cv::VideoCapture();
     221              :                continue;
     222              :                // End of video stream
     223              :             }
     224              :             cv::imwrite(filename, frame);
     225              : #endif
     226              :          } else {
     227            0 :             CURL *conn = curl_easy_init();
     228            0 :             curl_easy_setopt(conn, CURLOPT_URL, url.c_str());
     229            0 :             curl_easy_setopt(conn, CURLOPT_WRITEFUNCTION, write_data);
     230            0 :             curl_easy_setopt(conn, CURLOPT_VERBOSE, 0L);
     231            0 :             auto f = fopen(dotname.c_str(), "wb");
     232            0 :             if (f) {
     233            0 :                curl_easy_setopt(conn, CURLOPT_WRITEDATA, f);
     234            0 :                curl_easy_setopt(conn, CURLOPT_TIMEOUT, 60L);
     235            0 :                int status = curl_easy_perform(conn);
     236            0 :                fclose(f);
     237            0 :                std::string error;
     238            0 :                if (status == CURLE_COULDNT_CONNECT) {
     239            0 :                   error = "Cannot connect to camera \"" + name + "\" at " + url + ", please check camera power and URL";
     240            0 :                } else if (status != CURLE_OK) {
     241            0 :                   error = "Error fetching image from camera \"" + name + "\", curl status " + std::to_string(status);
     242              :                } else {
     243            0 :                   long http_code = 0;
     244            0 :                   curl_easy_getinfo(conn, CURLINFO_RESPONSE_CODE, &http_code);
     245            0 :                   if (http_code != 200)
     246            0 :                      error = "Error fetching image from camera \"" + name + "\", http error status " +
     247            0 :                              std::to_string(http_code);
     248              :                }
     249            0 :                if (!error.empty()) {
     250            0 :                   if (ss_time() > o["Last error"] + o["Error interval (s)"]) {
     251            0 :                      cm_msg(MERROR, "log_image_history", "%s", error.c_str());
     252            0 :                      o["Last error"] = ss_time();
     253              :                   }
     254            0 :                   remove(dotname.c_str());
     255              :                }
     256              :    
     257              :                // rename dotfile to filename to make it visible
     258            0 :                rename(dotname.c_str(), filename.c_str());
     259            0 :             }
     260            0 :             curl_easy_cleanup(conn);
     261              :          }
     262            0 :       }
     263              : 
     264            0 :    } while (!stop_all_threads);
     265            0 : }
     266              : 
     267            0 : void stop_image_history() {
     268            0 :    stop_all_threads = true;
     269            0 :    for (auto &t : _image_threads)
     270            0 :       t.join();
     271            0 :    curl_global_cleanup();
     272            0 : }
     273              : 
     274            0 : int get_number_image_history_threads() {
     275            0 :    return _image_threads.size();
     276              : }
     277              : 
     278            0 : void start_image_history() {
     279            0 :    curl_global_init(CURL_GLOBAL_DEFAULT);
     280              : 
     281              :    // create default "Demo" image if ODB tree does not exist
     282            0 :    if (!midas::odb::exists("/History/Images"))
     283            0 :       midas::odb::create("/History/Images/Demo", TID_KEY);
     284              : 
     285            0 :    static midas::odb h;
     286            0 :    h.connect("/History/Images");
     287              : 
     288              :    // loop over all cameras
     289            0 :    for (auto &ic: h) {
     290              : 
     291              :       // write default values if not present (ODB has precedence)
     292              :       midas::odb c = {
     293              :               {"Name",               "Demo Camera"},
     294              :               {"Enabled",            false},
     295              :               {"URL",                "https://localhost:8000/image.jpg"},
     296              :               {"Extension",          ".jpg"},
     297            0 :               {"Period",             60},
     298            0 :               {"Last fetch",         0},
     299            0 :               {"Storage hours",      0},
     300            0 :               {"Error interval (s)", 60},
     301            0 :               {"Last error",         0},
     302              :               {"Timescale",          "8h"}
     303            0 :       };
     304            0 :       c.connect(ic.get_odb().get_full_path());
     305              : 
     306            0 :       std::string name = ic.get_odb().get_name();
     307            0 :       if (name != "Demo" || c["Enabled"] == true)
     308            0 :          _image_threads.push_back(std::thread(image_thread, name));
     309            0 :    }
     310            0 : }
     311              : 
     312              : #else // HAVE_CURL
     313              : 
     314              : // no history image logging wihtout CURL library
     315              : void start_image_history() {}
     316              : void stop_image_history() {}
     317              : int get_number_image_history_threads() { return 0; }
     318              : 
     319              : #endif
     320              : 
     321              : //std::chrono::time_point<std::chrono::high_resolution_clock> usStart()
     322              : //{
     323              : //   return std::chrono::high_resolution_clock::now();
     324              : //}
     325              : //
     326              : //unsigned int usSince(std::chrono::time_point<std::chrono::high_resolution_clock> start) {
     327              : //   auto elapsed = std::chrono::high_resolution_clock::now() - start;
     328              : //   return std::chrono::duration_cast<std::chrono::microseconds>(elapsed).count();
     329              : //}
     330              : 
     331              : // retrieve image history
     332            0 : int hs_image_retrieve(std::string image_name, time_t start_time, time_t stop_time,
     333              :                       std::vector<time_t> &vtime, std::vector<std::string> &vfilename)
     334              : {
     335              :    // auto start = usStart();
     336            0 :    std::string path = history_dir() + image_name;
     337              : 
     338            0 :    std::string mask;
     339            0 :    if (start_time == stop_time) {
     340            0 :       ss_tzset(); // required by localtime_r()
     341              :       struct tm ltm;
     342            0 :       localtime_r(&start_time, &ltm);
     343            0 :       std::stringstream s;
     344              :       s <<
     345            0 :         std::setfill('0') << std::setw(2) << ltm.tm_year - 100 <<
     346            0 :         std::setfill('0') << std::setw(2) << ltm.tm_mon + 1 <<
     347            0 :         std::setfill('0') << std::setw(2) << ltm.tm_mday <<
     348            0 :         "_" << "??????.*";
     349            0 :       mask = s.str();
     350            0 :    } else {
     351            0 :       ss_tzset(); // required by localtime_r()
     352              :       struct tm ltStart, ltStop;
     353            0 :       localtime_r(&start_time, &ltStart);
     354            0 :       localtime_r(&stop_time, &ltStop);
     355            0 :       std::stringstream sStart, sStop;
     356            0 :       std::string mStart, mStop;
     357            0 :       mask = "??????_??????.*";
     358              : 
     359              :       sStart <<
     360            0 :         std::setfill('0') << std::setw(2) << ltStart.tm_year - 100 <<
     361            0 :         std::setfill('0') << std::setw(2) << ltStart.tm_mon + 1 <<
     362            0 :         std::setfill('0') << std::setw(2) << ltStart.tm_mday <<
     363              :         "_" <<
     364            0 :         std::setfill('0') << std::setw(2) << ltStart.tm_hour <<
     365            0 :         std::setfill('0') << std::setw(2) << ltStart.tm_min <<
     366            0 :         std::setfill('0') << std::setw(2) << ltStart.tm_sec;
     367            0 :       mStart = sStart.str();
     368              :       sStop <<
     369            0 :         std::setfill('0') << std::setw(2) << ltStop.tm_year - 100 <<
     370            0 :         std::setfill('0') << std::setw(2) << ltStop.tm_mon + 1 <<
     371            0 :         std::setfill('0') << std::setw(2) << ltStop.tm_mday <<
     372              :         "_" <<
     373            0 :         std::setfill('0') << std::setw(2) << ltStop.tm_hour <<
     374            0 :         std::setfill('0') << std::setw(2) << ltStop.tm_min <<
     375            0 :         std::setfill('0') << std::setw(2) << ltStop.tm_sec;
     376            0 :       mStop = sStop.str();
     377            0 :       for (int i=0 ; i<13 ; i++) {
     378            0 :          if (mStart[i] == mStop[i])
     379            0 :             mask[i] = mStart[i];
     380              :          else
     381            0 :             break;
     382              :       }
     383            0 :    }
     384              : 
     385            0 :    STRING_LIST vfn;
     386              : 
     387            0 :    ss_file_find(path.c_str(), mask.c_str(), &vfn);
     388              : 
     389            0 :    if (vfn.size() == 0)
     390            0 :       ss_file_find(path.c_str(), "??????_??????.*", &vfn);
     391              : 
     392            0 :    std::sort(vfn.begin(), vfn.end());
     393              : 
     394            0 :    time_t minDiff = 1E7;
     395            0 :    time_t minTime{};
     396            0 :    int minIndex{};
     397              : 
     398            0 :    for (unsigned i=0 ; i<vfn.size() ; i++) {
     399            0 :       struct tm ti{};
     400            0 :       sscanf(vfn[i].c_str(), "%2d%2d%2d_%2d%2d%2d", &ti.tm_year, &ti.tm_mon,
     401              :              &ti.tm_mday, &ti.tm_hour, &ti.tm_min, &ti.tm_sec);
     402            0 :       ti.tm_year += 100;
     403            0 :       ti.tm_mon -= 1;
     404            0 :       ti.tm_isdst = -1;
     405            0 :       time_t ft = ss_mktime(&ti);
     406              :       time_t now;
     407            0 :       time(&now);
     408              : 
     409            0 :       if (abs(ft - start_time) < minDiff) {
     410            0 :          minDiff = abs(ft - start_time);
     411            0 :          minTime = ft;
     412            0 :          minIndex = i;
     413              :       }
     414              : 
     415            0 :       if (start_time != stop_time && ft >= start_time && ft <= stop_time) {
     416            0 :          vtime.push_back(ft);
     417            0 :          vfilename.push_back(vfn[i]);
     418              :       }
     419              :    }
     420              : 
     421              :    // start == stop means return single image closest to them
     422            0 :    if (start_time == stop_time && vfn.size() > 0) {
     423            0 :       vtime.push_back(minTime);
     424            0 :       vfilename.push_back(vfn[minIndex]);
     425              :    }
     426              : 
     427              :    //std::cout << "mask = " << mask << ", n = " << n << ", t = " << usSince(start)/1000.0 << " ms" << std::endl;
     428              : 
     429            0 :    return HS_SUCCESS;
     430            0 : }
        

Generated by: LCOV version 2.0-1