MRPT  1.9.9
CVelodyneScanner.cpp
Go to the documentation of this file.
1 /* +------------------------------------------------------------------------+
2  | Mobile Robot Programming Toolkit (MRPT) |
3  | http://www.mrpt.org/ |
4  | |
5  | Copyright (c) 2005-2018, Individual contributors, see AUTHORS file |
6  | See: http://www.mrpt.org/Authors - All rights reserved. |
7  | Released under BSD License. See details in http://www.mrpt.org/License |
8  +------------------------------------------------------------------------+ */
9 
10 #include "hwdrivers-precomp.h" // Precompiled headers
11 
14 #include <mrpt/comms/net_utils.h>
16 #include <mrpt/system/filesystem.h>
17 #include <mrpt/system/datetime.h> // timeDifference
19 #include <thread>
20 
21 // socket's hdrs:
22 #ifdef _WIN32
23 #define _WINSOCK_DEPRECATED_NO_WARNINGS
24 #if defined(_WIN32_WINNT) && (_WIN32_WINNT < 0x600)
25 #undef _WIN32_WINNT
26 #define _WIN32_WINNT 0x600 // Minimum: Windows Vista (required to pollfd)
27 #endif
28 
29 #include <winsock2.h>
30 using socklen_t = int;
31 
32 #if defined(_MSC_VER)
33 #pragma comment(lib, "WS2_32.LIB")
34 #endif
35 
36 #else
37 #define INVALID_SOCKET (-1)
38 #include <sys/time.h> // gettimeofday()
39 #include <sys/socket.h>
40 #include <unistd.h>
41 #include <fcntl.h>
42 #include <errno.h>
43 #include <sys/types.h>
44 #include <sys/ioctl.h>
45 #include <netdb.h>
46 #include <arpa/inet.h>
47 #include <netinet/in.h>
48 #include <netinet/tcp.h>
49 #include <poll.h>
50 #endif
51 
52 #if MRPT_HAS_LIBPCAP
53 #include <pcap.h>
54 #if !defined(PCAP_NETMASK_UNKNOWN) // for older pcap versions
55 #define PCAP_NETMASK_UNKNOWN 0xffffffff
56 #endif
57 #endif
58 
59 using namespace mrpt;
60 using namespace mrpt::hwdrivers;
61 using namespace mrpt::obs;
62 using namespace std;
63 
65 
68 
71 {
72  static CVelodyneScanner::model_properties_list_t modelProperties;
73  static bool init = false;
74  if (!init)
75  {
76  init = true;
77  modelProperties[CVelodyneScanner::VLP16].maxRange = 120.0;
78  modelProperties[CVelodyneScanner::HDL32].maxRange = 70.0;
79  modelProperties[CVelodyneScanner::HDL64].maxRange = 120.0;
80  }
81  return modelProperties;
82 }
84 {
87  std::string s;
89  lst.begin();
90  it != lst.end(); ++it)
91  s += mrpt::format(
92  "`%s`,",
94  it->first)
95  .c_str());
96  return s;
97 }
98 
100  : m_model(CVelodyneScanner::VLP16),
101  m_pos_packets_min_period(0.5),
102  m_pos_packets_timing_timeout(30.0),
103  m_device_ip(""),
104  m_return_frames(true),
105  m_pcap_verbose(true),
106  m_last_pos_packet_timestamp(INVALID_TIMESTAMP),
107  m_pcap(nullptr),
108  m_pcap_out(nullptr),
109  m_pcap_dumper(nullptr),
110  m_pcap_bpf_program(nullptr),
111  m_pcap_file_empty(true),
112  m_pcap_read_once(false),
113  m_pcap_read_fast(false),
114  m_pcap_read_full_scan_delay_ms(100),
115  m_pcap_repeat_delay(0.0),
116  m_hDataSock(INVALID_SOCKET),
117  m_hPositionSock(INVALID_SOCKET),
118  m_last_gps_rmc_age(INVALID_TIMESTAMP),
119  m_lidar_rpm(0),
120  m_lidar_return(UNCHANGED)
121 {
122  m_sensorLabel = "Velodyne";
123 
124 #if MRPT_HAS_LIBPCAP
125  m_pcap_bpf_program = new bpf_program[1];
126 #endif
127 
128 #ifdef _WIN32
129  // Init the WinSock Library:
130  WORD wVersionRequested = MAKEWORD(2, 0);
131  WSADATA wsaData;
132  if (WSAStartup(wVersionRequested, &wsaData))
133  THROW_EXCEPTION("Error calling WSAStartup");
134 #endif
135 }
136 
138 {
139  this->close();
140 #ifdef _WIN32
141  WSACleanup();
142 #endif
143 
144 #if MRPT_HAS_LIBPCAP
145  delete[] reinterpret_cast<bpf_program*>(m_pcap_bpf_program);
146  m_pcap_bpf_program = nullptr;
147 #endif
148 }
149 
151  const std::string& velodyne_xml_calib_file_path)
152 {
153  return m_velodyne_calib.loadFromXMLFile(velodyne_xml_calib_file_path);
154 }
155 
157  const mrpt::config::CConfigFileBase& cfg, const std::string& sect)
158 {
159  MRPT_START
160 
161  cfg.read_enum<CVelodyneScanner::model_t>(sect, "model", m_model);
162  MRPT_LOAD_HERE_CONFIG_VAR(device_ip, string, m_device_ip, cfg, sect);
163  MRPT_LOAD_HERE_CONFIG_VAR(pcap_input, string, m_pcap_input_file, cfg, sect);
165  pcap_output, string, m_pcap_output_file, cfg, sect);
167  pcap_read_once, bool, m_pcap_read_once, cfg, sect);
169  pcap_read_fast, bool, m_pcap_read_fast, cfg, sect);
171  pcap_read_full_scan_delay_ms, double, m_pcap_read_full_scan_delay_ms,
172  cfg, sect);
174  pcap_repeat_delay, double, m_pcap_repeat_delay, cfg, sect);
176  pos_packets_timing_timeout, double, m_pos_packets_timing_timeout, cfg,
177  sect);
179  pos_packets_min_period, double, m_pos_packets_min_period, cfg, sect);
180 
181  using mrpt::DEG2RAD;
183  cfg.read_float(sect, "pose_x", 0), cfg.read_float(sect, "pose_y", 0),
184  cfg.read_float(sect, "pose_z", 0),
185  DEG2RAD(cfg.read_float(sect, "pose_yaw", 0)),
186  DEG2RAD(cfg.read_float(sect, "pose_pitch", 0)),
187  DEG2RAD(cfg.read_float(sect, "pose_roll", 0)));
188 
189  std::string calibration_file;
190  MRPT_LOAD_CONFIG_VAR(calibration_file, string, cfg, sect);
191  if (!calibration_file.empty()) this->loadCalibrationFile(calibration_file);
192 
193  // Check validity:
195  if (lstModels.find(m_model) == lstModels.end())
196  {
198  "Unrecognized `model` parameter: `%u` . Known values are: %s",
199  static_cast<unsigned int>(m_model),
201  }
202 
203  // Optional HTTP-based settings:
204  MRPT_LOAD_HERE_CONFIG_VAR(rpm, int, m_lidar_rpm, cfg, sect);
206  cfg.read_enum<return_type_t>(sect, "return_type", m_lidar_return);
207 
208  MRPT_END
209 }
210 
214 {
215  try
216  {
217  ASSERTMSG_(m_initialized, "initialize() has not been called yet!");
218 
219  // Init with empty smart pointers:
222 
223  // Try to get data & pos packets:
226  mrpt::system::TTimeStamp data_pkt_timestamp, pos_pkt_timestamp;
227  bool rx_all_ok = this->receivePackets(
228  data_pkt_timestamp, rx_pkt, pos_pkt_timestamp, rx_pos_pkt);
229 
230  if (!rx_all_ok)
231  {
232  // PCAP EOF:
233  return false;
234  }
235 
236  if (pos_pkt_timestamp != INVALID_TIMESTAMP)
237  {
239  mrpt::make_aligned_shared<mrpt::obs::CObservationGPS>();
240  gps_obs->sensorLabel = this->m_sensorLabel + std::string("_GPS");
241  gps_obs->sensorPose = m_sensorPose;
242 
243  gps_obs->originalReceivedTimestamp = pos_pkt_timestamp;
244 
245  bool parsed_ok = CGPSInterface::parse_NMEA(
246  std::string(rx_pos_pkt.NMEA_GPRMC), *gps_obs);
247  const mrpt::obs::gnss::Message_NMEA_RMC* msg_rmc =
248  gps_obs->getMsgByClassPtr<mrpt::obs::gnss::Message_NMEA_RMC>();
249  if (!parsed_ok || !msg_rmc || msg_rmc->fields.validity_char != 'A')
250  {
251  gps_obs->has_satellite_timestamp = false;
252  gps_obs->timestamp = pos_pkt_timestamp;
253  }
254  else
255  {
256  // We have live GPS signal and a recent RMC frame:
257  m_last_gps_rmc_age = pos_pkt_timestamp;
258  m_last_gps_rmc = *msg_rmc;
259  }
260  outGPS = gps_obs; // save in output object
261  }
262 
263  if (data_pkt_timestamp != INVALID_TIMESTAMP)
264  {
265  m_state = ssWorking;
266 
267  // Break into a new observation object when the azimuth passes
268  // 360->0 deg:
269  const uint16_t rx_pkt_start_angle = rx_pkt.blocks[0].rotation;
270  // const uint16_t rx_pkt_end_angle =
271  // rx_pkt.blocks[CObservationVelodyneScan::BLOCKS_PER_PACKET-1].rotation;
272 
273  // Return the observation as done when a complete 360 deg scan is
274  // ready:
275  if (m_rx_scan && !m_rx_scan->scan_packets.empty())
276  {
277  if ((rx_pkt_start_angle <
278  m_rx_scan->scan_packets.rbegin()->blocks[0].rotation) ||
280  {
281  outScan = m_rx_scan;
282  m_rx_scan.reset();
283 
284  if (m_pcap)
285  {
286  // Keep the reader from blowing through the file.
287  if (!m_pcap_read_fast)
288  std::this_thread::sleep_for(
289  std::chrono::duration<double, std::milli>(
291  }
292  }
293  }
294 
295  // Create smart ptr to new in-progress observation:
296  if (!m_rx_scan)
297  {
300  m_rx_scan->sensorLabel =
301  this->m_sensorLabel + std::string("_SCAN");
302  m_rx_scan->sensorPose = m_sensorPose;
303  m_rx_scan->calibration =
304  m_velodyne_calib; // Embed a copy of the calibration info
305 
306  {
307  const model_properties_list_t& lstModels =
310  lstModels.find(this->m_model);
311  if (it != lstModels.end())
312  { // Model params:
313  m_rx_scan->maxRange = it->second.maxRange;
314  }
315  else // default params:
316  {
317  m_rx_scan->maxRange = 120.0;
318  }
319  }
320  }
321 
322  // For the first packet, set timestamp:
323  if (m_rx_scan->scan_packets.empty())
324  {
325  m_rx_scan->originalReceivedTimestamp = data_pkt_timestamp;
326  // Using GPS, if available:
327  if (m_last_gps_rmc.fields.validity_char == 'A' &&
329  m_last_gps_rmc_age, data_pkt_timestamp) <
331  {
332  // Each Velodyne data packet has a timestamp field,
333  // with the number of us since the top of the current HOUR:
334  // take the date and time from the GPS, then modify minutes
335  // and seconds from data pkt:
336  const mrpt::system::TTimeStamp gps_tim =
339 
340  mrpt::system::TTimeParts tim_parts;
341  mrpt::system::timestampToParts(gps_tim, tim_parts);
342  tim_parts.minute =
343  rx_pkt.gps_timestamp /*us from top of hour*/ /
344  60000000ul;
345  tim_parts.second =
346  (rx_pkt.gps_timestamp /*us from top of hour*/ %
347  60000000ul) *
348  1e-6;
349 
350  const mrpt::system::TTimeStamp data_pkt_tim =
352 
353  m_rx_scan->timestamp = data_pkt_tim;
354  m_rx_scan->has_satellite_timestamp = true;
355  }
356  else
357  {
358  m_rx_scan->has_satellite_timestamp = false;
359  m_rx_scan->timestamp = data_pkt_timestamp;
360  }
361  }
362 
363  // Accumulate pkts in the observation object:
364  m_rx_scan->scan_packets.push_back(rx_pkt);
365  }
366 
367  return true;
368  }
369  catch (exception& e)
370  {
371  cerr << "[CVelodyneScanner::getObservation] Returning false due to "
372  "exception: "
373  << endl;
374  cerr << e.what() << endl;
375  return false;
376  }
377 }
378 
380 {
382  CObservationGPS::Ptr obs_gps;
383 
384  if (getNextObservation(obs, obs_gps))
385  {
386  m_state = ssWorking;
387  if (obs) appendObservation(obs);
388  if (obs_gps) appendObservation(obs_gps);
389  }
390  else
391  {
392  m_state = ssError;
393  cerr << "ERROR receiving data from Velodyne devic!" << endl;
394  }
395 }
396 
397 /** Tries to initialize the sensor, after setting all the parameters with a call
398  * to loadConfig.
399  * \exception This method must throw an exception with a descriptive message if
400  * some critical error is found. */
402 {
403  this->close();
404 
405  // (0) Preparation:
406  // --------------------------------
407  // Make sure we have calibration data:
408  if (m_velodyne_calib.empty())
409  {
410  // Try to load default data:
411  m_velodyne_calib = VelodyneCalibration::LoadDefaultCalibration(
413  m_model));
414  if (m_velodyne_calib.empty())
416  "Could not find default calibration data for the given LIDAR "
417  "`model` name. Please, specify a valid `model` or load a valid "
418  "XML configuration file first.");
419  }
420 
421  // online vs off line operation??
422  // -------------------------------
423  if (m_pcap_input_file.empty())
424  { // Online
425 
426  if (m_lidar_rpm > 0)
427  {
428  if (!setLidarRPM(m_lidar_rpm))
429  THROW_EXCEPTION("Error in setLidarRPM();");
430  }
431  if (m_lidar_return != UNCHANGED)
432  {
434  THROW_EXCEPTION("Error in setLidarReturnType();");
435  }
436 
437  // (1) Create LIDAR DATA socket
438  // --------------------------------
439  if (INVALID_SOCKET == (m_hDataSock = socket(PF_INET, SOCK_DGRAM, 0)))
441  "Error creating UDP socket:\n%s",
443 
444  struct sockaddr_in bindAddr;
445  memset(&bindAddr, 0, sizeof(bindAddr));
446  bindAddr.sin_family = AF_INET;
447  bindAddr.sin_port = htons(VELODYNE_DATA_UDP_PORT);
448  bindAddr.sin_addr.s_addr = INADDR_ANY;
449 
450  if (int(INVALID_SOCKET) ==
451  ::bind(
452  m_hDataSock, (struct sockaddr*)(&bindAddr), sizeof(sockaddr)))
454 
455 #ifdef _WIN32
456  unsigned long non_block_mode = 1;
457  if (ioctlsocket(m_hDataSock, FIONBIO, &non_block_mode))
459  "Error entering non-blocking mode with ioctlsocket().");
460 #else
461  int oldflags = fcntl(m_hDataSock, F_GETFL, 0);
462  if (oldflags == -1)
463  THROW_EXCEPTION("Error retrieving fcntl() of socket.");
464  oldflags |= O_NONBLOCK | FASYNC;
465  if (-1 == fcntl(m_hDataSock, F_SETFL, oldflags))
466  THROW_EXCEPTION("Error entering non-blocking mode with fcntl();");
467 #endif
468 
469  // (2) Create LIDAR POSITION socket
470  // --------------------------------
471  if (INVALID_SOCKET ==
472  (m_hPositionSock = socket(PF_INET, SOCK_DGRAM, 0)))
474  "Error creating UDP socket:\n%s",
476 
477  bindAddr.sin_port = htons(VELODYNE_POSITION_UDP_PORT);
478 
479  if (int(INVALID_SOCKET) == ::bind(
481  (struct sockaddr*)(&bindAddr),
482  sizeof(sockaddr)))
484 
485 #ifdef _WIN32
486  if (ioctlsocket(m_hPositionSock, FIONBIO, &non_block_mode))
488  "Error entering non-blocking mode with ioctlsocket().");
489 #else
490  oldflags = fcntl(m_hPositionSock, F_GETFL, 0);
491  if (oldflags == -1)
492  THROW_EXCEPTION("Error retrieving fcntl() of socket.");
493  oldflags |= O_NONBLOCK | FASYNC;
494  if (-1 == fcntl(m_hPositionSock, F_SETFL, oldflags))
495  THROW_EXCEPTION("Error entering non-blocking mode with fcntl();");
496 #endif
497  }
498  else
499  { // Offline:
500 #if MRPT_HAS_LIBPCAP
501  char errbuf[PCAP_ERRBUF_SIZE];
502 
503  if (m_pcap_verbose)
504  printf(
505  "\n[CVelodyneScanner] Opening PCAP file \"%s\"\n",
506  m_pcap_input_file.c_str());
507  if ((m_pcap = pcap_open_offline(m_pcap_input_file.c_str(), errbuf)) ==
508  nullptr)
509  {
510  THROW_EXCEPTION_FMT("Error opening PCAP file: '%s'", errbuf);
511  }
512 
513  // Build PCAP filter:
514  {
515  std::string filter_str = mrpt::format(
516  "(udp dst port %d || udp dst port %d)",
519  if (!m_device_ip.empty())
520  filter_str += "&& src host " + m_device_ip;
521 
522  static std::string sMsgError =
523  "[CVelodyneScanner] Error calling pcap_compile: "; // This is
524  // to avoid
525  // the
526  // ill-formed
527  // signature
528  // of
529  // pcap_error()
530  // accepting
531  // "char*",
532  // not
533  // "const
534  // char*"...
535  // sigh
536  if (pcap_compile(
537  reinterpret_cast<pcap_t*>(m_pcap),
538  reinterpret_cast<bpf_program*>(m_pcap_bpf_program),
539  filter_str.c_str(), 1, PCAP_NETMASK_UNKNOWN) < 0)
540  pcap_perror(reinterpret_cast<pcap_t*>(m_pcap), &sMsgError[0]);
541  }
542 
543  m_pcap_file_empty = true;
544  m_pcap_read_count = 0;
545 
546 #else
548  "Velodyne: Reading from PCAP requires building MRPT with libpcap "
549  "support!");
550 #endif
551  }
552 
556 
557  m_initialized = true;
558 }
559 
561 {
562  if (m_hDataSock != INVALID_SOCKET)
563  {
564  shutdown(m_hDataSock, 2); // SD_BOTH );
565 #ifdef _WIN32
566  closesocket(m_hDataSock);
567 #else
569 #endif
570  m_hDataSock = INVALID_SOCKET;
571  }
572 
573  if (m_hPositionSock != INVALID_SOCKET)
574  {
575  shutdown(m_hPositionSock, 2); // SD_BOTH );
576 #ifdef _WIN32
577  closesocket(m_hPositionSock);
578 #else
580 #endif
581  m_hPositionSock = INVALID_SOCKET;
582  }
583 #if MRPT_HAS_LIBPCAP
584  if (m_pcap)
585  {
586  pcap_close(reinterpret_cast<pcap_t*>(m_pcap));
587  m_pcap = nullptr;
588  }
589  if (m_pcap_dumper)
590  {
591  pcap_dump_close(reinterpret_cast<pcap_dumper_t*>(m_pcap_dumper));
592  m_pcap_dumper = nullptr;
593  }
594  if (m_pcap_out)
595  {
596  pcap_close(reinterpret_cast<pcap_t*>(m_pcap_out));
597  m_pcap_out = nullptr;
598  }
599 #endif
600  m_initialized = false;
601 }
602 
603 // Fixed Ethernet headers for PCAP capture --------
604 #if MRPT_HAS_LIBPCAP
605 const uint16_t LidarPacketHeader[21] = {0xffff, 0xffff, 0xffff, 0x7660, 0x0088,
606  0x0000, 0x0008, 0x0045, 0xd204, 0x0000,
607  0x0040, 0x11ff, 0xaab4, 0xa8c0, 0xc801,
608  0xffff, // checksum 0xa9b4 //source ip
609  // 0xa8c0, 0xc801 is
610  // 192.168.1.200
611  0xffff, 0x4009, 0x4009, 0xbe04, 0x0000};
612 const uint16_t PositionPacketHeader[21] = {
613  0xffff, 0xffff, 0xffff, 0x7660, 0x0088, 0x0000, 0x0008, 0x0045, 0xd204,
614  0x0000, 0x0040, 0x11ff, 0xaab4, 0xa8c0, 0xc801, 0xffff, // checksum 0xa9b4
615  // //source ip
616  // 0xa8c0, 0xc801
617  // is 192.168.1.200
618  0xffff, 0x7420, 0x7420, 0x0802, 0x0000};
619 #endif
620 
621 #if defined(_WIN32) && MRPT_HAS_LIBPCAP
622 int gettimeofday(struct timeval* tp, void*)
623 {
624  FILETIME ft;
625  ::GetSystemTimeAsFileTime(&ft);
626  long long t =
627  (static_cast<long long>(ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
628  t -= 116444736000000000LL;
629  t /= 10; // microseconds
630  tp->tv_sec = static_cast<long>(t / 1000000UL);
631  tp->tv_usec = static_cast<long>(t % 1000000UL);
632  return 0;
633 }
634 #endif
635 
637  mrpt::system::TTimeStamp& data_pkt_timestamp,
639  mrpt::system::TTimeStamp& pos_pkt_timestamp,
641 {
644  CObservationVelodyneScan::POS_PACKET_SIZE);
647  CObservationVelodyneScan::PACKET_SIZE);
648 
649  bool ret = true; // all ok
650  if (m_pcap)
651  {
653  data_pkt_timestamp, (uint8_t*)&out_data_pkt, pos_pkt_timestamp,
654  (uint8_t*)&out_pos_pkt);
655  }
656  else
657  {
658  data_pkt_timestamp = internal_receive_UDP_packet(
659  m_hDataSock, (uint8_t*)&out_data_pkt,
660  CObservationVelodyneScan::PACKET_SIZE, m_device_ip);
661  pos_pkt_timestamp = internal_receive_UDP_packet(
662  m_hPositionSock, (uint8_t*)&out_pos_pkt,
663  CObservationVelodyneScan::POS_PACKET_SIZE, m_device_ip);
664  }
665 
666 // Optional PCAP dump:
667 #if MRPT_HAS_LIBPCAP
668  // Save to PCAP file?
669  if (!m_pcap_output_file.empty() &&
670  !m_pcap_out) // 1st time: create output file
671  {
672 #if MRPT_HAS_LIBPCAP
673  char errbuf[PCAP_ERRBUF_SIZE];
674 
677  string sFilePostfix = "_";
678  sFilePostfix += format(
679  "%04u-%02u-%02u_%02uh%02um%02us", (unsigned int)parts.year,
680  (unsigned int)parts.month, (unsigned int)parts.day,
681  (unsigned int)parts.hour, (unsigned int)parts.minute,
682  (unsigned int)parts.second);
683  const string sFileName =
686  string(".pcap");
687 
688  m_pcap_out = pcap_open_dead(DLT_EN10MB, 65535);
689  ASSERTMSG_(
690  m_pcap_out != nullptr, "Error creating PCAP live capture handle");
691 
692  printf(
693  "\n[CVelodyneScanner] Writing to PCAP file \"%s\"\n",
694  sFileName.c_str());
695  if ((m_pcap_dumper = pcap_dump_open(
696  reinterpret_cast<pcap_t*>(m_pcap_out), sFileName.c_str())) ==
697  nullptr)
698  {
700  "Error creating PCAP live dumper: '%s'", errbuf);
701  }
702 #else
704  "Velodyne: Writing PCAP files requires building MRPT with libpcap "
705  "support!");
706 #endif
707  }
708 
709  if (m_pcap_out && m_pcap_dumper &&
710  (data_pkt_timestamp != INVALID_TIMESTAMP ||
711  pos_pkt_timestamp != INVALID_TIMESTAMP))
712  {
713  struct pcap_pkthdr header;
714  struct timeval currentTime;
715  gettimeofday(&currentTime, nullptr);
716  std::vector<unsigned char> packetBuffer;
717 
718  // Data pkt:
719  if (data_pkt_timestamp != INVALID_TIMESTAMP)
720  {
721  header.caplen = header.len =
722  CObservationVelodyneScan::PACKET_SIZE + 42;
723  packetBuffer.resize(header.caplen);
724  header.ts = currentTime;
725 
726  memcpy(&(packetBuffer[0]), LidarPacketHeader, 42);
727  memcpy(
728  &(packetBuffer[0]) + 42, (uint8_t*)&out_data_pkt,
729  CObservationVelodyneScan::PACKET_SIZE);
730  pcap_dump(
731  (u_char*)this->m_pcap_dumper, &header, &(packetBuffer[0]));
732  }
733  // Pos pkt:
734  if (pos_pkt_timestamp != INVALID_TIMESTAMP)
735  {
736  header.caplen = header.len =
737  CObservationVelodyneScan::POS_PACKET_SIZE + 42;
738  packetBuffer.resize(header.caplen);
739  header.ts = currentTime;
740 
741  memcpy(&(packetBuffer[0]), PositionPacketHeader, 42);
742  memcpy(
743  &(packetBuffer[0]) + 42, (uint8_t*)&out_pos_pkt,
744  CObservationVelodyneScan::POS_PACKET_SIZE);
745  pcap_dump(
746  (u_char*)this->m_pcap_dumper, &header, &(packetBuffer[0]));
747  }
748  }
749 #endif
750 
751 // Convert from Velodyne's standard little-endian ordering to host byte
752 // ordering:
753 // (done AFTER saving the pckg as is to pcap above)
754 #if MRPT_IS_BIG_ENDIAN
755  if (data_pkt_timestamp != INVALID_TIMESTAMP)
756  {
758  for (int i = 0; i < CObservationVelodyneScan::BLOCKS_PER_PACKET; i++)
759  {
762  for (int k = 0; k < CObservationVelodyneScan::SCANS_PER_BLOCK; k++)
763  {
765  out_data_pkt.blocks[i].laser_returns[k].distance);
766  }
767  }
768  }
769  if (pos_pkt_timestamp != INVALID_TIMESTAMP)
770  {
773  }
774 #endif
775 
776  // Position packet decimation:
777  if (pos_pkt_timestamp != INVALID_TIMESTAMP)
778  {
781  m_last_pos_packet_timestamp, pos_pkt_timestamp) <
783  {
784  // Ignore this packet
785  pos_pkt_timestamp = INVALID_TIMESTAMP;
786  }
787  else
788  {
789  // Reset time watch:
790  m_last_pos_packet_timestamp = pos_pkt_timestamp;
791  }
792  }
793 
794  return ret;
795 }
796 
797 // static method:
799  platform_socket_t hSocket, uint8_t* out_buffer,
800  const size_t expected_packet_size, const std::string& filter_only_from_IP)
801 {
802  if (hSocket == INVALID_SOCKET)
804  "Error: UDP socket is not open yet! Have you called initialize() "
805  "first?");
806 
807  unsigned long devip_addr = 0;
808  if (!filter_only_from_IP.empty())
809  devip_addr = inet_addr(filter_only_from_IP.c_str());
810 
812 
813  struct pollfd fds[1];
814  fds[0].fd = hSocket;
815  fds[0].events = POLLIN;
816  static const int POLL_TIMEOUT = 1; // (ms)
817 
818  sockaddr_in sender_address;
819  socklen_t sender_address_len = sizeof(sender_address);
820 
821  while (true)
822  {
823  // Unfortunately, the Linux kernel recvfrom() implementation
824  // uses a non-interruptible std::this_thread::sleep_for(ms) when waiting
825  // for data,
826  // which would cause this method to hang if the device is not
827  // providing data. We poll() the device first to make sure
828  // the recvfrom() will not block.
829  //
830  // Note, however, that there is a known Linux kernel bug:
831  //
832  // Under Linux, select() may report a socket file descriptor
833  // as "ready for reading", while nevertheless a subsequent
834  // read blocks. This could for example happen when data has
835  // arrived but upon examination has wrong checksum and is
836  // discarded. There may be other circumstances in which a
837  // file descriptor is spuriously reported as ready. Thus it
838  // may be safer to use O_NONBLOCK on sockets that should not
839  // block.
840 
841  // poll() until input available
842  do
843  {
844  int retval =
845 #if !defined(_WIN32)
846  poll
847 #else
848  WSAPoll
849 #endif
850  (fds, 1, POLL_TIMEOUT);
851  if (retval < 0) // poll() error?
852  {
853  if (errno != EINTR)
855  "Error in UDP poll():\n%s",
857  }
858  if (retval == 0) // poll() timeout?
859  {
860  return INVALID_TIMESTAMP;
861  }
862  if ((fds[0].revents & POLLERR) || (fds[0].revents & POLLHUP) ||
863  (fds[0].revents & POLLNVAL)) // device error?
864  {
866  "Error in UDP poll() (seems Velodyne device error?)");
867  }
868  } while ((fds[0].revents & POLLIN) == 0);
869 
870  // Receive packets that should now be available from the
871  // socket using a blocking read.
872  int nbytes = recvfrom(
873  hSocket, (char*)&out_buffer[0], expected_packet_size, 0,
874  (sockaddr*)&sender_address, &sender_address_len);
875 
876  if (nbytes < 0)
877  {
878  if (errno != EWOULDBLOCK) THROW_EXCEPTION("recvfrom() failed!?!");
879  }
880  else if ((size_t)nbytes == expected_packet_size)
881  {
882  // read successful,
883  // if packet is not from the lidar scanner we selected by IP,
884  // continue otherwise we are done
885  if (!filter_only_from_IP.empty() &&
886  sender_address.sin_addr.s_addr != devip_addr)
887  continue;
888  else
889  break; // done
890  }
891 
892  std::cerr
893  << "[CVelodyneScanner] Warning: incomplete Velodyne packet read: "
894  << nbytes << " bytes\n";
895  }
896 
897  // Average the times at which we begin and end reading. Use that to
898  // estimate when the scan occurred.
900 
902  time1.time_since_epoch().count() / 2 +
903  time2.time_since_epoch().count() / 2));
904 }
905 
907  mrpt::system::TTimeStamp& data_pkt_time, uint8_t* out_data_buffer,
908  mrpt::system::TTimeStamp& pos_pkt_time, uint8_t* out_pos_buffer)
909 {
910 #if MRPT_HAS_LIBPCAP
911  ASSERT_(m_pcap);
912 
913  data_pkt_time = INVALID_TIMESTAMP;
914  pos_pkt_time = INVALID_TIMESTAMP;
915 
916  char errbuf[PCAP_ERRBUF_SIZE];
917  struct pcap_pkthdr* header;
918  const u_char* pkt_data;
919 
920  while (true)
921  {
922  int res;
923  if ((res = pcap_next_ex(
924  reinterpret_cast<pcap_t*>(m_pcap), &header, &pkt_data)) >= 0)
925  {
927 
928  // if packet is not from the lidar scanner we selected by IP,
929  // continue
930  if (pcap_offline_filter(
931  reinterpret_cast<bpf_program*>(m_pcap_bpf_program), header,
932  pkt_data) == 0)
933  {
934  if (m_verbose)
935  std::cout
936  << "[CVelodyneScanner] DEBUG: Filtering out packet #"
937  << m_pcap_read_count << " in PCAP file.\n";
938  continue;
939  }
940 
941  // Determine whether it is a DATA or POSITION packet:
942  m_pcap_file_empty = false;
944  const uint16_t udp_dst_port =
945  ntohs(*reinterpret_cast<const uint16_t*>(pkt_data + 0x24));
946 
948  {
949  if (m_verbose)
950  std::cout << "[CVelodyneScanner] DEBUG: Packet #"
952  << " in PCAP file is POSITION pkt.\n";
953  memcpy(
954  out_pos_buffer, pkt_data + 42,
955  CObservationVelodyneScan::POS_PACKET_SIZE);
956  pos_pkt_time = tim; // success
957  return true;
958  }
959  else if (udp_dst_port == CVelodyneScanner::VELODYNE_DATA_UDP_PORT)
960  {
961  if (m_verbose)
962  std::cout << "[CVelodyneScanner] DEBUG: Packet #"
964  << " in PCAP file is DATA pkt.\n";
965  memcpy(
966  out_data_buffer, pkt_data + 42,
967  CObservationVelodyneScan::PACKET_SIZE);
968  data_pkt_time = tim; // success
969  return true;
970  }
971  else
972  {
973  std::cerr << "[CVelodyneScanner] ERROR: Packet "
975  << " in PCAP file passed the filter but does not "
976  "match expected UDP port numbers! Skipping it.\n";
977  }
978  }
979 
980  if (m_pcap_file_empty) // no data in file?
981  {
982  fprintf(
983  stderr,
984  "[CVelodyneScanner] Maybe the PCAP file is empty? Error %d "
985  "reading Velodyne packet: `%s`\n",
986  res, pcap_geterr(reinterpret_cast<pcap_t*>(m_pcap)));
987  return true;
988  }
989 
990  if (m_pcap_read_once)
991  {
992  if (m_pcap_verbose)
993  printf(
994  "[CVelodyneScanner] INFO: end of file reached -- done "
995  "reading.\n");
996  std::this_thread::sleep_for(250ms);
997  return false;
998  }
999 
1000  if (m_pcap_repeat_delay > 0.0)
1001  {
1002  if (m_pcap_verbose)
1003  printf(
1004  "[CVelodyneScanner] INFO: end of file reached -- delaying "
1005  "%.3f seconds.\n",
1007  std::this_thread::sleep_for(
1008  std::chrono::duration<double, std::milli>(
1009  m_pcap_repeat_delay * 1000.0));
1010  }
1011 
1012  if (m_pcap_verbose)
1013  printf("[CVelodyneScanner] INFO: replaying Velodyne dump file.\n");
1014 
1015  // rewind the file
1016  pcap_close(reinterpret_cast<pcap_t*>(m_pcap));
1017  if ((m_pcap = pcap_open_offline(m_pcap_input_file.c_str(), errbuf)) ==
1018  nullptr)
1019  {
1020  THROW_EXCEPTION_FMT("Error opening PCAP file: '%s'", errbuf);
1021  }
1022  m_pcap_file_empty = true; // maybe the file disappeared?
1023  } // loop back and try again
1024 #else
1026  "MRPT needs to be built against libpcap to enable this functionality")
1027 #endif
1028 }
1029 
1031 {
1032  /* HTTP-based config: http://10.0.0.100/tab/config.html
1033  <form name='returns' method="post" action="/cgi/setting">
1034  <span>Return&nbsp;Type:&nbsp;</span> <select name="returns"
1035  onchange='javascript:this.form.submit()'>
1036  <option>Strongest</option>
1037  <option>Last</option>
1038  <option>Dual</option>
1039  </select>
1040  </form>
1041  */
1042  MRPT_START;
1043  std::string strRet;
1044  switch (ret_type)
1045  {
1046  case STRONGEST:
1047  strRet = "Strongest";
1048  break;
1049  case DUAL:
1050  strRet = "Dual";
1051  break;
1052  case LAST:
1053  strRet = "Last";
1054  break;
1055  case UNCHANGED:
1056  return true;
1057  default:
1058  THROW_EXCEPTION("Invalid value for return type!");
1059  };
1060 
1061  const std::string cmd = mrpt::format("returns=%s", strRet.c_str());
1062  return this->internal_send_http_post(cmd);
1063  MRPT_END;
1064 }
1065 
1067 {
1068  /* HTTP-based config: http://10.0.0.100/tab/config.html
1069  <form name='rpm' method="post" action="/cgi/setting">
1070  Motor
1071  &nbsp;RPM:&nbsp;<input type="text" name="rpm" size="5"
1072  style="text-align:right" /><input type="button" value="+"
1073  onclick="javascript:this.form.rpm.value++;this.form.submit()" /><input
1074  type="button" value="-"
1075  onclick="javascript:this.form.rpm.value--;this.form.submit()" />
1076  &nbsp;<input type="submit" value="Set" />
1077  </form>
1078  */
1079 
1080  MRPT_START;
1081  const std::string cmd = mrpt::format("rpm=%i", rpm);
1082  return this->internal_send_http_post(cmd);
1083  MRPT_END;
1084 }
1085 
1087 {
1088  // laser = on|off
1089  MRPT_START;
1090  const std::string cmd = mrpt::format("laser=%s", on ? "on" : "off");
1091  return this->internal_send_http_post(cmd);
1092  MRPT_END;
1093 }
1094 
1096 {
1097  // frame publishing | data packet publishing = on|off
1098  MRPT_START;
1099  m_return_frames = on;
1100  MRPT_END;
1101 }
1102 
1104 {
1105  MRPT_START;
1106 
1107  ASSERTMSG_(
1108  !m_device_ip.empty(), "A device IP address must be specified first!");
1109 
1110  using namespace mrpt::comms::net;
1111 
1112  std::vector<uint8_t> post_out;
1113  string post_err_str;
1114 
1115  int http_rep_code;
1116  mrpt::system::TParameters<string> extra_headers, out_headers;
1117 
1118  extra_headers["Origin"] = mrpt::format("http://%s", m_device_ip.c_str());
1119  extra_headers["Referer"] = mrpt::format("http://%s", m_device_ip.c_str());
1120  extra_headers["Upgrade-Insecure-Requests"] = "1";
1121  extra_headers["Content-Type"] = "application/x-www-form-urlencoded";
1122 
1124  "POST", post_data,
1125  mrpt::format("http://%s/cgi/setting", m_device_ip.c_str()), post_out,
1126  post_err_str, 80 /* port */, string(), string(), // user,pass
1127  &http_rep_code, &extra_headers, &out_headers);
1128 
1129  return mrpt::comms::net::erOk == ret &&
1130  (http_rep_code == 200 || http_rep_code == 204); // OK codes
1131 
1132  MRPT_END;
1133 }
std::chrono::duration< rep, period > duration
Definition: Clock.h:25
void timestampToParts(TTimeStamp t, TTimeParts &p, bool localTime=false)
Gets the individual parts of a date/time (days, hours, minutes, seconds) - UTC time or local time...
Definition: datetime.cpp:61
double time1
content_t fields
Message content, accesible by individual fields.
bool loadFromXMLFile(const std::string &velodyne_calibration_xml_filename)
Loads calibration from file, in the format supplied by the manufacturer.
#define MRPT_START
Definition: exceptions.h:262
A CObservation-derived class for RAW DATA (and optionally, point cloud) of scans from 3D Velodyne LID...
GLdouble GLdouble t
Definition: glext.h:3689
unsigned __int16 uint16_t
Definition: rptypes.h:44
A C++ interface to Velodyne laser scanners (HDL-64, HDL-32, VLP-16), working on Linux and Windows...
void appendObservation(const mrpt::serialization::CSerializable::Ptr &obj)
Like appendObservations() but for just one observation.
mrpt::system::TTimeStamp getAsTimestamp(const mrpt::system::TTimeStamp &date) const
Build an MRPT timestamp with the hour/minute/sec of this structure and the date from the given timest...
static short int VELODYNE_POSITION_UDP_PORT
Default: 8308.
std::map< model_t, TModelProperties > model_properties_list_t
std::chrono::time_point< Clock > time_point
Definition: Clock.h:26
#define THROW_EXCEPTION(msg)
Definition: exceptions.h:41
std::string m_sensorLabel
See CGenericSensor.
double DEG2RAD(const double x)
Degrees to radians.
bool getNextObservation(mrpt::obs::CObservationVelodyneScan::Ptr &outScan, mrpt::obs::CObservationGPS::Ptr &outGPS)
Polls the UDP port for incoming data packets.
bool internal_read_PCAP_packet(mrpt::system::TTimeStamp &data_pkt_time, uint8_t *out_data_buffer, mrpt::system::TTimeStamp &pos_pkt_time, uint8_t *out_pos_buffer)
mrpt::system::TTimeStamp buildTimestampFromParts(const mrpt::system::TTimeParts &p)
Builds a timestamp from the parts (Parts are in UTC)
Definition: datetime.cpp:85
bool m_return_frames
Default: true Output whole frames and not data packets.
static std::string getListKnownModels()
Return human-readable string: "`VLP16`,`XXX`,...".
std::string getLastSocketErrorStr()
Returns a description of the last Sockets error.
Definition: net_utils.cpp:480
void close()
Close the UDP sockets set-up in initialize().
bool m_pcap_read_once
Default: false.
mrpt::system::TTimeStamp now()
A shortcut for system::getCurrentTime.
Definition: datetime.h:87
Contains classes for various device interfaces.
void * m_pcap_bpf_program
opaque ptr: bpf_program*
float read_float(const std::string &section, const std::string &name, float defaultValue, bool failIfNotFound=false) const
STL namespace.
std::string fileNameStripInvalidChars(const std::string &filename, const char replacement_to_invalid_chars='_')
Replace invalid filename chars by underscores (&#39;_&#39;) or any other user-given char. ...
Definition: filesystem.cpp:328
GLdouble s
Definition: glext.h:3676
void reverseBytesInPlace(bool &v_in_out)
Reverse the order of the bytes of a given type (useful for transforming btw little/big endian) ...
int8_t validity_char
This will be: &#39;A&#39;=OK or &#39;V&#39;=void.
bool m_pcap_read_fast
(Default: false) If false, will use m_pcap_read_full_scan_delay_ms
UTC_time UTCTime
The GPS sensor measured timestamp (in UTC time)
mrpt::system::TTimeStamp m_last_pos_packet_timestamp
char NMEA_GPRMC[72+234]
the full $GPRMC message, as received by Velodyne, terminated with "\r\n\0"
unsigned char uint8_t
Definition: rptypes.h:41
ENUMTYPE read_enum(const std::string &section, const std::string &name, const ENUMTYPE &defaultValue, bool failIfNotFound=false) const
Reads an "enum" value, where the value in the config file can be either a numerical value or the symb...
double m_pcap_repeat_delay
Default: 0 (in seconds)
mrpt::obs::VelodyneCalibration m_velodyne_calib
Device calibration file (supplied by vendor in an XML file)
#define ASSERT_(f)
Defines an assertion mechanism.
Definition: exceptions.h:113
mrpt::Clock::time_point TTimeStamp
A system independent time type, it holds the the number of 100-nanosecond intervals since January 1...
Definition: datetime.h:40
This class allows loading and storing values and vectors of different types from a configuration text...
ERRORCODE_HTTP
Possible returns from a HTTP request.
Definition: net_utils.h:31
virtual void initialize()
Tries to initialize the sensor driver, after setting all the parameters with a call to loadConfig...
double m_pos_packets_min_period
Default: 0.5 seconds.
std::shared_ptr< CObservationVelodyneScan > Ptr
bool setLidarOnOff(bool on)
Switches the LASER on/off (saves energy when not measuring) (via HTTP post interface).
A helper class that can convert an enum value into its textual representation, and viceversa...
nv_oem6_header_t header
Novatel frame: NV_OEM6_BESTPOS.
bool receivePackets(mrpt::system::TTimeStamp &data_pkt_timestamp, mrpt::obs::CObservationVelodyneScan::TVelodyneRawPacket &out_data_pkt, mrpt::system::TTimeStamp &pos_pkt_timestamp, mrpt::obs::CObservationVelodyneScan::TVelodynePositionPacket &out_pos_pkt)
Users normally would prefer calling getNextObservation() instead.
The parts of a date/time (it&#39;s like the standard &#39;tm&#39; but with fractions of seconds).
Definition: datetime.h:49
unsigned int m_pcap_read_count
number of pkts read from the file so far (for debugging)
std::string m_pcap_output_file
Default: "" (do not dump to an offline file)
uint8_t day
Month (1-12)
Definition: datetime.h:53
static const model_properties_list_t & get()
Singleton access.
This namespace contains representation of robot actions and observations.
bool m_pcap_verbose
Default: true Output PCAP Info msgs.
#define ASSERTMSG_(f, __ERROR_MSG)
Defines an assertion mechanism.
Definition: exceptions.h:101
bool setLidarReturnType(return_type_t ret_type)
Changes among STRONGEST, LAST, DUAL return types (via HTTP post interface).
GLsizei const GLchar ** string
Definition: glext.h:4101
uint16_t header
Block id: UPPER_BANK or LOWER_BANK.
std::string m_pcap_input_file
Default: "" (do not operate from an offline file)
#define IMPLEMENTS_GENERIC_SENSOR(class_name, NameSpace)
This must be inserted in all CGenericSensor classes implementation files:
static bool parse_NMEA(const std::string &cmd_line, mrpt::obs::CObservationGPS &out_obs, const bool verbose=false)
Parses one line of NMEA data from a GPS receiver, and writes the recognized fields (if any) into an o...
void setFramePublishing(bool on)
Switches whole frame (points in a single revolution) on/off publication to data packet publication...
int fprintf(FILE *fil, const char *format,...) noexcept MRPT_printf_format_check(2
An OS-independent version of fprintf.
Definition: os.cpp:406
double time2
double second
Minute (0-59)
Definition: datetime.h:56
#define MRPT_LOAD_CONFIG_VAR( variableName, variableType, configFileObject, sectionNameStr)
An useful macro for loading variables stored in a INI-like file under a key with the same name that t...
void * m_pcap_out
opaque ptr: "pcap_t*"
bool internal_send_http_post(const std::string &post_data)
mrpt::system::TTimeStamp getDateAsTimestamp() const
Build an MRPT timestamp with the year/month/day of this observation.
This is the global namespace for all Mobile Robot Programming Toolkit (MRPT) libraries.
double m_pos_packets_timing_timeout
Default: 30 seconds.
uint8_t minute
Hour (0-23)
Definition: datetime.h:55
A class used to store a 3D pose (a 3D translation + a rotation in 3D).
Definition: CPose3D.h:86
One unit of data from the scanner (the payload of one UDP DATA packet)
std::string format(const char *fmt,...) MRPT_printf_format_check(1
A std::string version of C sprintf.
Definition: format.cpp:16
bool empty() const
Returns true if no calibration has been loaded yet.
#define MRPT_END
Definition: exceptions.h:266
static short int VELODYNE_DATA_UDP_PORT
Default: 2368.
double m_pcap_read_full_scan_delay_ms
(Default:100 ms) delay after each full scan read from a PCAP log
void * m_pcap
opaque ptr: "pcap_t*"
std::shared_ptr< CObservationGPS > Ptr
uint8_t month
The year.
Definition: datetime.h:52
ERRORCODE_HTTP http_request(const string &http_method, const string &http_send_content, const string &url, std::vector< uint8_t > &out_content, string &out_errormsg, int port=80, const string &auth_user=string(), const string &auth_pass=string(), int *out_http_responsecode=nullptr, mrpt::system::TParameters< string > *extra_headers=nullptr, mrpt::system::TParameters< string > *out_headers=nullptr, int timeout_ms=1000)
Generic function for HTTP GET & POST methods.
Definition: net_utils.cpp:69
void doProcess()
This method will be invoked at a minimum rate of "process_rate" (Hz)
A set of useful routines for networking.
Definition: net_utils.h:23
#define MRPT_LOAD_HERE_CONFIG_VAR( variableName, variableType, targetVariable, configFileObject, sectionNameStr)
std::shared_ptr< T > make_aligned_shared(Args &&... args)
Creates a shared_ptr with aligned memory via aligned_allocator_cpp11<>.
uint8_t hour
Day (1-31)
Definition: datetime.h:54
uint32_t platform_socket_t
Handles for the UDP sockets, or INVALID_SOCKET (-1)
uint16_t rotation
0-35999, divide by 100 to get degrees
#define MRPT_COMPILE_TIME_ASSERT(expression)
Definition: exceptions.h:127
double timeDifference(const mrpt::system::TTimeStamp t_first, const mrpt::system::TTimeStamp t_later)
Returns the time difference from t1 to t2 (positive if t2 is posterior to t1), in seconds...
Definition: datetime.h:122
GLuint res
Definition: glext.h:7268
void * m_pcap_dumper
opaque ptr: "pcap_dumper_t *"
std::string m_device_ip
Default: "" (no IP-based filtering)
#define THROW_EXCEPTION_FMT(_FORMAT_STRING,...)
Definition: exceptions.h:43
bool loadCalibrationFile(const std::string &velodyne_xml_calib_file_path)
Returns false on error.
For usage when passing a dynamic number of (numeric) arguments to a function, by name.
Definition: TParameters.h:54
void loadConfig_sensorSpecific(const mrpt::config::CConfigFileBase &configSource, const std::string &section)
See the class documentation at the top for expected parameters.
const Scalar * const_iterator
Definition: eigen_plugins.h:27
#define INVALID_TIMESTAMP
Represents an invalid timestamp, where applicable.
Definition: datetime.h:43
mrpt::system::TTimeStamp m_last_gps_rmc_age
mrpt::obs::CObservationVelodyneScan::Ptr m_rx_scan
In progress RX scan.
static mrpt::system::TTimeStamp internal_receive_UDP_packet(platform_socket_t hSocket, uint8_t *out_buffer, const size_t expected_packet_size, const std::string &filter_only_from_IP)
model_t m_model
Default: "VLP16".
mrpt::obs::gnss::Message_NMEA_RMC m_last_gps_rmc
void memcpy(void *dest, size_t destSize, const void *src, size_t copyCount) noexcept
An OS and compiler independent version of "memcpy".
Definition: os.cpp:356
bool setLidarRPM(int rpm)
Changes Lidar RPM (valid range: 300-600) (via HTTP post interface).



Page generated by Doxygen 1.8.14 for MRPT 1.9.9 Git: 7d5e6d718 Fri Aug 24 01:51:28 2018 +0200 at lun nov 2 08:35:50 CET 2020