ifm_o3mxxx_node.cpp
Go to the documentation of this file.
1 /* This IFM data receiver is done based on the IFM example codes.
2  * Done by Antti Kolu, antti.kolu@tut.fi, anttikolu@gmail.com
3 */
4 
5 #include "ros/ros.h"
7 #include "ifm/example_results.h"
8 #include "ifm/example_types.h"
9 #include "ifm_o3mxxx/IFMO3mxxx.h"
10 
11 #include "DI_CH08_1_0.h"
12 #include "DI_CH08_1_1.h"
13 #include "DI_CH08_1_2.h"
14 #include "DI_CH08_1_3.h"
15 
16 #include "sensor_msgs/Image.h"
18 #include "sensor_msgs/PointCloud2.h"
20 
21 
22 using std::string;
23 
24 // include all versions of distance image channel 14
25 //#include "DI_CH14_1_3.h"
26 
27 // include all versions of distance image channel 256
28 //#include "DI_CH256_1_4.h"
29 
30 #define NUM_SENSOR_PIXELS (1024)
31 
32 
33 // We copy the received data into this structures.
34 //
35 //struct distanceData_t
36 //{
37 // uint16_t distanceData[NUM_SENSOR_PIXELS];
38 // uint16_t confidence[NUM_SENSOR_PIXELS];
39 // float x[NUM_SENSOR_PIXELS];
40 // float y[NUM_SENSOR_PIXELS];
41 // float z[NUM_SENSOR_PIXELS];
42 // uint16_t width;
43 // uint16_t height;
44 //}__attribute__((packed));
45 
46 ifm_o3mxxx::IFMO3mxxx ifm_ros_msg;
47 
48 //static distanceData_t distanceData;
49 
50 #define UDP_PACKET_BUF_LEN (2000)
51 
59 const uint16_t confidence128 = 128;
60 const uint16_t confidence256 = 256;
61 const uint16_t confidence512 = 512;
62 const uint16_t confidence1024 = 1024;
63 const uint16_t confidence2048 = 2048;
64 const uint16_t confidence4096 = 4096;
65 
66 
68 {
80 
81 }__attribute__((packed));
82 
83 
85 {
88 
89 }__attribute__((packed));
90 
91 struct ChannelEnd
92 {
94 
95 }__attribute__((packed));
96 
97 // Extracts the data in the payload of the udp packet und puts it into the channel buffer
98 
99 int processPacket( int8_t* currentPacketData, // payload of the udp packet (without ethernet/IP/UDP header)
100  int currentPacketSize, // size of the udp packet payload
101  int8_t* channelBuffer, // buffer for a complete channel
102  uint32_t channelBufferSize, // size of the buffer for the complete channel
103  uint32_t* pos) // the current pos in the channel buffer
104 {
105 
106  // There is always a PacketHeader structure at the beginning
107  PacketHeader* ph = (PacketHeader*)currentPacketData;
108  int Start = sizeof(PacketHeader);
109  int Length = currentPacketSize - sizeof(PacketHeader);
110 
111  // Only the first packet of a channel contains a ChannelHeader
112  if (ph->IndexOfPacketInChannel == 0)
113  {
114  Start += sizeof(ChannelHeader);
115  Length -= sizeof(ChannelHeader);
116  }
117 
118  // Only the last packet of a channel contains an EndDelimiter (at the end, after the data)
120  {
121  Length -= sizeof(ChannelEnd);
122  }
123 
124  // Is the buffer big enough?
125  if ((*pos) + Length > channelBufferSize)
126  {
127  // Too small means either an error in the program logic or a corrupt packet
128  printf("Channel buffer is too small.\n");
129  return RESULT_ERROR;
130  }
131  else
132  {
133  memcpy(channelBuffer+(*pos), currentPacketData+Start, Length);
134  }
135 
136  (*pos) += Length;
137 
138  return RESULT_OK;
139 
140 }
141 
142 
143 // For every version of the data structure there needs to be a copy function.
144 // The content of the function is always the same, only the type of p will be different each time.
145 // In C++ this could be solved with a template.
146 
147 // copy the data for DI structure version 1.0 into our internal structure
149 {
150 
151  if (p == NULL)
152  return RESULT_ERROR;
153 
157  memcpy(&ifm_ros_msg.x[0], p->distanceImageResult.X, NUM_SENSOR_PIXELS*sizeof(*(p->distanceImageResult.X)));
158  memcpy(&ifm_ros_msg.y[0], p->distanceImageResult.Y, NUM_SENSOR_PIXELS*sizeof(*(p->distanceImageResult.Y)));
159  memcpy(&ifm_ros_msg.z[0], p->distanceImageResult.Z, NUM_SENSOR_PIXELS*sizeof(*(p->distanceImageResult.Z)));
160 
167 
171 
172  memcpy(&ifm_ros_msg.amplitude_normalization[0], p->distanceImageResult.amplitude_normalization, 4*sizeof(*(p->distanceImageResult.amplitude_normalization)));
173 
174 
175  return RESULT_OK;
176 }
177 
178 
179 // copy the data for DI structure version 1.1 into our internal structure
181 {
182  if (p == NULL)
183  return RESULT_ERROR;
184 
188  memcpy(&ifm_ros_msg.x[0], p->distanceImageResult.X, NUM_SENSOR_PIXELS*sizeof(*(p->distanceImageResult.X)));
189  memcpy(&ifm_ros_msg.y[0], p->distanceImageResult.Y, NUM_SENSOR_PIXELS*sizeof(*(p->distanceImageResult.Y)));
190  memcpy(&ifm_ros_msg.z[0], p->distanceImageResult.Z, NUM_SENSOR_PIXELS*sizeof(*(p->distanceImageResult.Z)));
191 
198 
202 
203  memcpy(&ifm_ros_msg.amplitude_normalization[0], p->distanceImageResult.amplitude_normalization, 4*sizeof(*(p->distanceImageResult.amplitude_normalization)));
204 
205 
206  return RESULT_OK;
207 }
208 
209 // copy the data for DI structure version 1.2 into our internal structure
211 {
212  if (p == NULL)
213  return RESULT_ERROR;
214 
218  memcpy(&ifm_ros_msg.x[0], p->distanceImageResult.X, NUM_SENSOR_PIXELS*sizeof(*(p->distanceImageResult.X)));
219  memcpy(&ifm_ros_msg.y[0], p->distanceImageResult.Y, NUM_SENSOR_PIXELS*sizeof(*(p->distanceImageResult.Y)));
220  memcpy(&ifm_ros_msg.z[0], p->distanceImageResult.Z, NUM_SENSOR_PIXELS*sizeof(*(p->distanceImageResult.Z)));
221 
228 
232 
233  memcpy(&ifm_ros_msg.amplitude_normalization[0], p->distanceImageResult.amplitude_normalization, 4*sizeof(*(p->distanceImageResult.amplitude_normalization)));
234 
235 
236  return RESULT_OK;
237 }
238 
239 // copy the data for DI structure version 1.3 into our internal structure
241 {
242  if (p == NULL)
243  return RESULT_ERROR;
244 
248  memcpy(&ifm_ros_msg.x[0], p->distanceImageResult.X, NUM_SENSOR_PIXELS*sizeof(*(p->distanceImageResult.X)));
249  memcpy(&ifm_ros_msg.y[0], p->distanceImageResult.Y, NUM_SENSOR_PIXELS*sizeof(*(p->distanceImageResult.Y)));
250  memcpy(&ifm_ros_msg.z[0], p->distanceImageResult.Z, NUM_SENSOR_PIXELS*sizeof(*(p->distanceImageResult.Z)));
251 
258 
262 
263  memcpy(&ifm_ros_msg.amplitude_normalization[0], p->distanceImageResult.amplitude_normalization, 4*sizeof(*(p->distanceImageResult.amplitude_normalization)));
264 
265 
266  return RESULT_OK;
267 }
268 
269 
270 // checks which version of the data it is and copies the data into our own structure.
271 // You need this for every channel you want to process.
272 int processChannel8(void* buf, uint32_t size)
273 {
278 
279  // Is this DI structure version 1.0?
280  pDIA1_1_0 = ifm_o3m_ConvertBufferToLittleEndian_DIA1_1_0(buf, size);
281  if (pDIA1_1_0)
282  {
283  // yes it is, so copy the data from this structure
284  copyChannel8_DIA1_1_0(pDIA1_1_0);
285  return RESULT_OK;
286  }
287 
288  // It wasn't a version 1.0 structure, so let's check if it's a version 1.1 structure
289  pDIA1_1_1 = ifm_o3m_ConvertBufferToLittleEndian_DIA1_1_1(buf, size);
290  if (pDIA1_1_1)
291  {
292  // It's a version 1.1 structure. Copy data from this structure
293  copyChannel8_DIA1_1_1(pDIA1_1_1);
294  return RESULT_OK;
295  }
296 
297  // It wasn't a version 1.0 structure, so let's check if it's a version 1.1 structure
298  pDIA1_1_2 = ifm_o3m_ConvertBufferToLittleEndian_DIA1_1_2(buf, size);
299  if (pDIA1_1_2)
300  {
301  // It's a version 1.1 structure. Copy data from this structure
302  copyChannel8_DIA1_1_2(pDIA1_1_2);
303  return RESULT_OK;
304  }
305 
306  // It wasn't a version 1.0 structure, so let's check if it's a version 1.1 structure
307  pDIA1_1_3 = ifm_o3m_ConvertBufferToLittleEndian_DIA1_1_3(buf, size);
308  if (pDIA1_1_3)
309  {
310  // It's a version 1.1 structure. Copy data from this structure
311  copyChannel8_DIA1_1_3(pDIA1_1_3);
312  return RESULT_OK;
313  }
314 
315  // For new versions you have to add the additional converts/copy hier.
316 
317 
318  // This is no known version of the data
319  printf("*** Unknown version *** \n\n");
320  return RESULT_ERROR;
321 
322 
323 }
324 
325 
326 int main(int argc, char **argv)
327 {
328  ros::init(argc, argv, "ifm_o3mxxx_node");
329  ros::NodeHandle node;
330  ros::NodeHandle node_priv("~");
331 
332 
333  string out_imf_depth_image = "";
334  string out_imf_point_cloud = "";
335  string out_imf_raw = "";
336  string frame_id = "";
337 
338  int port = 4599;
339  string ip = "255.255.255.255";
340 
341  node_priv.param("OutDepthImageTopic", out_imf_depth_image, std::string("ifm_o3mxxx_depth_image"));
342  node_priv.param("PointCloudTopic", out_imf_point_cloud, std::string("ifm_o3mxxx_pc"));
343  node_priv.param("RawTopic", out_imf_raw, std::string("ifm_o3mxxx_raw"));
344  node_priv.param("Port", port, 4599);
345  node_priv.param("IP", ip, std::string("255.255.255.255"));
346  node_priv.param("FrameID", frame_id, std::string("ifm"));
347 
348  ros::Publisher pub_img = node.advertise<sensor_msgs::Image> (out_imf_depth_image, 1);
349  ros::Publisher pub_pc = node.advertise<sensor_msgs::PointCloud2> (out_imf_point_cloud, 1);
350  ros::Publisher pub_imf_raw = node.advertise<ifm_o3mxxx::IFMO3mxxx> (out_imf_raw, 1);
351 
352  UDPReceiver receiver;
353  receiver.init(port, ip);
354 
355  // The data is in the channel 8
356  const uint32_t customerDataChannel = 8;
357 
358  // buffer for a single UDP packet
359  int8_t udpPacketBuf[UDP_PACKET_BUF_LEN];
360 
361  // As the alignment was forced to 1 we can work with the struct on the buffer.
362  // This assumes the byte order is little endian which it is on a PC.
363  PacketHeader* ph = (PacketHeader*)udpPacketBuf;
364 
365  // the size of the channel may change so the size will be taken from the packet
366  uint32_t channel_buf_size = 0;
367  int8_t* channelBuf = NULL;
368 
369  // As there is no offset in the packet header we have to remember where the next part should go
370  uint32_t pos_in_channel = 0;
371 
372  // remember the counter of the previous packet so we know when we are losing packets
373  uint32_t previous_packet_counter = 0;
374  bool previous_packet_counter_valid = false;
375 
376  // the receiption of the data may start at any time. So we wait til we find the beginning of our channel
377  bool startOfChannelFound = false;
378 
379  while (node.ok())
380  {
381  int ret_val = receiver.receive(udpPacketBuf, UDP_PACKET_BUF_LEN, 100000);
382 
383  if(ret_val >= 0){
384  //ROS_INFO("packet received");
385  // Check the packet counter for missing packets
386  if (previous_packet_counter_valid)
387  {
388  // if the type of the variables is ui32, it will also work when the wrap around happens.
389  if ((ph->PacketCounter - previous_packet_counter) != 1)
390  {
391  printf("Packet Counter jumped from %i to %i (missing packets; try to redirect output)\n", previous_packet_counter, ph->PacketCounter);
392 
393  // With this it will ignore the already received parts and resynchronize at
394  // the beginning of the next cycle.
395  startOfChannelFound = false;
396  }
397  }
398 
399  previous_packet_counter = ph->PacketCounter;
400  previous_packet_counter_valid = true;
401 
402  // is this the channel with our data?
403  //ROS_INFO("channel %i, prev_counter %i, packet_counter %i", ph->ChannelID, previous_packet_counter, ph->PacketCounter );
404  if (ph->ChannelID == customerDataChannel)
405  {
406 
407  // are we at the beginning of the channel?
408  if (ph->IndexOfPacketInChannel == 0)
409  {
410  startOfChannelFound = true;
411 
412  // If we haven't allocated memory for the channel do it now.
413  if (channel_buf_size == 0)
414  {
415  channel_buf_size = ph->TotalLengthOfChannel;
416  channelBuf = (int8_t*) malloc(channel_buf_size);
417  }
418 
419  // as we reuse the buffer we clear it at the beginning of a transmission
420  memset(channelBuf, 0, channel_buf_size);
421  pos_in_channel = 0;
422 
423  }
424 
425  // if we have found the start of the channel at least once, we are ready to process the packet
426  if (startOfChannelFound)
427  {
428 
429  processPacket(udpPacketBuf, ret_val, channelBuf, channel_buf_size, &pos_in_channel);
430 
431  // Have we found the last packet in this channel? Then we are able to process it
432  // The index is zero based so a channel with n parts will have indices from 0 to n-1
434  {
435  // pos_in_channel is the position where the (not existing) next packet would be
436  // placed. This is also the size of the data.
437  int ret = processChannel8((void*)channelBuf, pos_in_channel);
438  //displayData();
439  sensor_msgs::Image img;
440  img.header.stamp = ros::Time::now();
441  img.header.frame_id = frame_id;
443  img.data.resize(NUM_SENSOR_PIXELS*4);
444  for(unsigned int i = 0 ; i < NUM_SENSOR_PIXELS; ++i){
445  //img.data.at(i) = (static_cast<float>(ifm_ros_msg.distance_data[i])/100.0);
446  //img.data.at(i*2) = static_cast<uint16_t>(ifm_ros_msg.distance_data[i]);
447  float32_t pixel = static_cast<float32_t>(ifm_ros_msg.distance_data[i])/100;
448  memcpy(&(img.data.at(i*4)), &pixel, sizeof(float32_t) );
449  }
450 
451  img.height = ifm_ros_msg.height;
452  img.width = ifm_ros_msg.width;
453  img.step = img.width*4;
454 
455  pub_img.publish(img);
456 
457  pcl::PointCloud<pcl::PointXYZI> pcl_cloud;
458  sensor_msgs::PointCloud2 cloud;
459 
460  pcl::PointXYZI point;
461  for(unsigned int i = 0 ; i < NUM_SENSOR_PIXELS; ++i){
462  if(ifm_ros_msg.confidence[i] & confidence1 || ifm_ros_msg.confidence[i] & confidence4096) // invalid pixel
463  continue;
464  point.x = ifm_ros_msg.x[i];
465  point.y = ifm_ros_msg.y[i];
466  point.z = ifm_ros_msg.z[i];
467  point.intensity = ifm_ros_msg.amplitude[i];
468  pcl_cloud.push_back(point);
469  }
470 
471  pcl::toROSMsg(pcl_cloud, cloud);
472  cloud.header.frame_id = frame_id;
473  cloud.header.stamp = img.header.stamp;
474  pub_pc.publish(cloud);
475 
476  ifm_ros_msg.header.frame_id = frame_id;
477  ifm_ros_msg.header.stamp = img.header.stamp;
478  pub_imf_raw.publish(ifm_ros_msg);
479 
480  }
481  }
482  }
483  }else{
484  ROS_ERROR("timeout");
485  }
486 
487  ros::spinOnce();// Handle ROS events
488  }
489 }
ifm_o3m_uint16_t sensorWidth
Definition: DI_CH08_1_0.h:20
uint32_t ChannelID
ifm_o3m_float32_t amplitude_normalization[4]
Definition: DI_CH08_1_2.h:33
struct ifm_o3m_AlgoIFOutput_DIA1_1_2::@17::@19 cameraCalibration
ifm_o3m_uint16_t distanceData[1024]
Definition: DI_CH08_1_3.h:56
ifm_o3m_uint16_t sensorHeight
Definition: DI_CH08_1_2.h:26
void publish(const boost::shared_ptr< M > &message) const
ifm_o3m_uint16_t distanceData[1024]
Definition: DI_CH08_1_2.h:27
#define NUM_SENSOR_PIXELS
ifm_o3m_float32_t Y[1024]
Definition: DI_CH08_1_1.h:29
ifm_o3mxxx::IFMO3mxxx ifm_ros_msg
const uint16_t confidence1
ifm_o3m_uint16_t sensorWidth
Definition: DI_CH08_1_3.h:39
ifm_o3m_float32_t X[1024]
Definition: DI_CH08_1_1.h:28
const uint16_t confidence512
unsigned int uint32_t
Definition: example_types.h:8
ROSCPP_DECL void init(int &argc, char **argv, const std::string &name, uint32_t options=0)
ifm_o3m_float32_t Z[1024]
Definition: DI_CH08_1_2.h:30
const uint16_t confidence32
ifm_o3m_AlgoIFOutput_DIA1_1_2 * ifm_o3m_ConvertBufferToLittleEndian_DIA1_1_2(void *buffer, ifm_o3m_uint32_t bufferSize)
Definition: DI_CH08_1_2.cpp:12
uint32_t EndDelimiter
ifm_o3m_uint16_t sensorWidth
Definition: DI_CH08_1_1.h:25
uint32_t CycleCounter
ifm_o3m_uint32_t available
Definition: DI_CH08_1_1.h:36
struct ifm_o3m_AlgoIFOutput_DIA1_1_0::@0 distanceImageResult
bool init(int port, std::string localInterface)
Definition: udpreceiver.cpp:33
ifm_o3m_uint16_t confidence[1024]
Definition: DI_CH08_1_1.h:31
ifm_o3m_uint16_t sensorHeight
Definition: DI_CH08_1_0.h:21
ifm_o3m_uint16_t confidence[1024]
Definition: DI_CH08_1_0.h:26
signed char int8_t
Definition: example_types.h:5
uint16_t IndexOfPacketInCycle
ifm_o3m_uint16_t confidence[1024]
Definition: DI_CH08_1_2.h:31
struct ifm_o3m_AlgoIFOutput_DIA1_1_0::@0::@2 cameraCalibration
ifm_o3m_AlgoIFOutput_DIA1_1_1 * ifm_o3m_ConvertBufferToLittleEndian_DIA1_1_1(void *buffer, ifm_o3m_uint32_t bufferSize)
Definition: DI_CH08_1_1.cpp:12
struct ifm_o3m_AlgoIFOutput_DIA1_1_3::@28 distanceImageResult
const uint16_t confidence8
ifm_o3m_float32_t Y[1024]
Definition: DI_CH08_1_0.h:24
ifm_o3m_uint16_t sensorWidth
Definition: DI_CH08_1_2.h:25
#define UDP_PACKET_BUF_LEN
struct ifm_o3m_AlgoIFOutput_DIA1_1_2::@17 distanceImageResult
uint16_t NumberOfPacketsInCycle
ifm_o3m_float32_t X[1024]
Definition: DI_CH08_1_2.h:28
ifm_o3m_uint32_t available
Definition: DI_CH08_1_2.h:36
ifm_o3m_uint16_t sensorHeight
Definition: DI_CH08_1_3.h:46
bool param(const std::string &param_name, T &param_val, const T &default_val) const
const std::string TYPE_32FC1
uint8_t reserved[24]
ifm_o3m_float32_t Z[1024]
Definition: DI_CH08_1_1.h:30
const uint16_t confidence4096
uint32_t PacketCounter
ifm_o3m_uint32_t available
Definition: DI_CH08_1_3.h:172
ifm_o3m_uint16_t amplitude[1024]
Definition: DI_CH08_1_1.h:32
Publisher advertise(const std::string &topic, uint32_t queue_size, bool latch=false)
ifm_o3m_float32_t Z[1024]
Definition: DI_CH08_1_3.h:95
ifm_o3m_float32_t amplitude_normalization[4]
Definition: DI_CH08_1_1.h:33
ifm_o3m_float32_t Z[1024]
Definition: DI_CH08_1_0.h:25
const uint16_t confidence1024
ifm_o3m_float32_t Y[1024]
Definition: DI_CH08_1_2.h:29
ifm_o3m_AlgoIFOutput_DIA1_1_3 * ifm_o3m_ConvertBufferToLittleEndian_DIA1_1_3(void *buffer, ifm_o3m_uint32_t bufferSize)
Definition: DI_CH08_1_3.cpp:12
unsigned char uint8_t
Definition: example_types.h:4
struct ifm_o3m_AlgoIFOutput_DIA1_1_3::@28::@31 cameraCalibration
const uint16_t confidence4
int processChannel8(void *buf, uint32_t size)
ifm_o3m_uint16_t sensorHeight
Definition: DI_CH08_1_1.h:26
uint16_t NumberOfPacketsInChannel
ifm_o3m_uint16_t distanceData[1024]
Definition: DI_CH08_1_0.h:22
ifm_o3m_uint16_t confidence[1024]
Definition: DI_CH08_1_3.h:117
int copyChannel8_DIA1_1_0(ifm_o3m_AlgoIFOutput_DIA1_1_0 *p)
uint32_t StartDelimiter
int main(int argc, char **argv)
struct ifm_o3m_AlgoIFOutput_DIA1_1_1::@6 distanceImageResult
ifm_o3m_uint16_t amplitude[1024]
Definition: DI_CH08_1_0.h:27
const uint16_t confidence2
void toROSMsg(const sensor_msgs::PointCloud2 &cloud, sensor_msgs::Image &image)
ifm_o3m_uint16_t amplitude[1024]
Definition: DI_CH08_1_3.h:127
int copyChannel8_DIA1_1_2(ifm_o3m_AlgoIFOutput_DIA1_1_2 *p)
const uint16_t confidence64
static Time now()
struct ifm_o3m_AlgoIFOutput_DIA1_1_1::@6::@8 cameraCalibration
ifm_o3m_float32_t Y[1024]
Definition: DI_CH08_1_3.h:82
ifm_o3m_uint16_t amplitude[1024]
Definition: DI_CH08_1_2.h:32
ifm_o3m_float32_t X[1024]
Definition: DI_CH08_1_3.h:69
bool ok() const
#define RESULT_ERROR
int processPacket(int8_t *currentPacketData, int currentPacketSize, int8_t *channelBuffer, uint32_t channelBufferSize, uint32_t *pos)
uint32_t TotalLengthOfChannel
ifm_o3m_float32_t amplitude_normalization[4]
Definition: DI_CH08_1_3.h:140
const uint16_t confidence256
uint32_t LengthPayload
Length
const uint16_t confidence128
ifm_o3m_float32_t amplitude_normalization[4]
Definition: DI_CH08_1_0.h:28
float float32_t
Definition: example_types.h:10
ROSCPP_DECL void spinOnce()
#define ROS_ERROR(...)
#define RESULT_OK
int copyChannel8_DIA1_1_1(ifm_o3m_AlgoIFOutput_DIA1_1_1 *p)
uint16_t IndexOfPacketInChannel
const uint16_t confidence16
ifm_o3m_AlgoIFOutput_DIA1_1_0 * ifm_o3m_ConvertBufferToLittleEndian_DIA1_1_0(void *buffer, ifm_o3m_uint32_t bufferSize)
Definition: DI_CH08_1_0.cpp:12
ifm_o3m_uint16_t distanceData[1024]
Definition: DI_CH08_1_1.h:27
struct PacketHeader __attribute__((packed))
const uint16_t confidence2048
unsigned short uint16_t
Definition: example_types.h:6
int copyChannel8_DIA1_1_3(ifm_o3m_AlgoIFOutput_DIA1_1_3 *p)
ifm_o3m_float32_t X[1024]
Definition: DI_CH08_1_0.h:23
ifm_o3m_uint32_t available
Definition: DI_CH08_1_0.h:31


ifm_o3mxxx
Author(s):
autogenerated on Mon Jun 10 2019 13:34:12