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, <m);
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, <m);
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, <Start);
354 0 : localtime_r(&stop_time, <Stop);
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 : }
|