power_node_simulator.cpp
Go to the documentation of this file.
1 /*
2  * Copyright (c) 2008, Willow Garage, Inc.
3  * All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions are met:
7  *
8  * * Redistributions of source code must retain the above copyright
9  * notice, this list of conditions and the following disclaimer.
10  * * Redistributions in binary form must reproduce the above copyright
11  * notice, this list of conditions and the following disclaimer in the
12  * documentation and/or other materials provided with the distribution.
13  * * Neither the name of the Willow Garage, Inc. nor the names of its
14  * contributors may be used to endorse or promote products derived from
15  * this software without specific prior written permission.
16  *
17  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27  * POSSIBILITY OF SUCH DAMAGE.
28  */
29 
30 #include <stdio.h>
31 #include <stdint.h>
32 #include <string.h>
33 #include <unistd.h>
34 #include <stdlib.h>
35 #include <sys/select.h>
36 #include <sys/types.h>
37 #include <assert.h>
38 #include <errno.h>
39 #include <signal.h>
40 #include <vector>
41 #include <sstream>
42 #include <iostream>
43 #include <boost/thread/thread.hpp>
44 
45 // Internet/Socket stuff
46 #include <sys/types.h>
47 #include <sys/socket.h>
48 #include <netinet/in.h>
49 #include <arpa/inet.h>
50 #include <sys/socket.h>
51 #include <net/if.h>
52 #include <sys/ioctl.h>
53 
54 #include "power_comm.h"
55 #include "power_node.h"
56 #include "diagnostic_msgs/DiagnosticMessage.h"
59 #include "ros/console.h"
60 
61 using namespace std;
62 
63 // Keep a pointer to the last message recieved for
64 // Each board.
65 static std::vector<Device*> Devices;
67 
68 void processSentMessage(CommandMessage *cmd);
69 
70 void Device::setTransitionMessage(const TransitionMessage &newtmsg)
71 {
72  if (tmsgset)
73  {
74 #define PRINT_IF_CHANGED(val) \
75 for (int counter = 0; counter < 3; counter++) \
76 { \
77  if (tmsg.cb[counter].val##_count != newtmsg.cb[counter].val##_count) \
78  { \
79  ROS_INFO("Power board: CB%i "#val" event count changed to %i.", counter, newtmsg.cb[counter].val##_count);\
80  } \
81 }
82 
83  PRINT_IF_CHANGED(stop);
84  PRINT_IF_CHANGED(estop);
85  PRINT_IF_CHANGED(trip);
86  PRINT_IF_CHANGED(fail_18V);
87  PRINT_IF_CHANGED(disable);
88  PRINT_IF_CHANGED(start);
89  PRINT_IF_CHANGED(pump_fail);
90  PRINT_IF_CHANGED(reset);
91 
92 #undef PRINT_IF_CHANGED
93  }
94  else
95  tmsgset = 1;
96 
97  tmsg = newtmsg;
98 }
99 
100 void Device::setPowerMessage(const PowerMessage &newpmsg)
101 {
102  if (pmsgset)
103  {
104 #define PRINT_IF_CHANGED(val) \
105 if (pmsg.status.val##_status != newpmsg.status.val##_status) \
106 { \
107  ROS_INFO("Power board: status of "#val" changed to %i.", newpmsg.status.val##_status); \
108 }
109 
110  PRINT_IF_CHANGED(CB0);
111  PRINT_IF_CHANGED(CB1);
112  PRINT_IF_CHANGED(CB2);
113  PRINT_IF_CHANGED(estop_button);
114  PRINT_IF_CHANGED(estop);
115 
116 #undef PRINT_IF_CHANGED
117  }
118  else
119  pmsgset = 1;
120 
121  pmsg = newpmsg;
122 }
123 
124 int PowerBoard::send_command(unsigned int serial_number, int circuit_breaker, const std::string &command, unsigned flags)
125 {
126  if (Devices.size() == 0) {
127  fprintf(stderr,"No devices to send command to\n");
128  return -1;
129  }
130 
131  int selected_device = -1;
132  // Look for device serial number in list of devices...
133  for (unsigned i = 0; i<Devices.size(); ++i) {
134  if (Devices[i]->getPowerMessage().header.serial_num == serial_number) {
135  selected_device = i;
136  break;
137  }
138  }
139 
140  if ((selected_device < 0) || (selected_device >= (int)Devices.size())) {
141  fprintf(stderr, "Device number must be between 0 and %u\n", Devices.size()-1);
142  return -1;
143  }
144 
145  Device* device = Devices[selected_device];
146  assert(device != NULL);
147 
148 
149  if ((circuit_breaker < 0) || (circuit_breaker > 2)) {
150  fprintf(stderr, "Circuit breaker number must be between 0 and 2\n");
151  return -1;
152  }
153 
154  ROS_DEBUG("circuit=%d command=%s flags=%x\n", circuit_breaker, command.c_str(), flags);
155 
156  // Determine what command to send
157  char command_enum = NONE;
158  if (command == "start") {
159  command_enum = COMMAND_START;
160  }
161  else if (command == "stop") {
162  command_enum = COMMAND_STOP;
163  }
164  else if (command == "reset") {
165  command_enum = COMMAND_RESET;
166  }
167  else if (command == "disable") {
168  command_enum = COMMAND_DISABLE;
169  }
170  else if (command == "none") {
171  command_enum = NONE;
172  }
173  /*else if (command == "terrible_hack_shutdown") {
174  exit(0);
175  }*/
176  else {
177  ROS_ERROR("invalid command '%s'", command.c_str());
178  return -1;
179  }
180  //" -c : command to send to device : 'start', 'stop', 'reset', 'disable'\n"
181 
182 
183  // Build command message
184  CommandMessage cmdmsg;
185  memset(&cmdmsg, 0, sizeof(cmdmsg));
186  cmdmsg.header.message_revision = CURRENT_MESSAGE_REVISION;
187  cmdmsg.header.message_id = MESSAGE_ID_COMMAND;
188  cmdmsg.header.serial_num = device->getPowerMessage().header.serial_num;
189  //cmdmsg.header.serial_num = 0x12345678;
190  strncpy(cmdmsg.header.text, "power command message", sizeof(cmdmsg.header.text));
191  cmdmsg.command.CB0_command = NONE;
192  cmdmsg.command.CB1_command = NONE;
193  cmdmsg.command.CB2_command = NONE;
194  if (circuit_breaker==0) {
195  cmdmsg.command.CB0_command = command_enum;
196  }
197  else if (circuit_breaker==1) {
198  cmdmsg.command.CB1_command = command_enum;
199  }
200  else if (circuit_breaker==2) {
201  cmdmsg.command.CB2_command = command_enum;
202  }
203  else if (circuit_breaker==-1) {
204  cmdmsg.command.CB0_command = command_enum;
205  cmdmsg.command.CB1_command = command_enum;
206  cmdmsg.command.CB2_command = command_enum;
207  }
208 
209  cmdmsg.command.flags = flags;
210 
211  errno = 0;
212 
213  processSentMessage(&cmdmsg);
214 
215  ROS_DEBUG("Send to Serial=%u, revision=%u", cmdmsg.header.serial_num, cmdmsg.header.message_revision);
216  ROS_DEBUG("Sent command %s(%d) to device %d, circuit %d",
217  command.c_str(), command_enum, selected_device, circuit_breaker);
218 
219  return 0;
220 }
221 
222 
223 const char* PowerBoard::cb_state_to_str(char state)
224 {
225  //enum CB_State { STATE_NOPOWER, STATE_STANDBY, STATE_PUMPING, STATE_ON, STATE_DISABLED };
226  switch(state) {
227  case STATE_NOPOWER:
228  return "no-power";
229  case STATE_STANDBY:
230  return "Standby";
231  case STATE_PUMPING:
232  return "pumping";
233  case STATE_ON:
234  return "On";
235  case STATE_DISABLED:
236  return "Disabled";
237  }
238  return "???";
239 }
240 
241 const char* PowerBoard::master_state_to_str(char state)
242 {
243  //enum CB_State { STATE_NOPOWER, STATE_STANDBY, STATE_PUMPING, STATE_ON, STATE_DISABLED };
244  switch(state) {
245  case MASTER_NOPOWER:
246  return "no-power";
247  case MASTER_STANDBY:
248  return "stand-by";
249  case MASTER_ON:
250  return "on";
251  case MASTER_OFF:
252  return "off";
253  }
254  return "???";
255 }
256 
257 
258 // Determine if a record of the device already exists...
259 // If it does use newest message a fill in pointer to old one .
260 // If it does not.. use
261 int PowerBoard::process_message(const PowerMessage *msg)
262 {
263  if (msg->header.message_revision != CURRENT_MESSAGE_REVISION) {
264  ROS_WARN("Got message with incorrect message revision %u\n", msg->header.message_revision);
265  return -1;
266  }
267 
268  // Look for device serial number in list of devices...
269  for (unsigned i = 0; i<Devices.size(); ++i) {
270  if (Devices[i]->getPowerMessage().header.serial_num == msg->header.serial_num) {
271  boost::mutex::scoped_lock(library_lock_);
272  Devices[i]->message_time = ros::Time::now();
273  Devices[i]->setPowerMessage(*msg);
274  return 0;
275  }
276  }
277 
278  // Add new device to list
279  Device *newDevice = new Device();
280  Devices.push_back(newDevice);
281  newDevice->message_time = ros::Time::now();
282  newDevice->setPowerMessage(*msg);
283  return 0;
284 }
285 
286 int PowerBoard::process_transition_message(const TransitionMessage *msg)
287 {
288  if (msg->header.message_revision != CURRENT_MESSAGE_REVISION) {
289  ROS_WARN("Got message with incorrect message revision %u\n", msg->header.message_revision);
290  return -1;
291  }
292 
293  // Look for device serial number in list of devices...
294  for (unsigned i = 0; i<Devices.size(); ++i) {
295  if (Devices[i]->getPowerMessage().header.serial_num == msg->header.serial_num) {
296  boost::mutex::scoped_lock(library_lock_);
297  Devices[i]->setTransitionMessage(*msg);
298  return 0;
299  }
300  }
301 
302  // Add new device to list
303  Device *newDevice = new Device();
304  Devices.push_back(newDevice);
305  newDevice->setTransitionMessage(*msg);
306  return 0;
307 }
308 
309 PowerBoard::PowerBoard( unsigned int serial_number ): ros::Node ("pr2_power_board")
310 {
311 
313  log4cxx::LoggerPtr my_logger = log4cxx::Logger::getLogger(ROSCONSOLE_DEFAULT_NAME);
314 
315  if( my_logger->getLevel() == 0 ) //has anyone set our level??
316  {
317  // Set the ROS logger
318  my_logger->setLevel(ros::console::g_level_lookup[ros::console::levels::Info]);
319  }
320 
321  advertiseService("power_board_control", &PowerBoard::commandCallback);
322  advertise<diagnostic_msgs::DiagnosticMessage>("/diagnostics", 2);
323 }
324 
325 bool PowerBoard::commandCallback(pr2_power_board::PowerBoardCommand::Request &req_,
326  pr2_power_board::PowerBoardCommand::Response &res_)
327 {
328  res_.retval = send_command( req_.serial_number, req_.breaker_number, req_.command, req_.flags);
329 
330  return true;
331 }
332 
333 void PowerBoard::sendDiagnostic()
334 {
335  ros::Rate r(1);
336  while(node_handle.ok())
337  {
338  r.sleep();
339  //ROS_DEBUG("-");
340  boost::mutex::scoped_lock(library_lock_);
341 
342  for (unsigned i = 0; i<Devices.size(); ++i)
343  {
344  diagnostic_msgs::DiagnosticArray msg_out;
346 
347  Device *device = Devices[i];
348  const PowerMessage *pmesg = &device->getPowerMessage();
349 
350  ostringstream ss;
351  ss << "Power board " << pmesg->header.serial_num;
352  stat.name = ss.str();
353 
354  if( (ros::Time::now() - device->message_time) > TIMEOUT )
355  {
356  stat.summary(2, "No Updates");
357  }
358  else
359  {
360  stat.summary(0, "Running");
361  }
362  const StatusStruct *status = &pmesg->status;
363 
364  ROS_DEBUG("Device %u", i);
365  ROS_DEBUG(" Serial = %u", pmesg->header.serial_num);
366 
367  stat.add("Serial Number", pmesg->header.serial_num);
368 
369  ROS_DEBUG(" Current = %f", status->input_current);
370  stat.add("Input Current", status->input_current);
371 
372  ROS_DEBUG(" Voltages:");
373  ROS_DEBUG(" Input = %f", status->input_voltage);
374  stat.add("Input Voltage", status->input_voltage);
375 
376  ROS_DEBUG(" DCDC 12 = %f", status->DCDC_12V_out_voltage);
377  stat.add("DCDC12", status->DCDC_12V_out_voltage);
378 
379  ROS_DEBUG(" DCDC 15 = %f", status->DCDC_19V_out_voltage);
380  stat.add("DCDC 15", status->DCDC_19V_out_voltage);
381 
382  ROS_DEBUG(" CB0 (Base) = %f", status->CB0_voltage);
383  stat.add("Breaker 0 Voltage", status->CB0_voltage);
384 
385  ROS_DEBUG(" CB1 (R-arm) = %f", status->CB1_voltage);
386  stat.add("Breaker 1 Voltage", status->CB1_voltage);
387 
388  ROS_DEBUG(" CB2 (L-arm) = %f", status->CB2_voltage);
389  stat.add("Breaker 2 Voltage", status->CB2_voltage);
390 
391  ROS_DEBUG(" Board Temp = %f", status->ambient_temp);
392  stat.add("Board Temperature", status->ambient_temp);
393 
394  ROS_DEBUG(" Fan Speeds:");
395  ROS_DEBUG(" Fan 0 = %u", status->fan0_speed);
396  stat.add("Fan 0 Speed", status->fan0_speed);
397 
398  ROS_DEBUG(" Fan 1 = %u", status->fan1_speed);
399  stat.add("Fan 1 Speed", status->fan1_speed);
400 
401  ROS_DEBUG(" Fan 2 = %u", status->fan2_speed);
402  stat.add("Fan 2 Speed", status->fan2_speed);
403 
404  ROS_DEBUG(" Fan 3 = %u", status->fan3_speed);
405  stat.add("Fan 3 Speed", status->fan3_speed);
406 
407  ROS_DEBUG(" State:");
408  ROS_DEBUG(" CB0 (Base) = %s", cb_state_to_str(status->CB0_state));
409  stat.add("Breaker 0 State", cb_state_to_str(status->CB0_state));
410 
411  ROS_DEBUG(" CB1 (R-arm) = %s", cb_state_to_str(status->CB1_state));
412  stat.add("Breaker 1 State", cb_state_to_str(status->CB1_state));
413 
414  ROS_DEBUG(" CB2 (L-arm) = %s", cb_state_to_str(status->CB2_state));
415  stat.add("Breaker 2 State", cb_state_to_str(status->CB2_state));
416 
417  ROS_DEBUG(" DCDC = %s", master_state_to_str(status->DCDC_state));
418  stat.add("DCDC state", master_state_to_str(status->DCDC_state));
419 
420  ROS_DEBUG(" Status:");
421  ROS_DEBUG(" CB0 (Base) = %s", (status->CB0_status) ? "On" : "Off");
422  stat.add("Breaker 0 Status", (status->CB0_status) ? "On" : "Off");
423 
424  ROS_DEBUG(" CB1 (R-arm) = %s", (status->CB1_status) ? "On" : "Off");
425  stat.add("Breaker 1 Status", (status->CB1_status) ? "On" : "Off");
426 
427  ROS_DEBUG(" CB2 (L-arm) = %s", (status->CB2_status) ? "On" : "Off");
428  stat.add("Breaker 2 Status", (status->CB2_status) ? "On" : "Off");
429 
430  ROS_DEBUG(" estop_button= %x", (status->estop_button_status));
431  stat.add("RunStop Button Status", status->estop_button_status);
432 
433  ROS_DEBUG(" estop_status= %x", (status->estop_status));
434  stat.add("RunStop Status", status->estop_status);
435 
436  ROS_DEBUG(" Revisions:");
437  ROS_DEBUG(" PCA = %c", status->pca_rev);
438  stat.add("PCA", status->pca_rev);
439 
440  ROS_DEBUG(" PCB = %c", status->pcb_rev);
441  stat.add("PCB", status->pcb_rev);
442 
443  ROS_DEBUG(" Major = %c", status->major_rev);
444  stat.add("Major Revision", status->major_rev);
445 
446  ROS_DEBUG(" Minor = %c", status->minor_rev);
447  stat.add("Minor Revision", status->minor_rev);
448 
449  stat.add("Min Voltage", status->min_input_voltage);
450  stat.add("Max Current", status->max_input_current);
451 
452  const TransitionMessage *tmsg = &device->getTransitionMessage();
453  for(int cb_index=0; cb_index < 3; ++cb_index)
454  {
455  const TransitionCount *trans = &tmsg->cb[cb_index];
456  ROS_DEBUG("Transition: CB%d", cb_index);
457  ss.str("");
458  ss << "CB" << cb_index << " Stop Count";
459  stat.add(ss.str(), trans->stop_count);
460 
461  ss.str("");
462  ss << "CB" << cb_index << " E-Stop Count";
463  stat.add(ss.str(), trans->estop_count);
464 
465  ss.str("");
466  ss << "CB" << cb_index << " Trip Count";
467  stat.add(ss.str(), trans->trip_count);
468 
469  ss.str("");
470  ss << "CB" << cb_index << " 18V Fail Count";
471  stat.add(ss.str(), trans->fail_18V_count);
472 
473  ss.str("");
474  ss << "CB" << cb_index << " Disable Count";
475  stat.add(ss.str(), trans->disable_count);
476 
477  ss.str("");
478  ss << "CB" << cb_index << " Start Count";
479  stat.add(ss.str(), trans->start_count);
480 
481  ss.str("");
482  ss << "CB" << cb_index << " Pump Fail Count";
483  stat.add(ss.str(), trans->pump_fail_count);
484 
485  ss.str("");
486  ss << "CB" << cb_index << " Reset Count";
487  stat.add(ss.str(), trans->reset_count);
488  }
489 
490  msg_out.status.push_back(stat);
491 
492  //ROS_DEBUG("Publishing ");
493  pub.publish(msg_out);
494  }
495 
496  }
497 }
498 
500 {
501  myBoard->sendDiagnostic();
502 }
503 
504 void CloseAllDevices(void) {
505  for (unsigned i=0; i<Devices.size(); ++i){
506  if (Devices[i] != NULL) {
507  delete Devices[i];
508  }
509  }
510 }
511 
512 
513 // Build list of interfaces
515 {
516  return 0;
517 }
518 
520 {
521  static PowerMessage pm;
522 
523  pm.header.message_revision = CURRENT_MESSAGE_REVISION; //32 bit
524  pm.header.serial_num = 0xDADABABA; //32 bit Unique ID number
525  //pm.header.text[32]; //Description identifier
526  pm.header.message_id = MESSAGE_ID_POWER;
527  pm.header.data_length = sizeof(pm.status); //Length of the following structure
528 
529  /*pm.status.CB0_state; // CB_State enum
530  pm.status.CB1_state;
531  pm.status.CB2_state;
532  pm.status.DCDC_state; // Master_State enum
533 
534  pm.status.input_voltage;
535  pm.status.input_current;
536  pm.status.DCDC_12V_out_voltage;
537  pm.status.DCDC_19V_out_voltage;
538  pm.status.CB0_voltage;
539  pm.status.CB1_voltage;
540  pm.status.CB2_voltage;
541  pm.status.ambient_temp;
542  pm.status.fan0_speed;
543  pm.status.fan1_speed;
544  pm.status.fan2_speed;
545  pm.status.fan3_speed;
546  pm.status.CB0_status; //0-off 1-on
547  pm.status.CB1_status;
548  pm.status.CB2_status;
549  pm.status.estop_button_status;
550  pm.status.estop_status;
551  pm.status.pca_rev;
552  pm.status.pcb_rev;
553  pm.status.major_rev;
554  pm.status.minor_rev;
555  pm.status.min_input_voltage;
556  pm.status.max_input_current;*/
557 
558  int i = 0;
559  while (myBoard->ok())
560  {
561  i++;
562  if (i % 200 < 100) // Alternate between sending and not sending messages to test the display.
563  {
564  myBoard->process_message(&pm);
565  ROS_INFO("Sent message");
566  }
567  else
568  ROS_INFO("Idling");
569  ros::Duration(0,1e8).sleep();
570  }
571 }
572 
573 void processSentMessage(CommandMessage *cmd)
574 {
575  ROS_DEBUG("processSentMessage");
576 }
577 
578 int main(int argc, char** argv)
579 {
580  ros::init(argc, argv);
581 
583  myBoard = new PowerBoard();
584 
585  boost::thread sendThread( &sendMessages );
586  boost::thread fakeDeviceThread( &generateDeviceMessages );
587 
588  myBoard->spin(); //wait for ros to shut us down
589 
590  sendThread.join();
591 
592  CloseAllDevices();
593 
594 
595  delete myBoard;
596  return 0;
597 
598 }
static std::vector< Device * > Devices
const char * master_state_to_str(char state)
Definition: power_node.cpp:417
void summary(unsigned char lvl, const std::string s)
void CloseAllDevices(void)
ROSCPP_DECL void init(int &argc, char **argv, const std::string &name, uint32_t options=0)
#define ROSCONSOLE_AUTOINIT
#define PRINT_IF_CHANGED(val)
int main(int argc, char **argv)
int send_command(unsigned int serial_number, int circuit_breaker, const std::string &command, unsigned flags)
Definition: power_node.cpp:285
#define ROS_WARN(...)
ros::Time message_time
Definition: power_node.h:30
static const unsigned MESSAGE_ID_COMMAND
Definition: power_comm.h:43
void generateDeviceMessages()
void sendMessages()
Definition: power_node.cpp:680
void setPowerMessage(const PowerMessage &newpmsg)
Definition: power_node.cpp:108
#define ROS_INFO(...)
static const ros::Duration TIMEOUT
Definition: power_node.cpp:75
ros::NodeHandle node_handle
Definition: power_node.h:72
static PowerBoard * myBoard
int CreateAllInterfaces(void)
static const unsigned MESSAGE_ID_POWER
Definition: power_comm.h:42
const TransitionMessage & getTransitionMessage()
Definition: power_node.h:32
const char * cb_state_to_str(char state)
Definition: power_node.cpp:399
boost::mutex library_lock_
Definition: power_node.h:79
bool commandCallback(pr2_power_board::PowerBoardCommand::Request &req_, pr2_power_board::PowerBoardCommand::Response &res_)
Definition: power_node.cpp:663
const PowerMessage & getPowerMessage()
Definition: power_node.h:38
void setTransitionMessage(const TransitionMessage &newtmsg)
Definition: power_node.cpp:78
list command
Definition: test_power.py:44
static const unsigned CURRENT_MESSAGE_REVISION
Definition: power_comm.h:33
PowerBoard(const ros::NodeHandle node_handle, unsigned int serial_number=0)
Definition: power_node.cpp:632
static Time now()
void add(const std::string &key, const T &val)
bool sleep() const
bool ok() const
void processSentMessage(CommandMessage *cmd)
#define ROS_ERROR(...)
#define ROS_DEBUG(...)
int process_message(const PowerMessage *msg, int len)
Definition: power_node.cpp:439
int process_transition_message(const TransitionMessage *msg, int len)
Definition: power_node.cpp:484


pr2_power_board
Author(s): Curt Meyers, Blaise Gassend
autogenerated on Wed Mar 8 2023 03:37:51