laptop_battery.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 
3 # Software License Agreement (BSD License)
4 #
5 # Copyright (c) 2011, Willow Garage, Inc.
6 # All rights reserved.
7 #
8 # Redistribution and use in source and binary forms, with or without
9 # modification, are permitted provided that the following conditions
10 # are met:
11 #
12 # * Redistributions of source code must retain the above copyright
13 # notice, this list of conditions and the following disclaimer.
14 # * Redistributions in binary form must reproduce the above
15 # copyright notice, this list of conditions and the following
16 # disclaimer in the documentation and/or other materials provided
17 # with the distribution.
18 # * Neither the name of the Willow Garage nor the names of its
19 # contributors may be used to endorse or promote products derived
20 # from this software without specific prior written permission.
21 #
22 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
23 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
24 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
25 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
26 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
27 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
28 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
30 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
31 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
32 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
33 # POSSIBILITY OF SUCH DAMAGE.
34 
35 ##\author Kevin Watts
36 ##\brief Monitors laptop battery status
37 
38 from __future__ import division
39 
40 from collections import deque
41 import threading
42 import copy
43 import yaml
44 import math
45 import rospy
46 import socket
47 import os # to check path existence
48 import exceptions
49 
50 from sensor_msgs.msg import BatteryState
51 from diagnostic_msgs.msg import DiagnosticStatus, DiagnosticArray, KeyValue
52 
53 def _strip_Ah(raw_val):
54  if 'mAh' in raw_val:
55  rv = float(raw_val.rstrip('mAh').strip()) / 1000.0
56  elif 'Ah' in raw_val:
57  rv = float(raw_val.rstrip('Ah').strip())
58  elif 'mWh' in raw_val:
59  rv = float(raw_val.rstrip('mWh').strip()) / 1000.0
60  elif 'Wh' in raw_val:
61  rv = float(raw_val.rstrip('Wh').strip())
62  else:
63  raise Exception('Value %s did not have supported units. (mAh,Ah,mWh,Wh)' % raw_val)
64  return rv
65 
66 def _strip_V(raw_val):
67  if 'mV' in raw_val:
68  rv = float(raw_val.rstrip('mV').strip()) / 1000.0
69  elif 'V' in raw_val:
70  rv = float(raw_val.rstrip('V').strip())
71  else:
72  raise Exception('Value %s did not have "V" or "mV"' % raw_val)
73  return rv
74 
75 def _strip_A(raw_val):
76  if 'mA' in raw_val:
77  rv = float(raw_val.rstrip('mA').strip()) / 1000.0
78  elif 'A' in raw_val:
79  rv = float(raw_val.rstrip('A').strip())
80  elif 'mW' in raw_val:
81  rv = float(raw_val.rstrip('mW').strip()) / 1000.0
82  elif 'W' in raw_val:
83  rv = float(raw_val.rstrip('W').strip())
84  else:
85  raise Exception('Value %s did not have supported units. (mAh,Ah,mWh,Wh)' % raw_val)
86  return rv
87 
88 def slerp(filename):
89  f = open(filename, 'r')
90  data = f.read()
91  f.close()
92  data = data.replace('\t', ' ')
93  return data
94 
95 def _read_string(filename, default=""):
96  if not os.access(filename, os.F_OK):
97  return default
98  f = open(filename, 'r')
99  data = f.read()
100  f.close()
101  return data
102 
103 def _read_number(filename, default=0):
104  try:
105  data = int(_read_string(filename))
106  return data
107  except exceptions.ValueError:
108  return default
109 
110 def _check_battery_info(_battery_acpi_path):
111  if _battery_acpi_path.startswith('/proc'):
112 
113  if os.access(_battery_acpi_path, os.F_OK):
114  o = slerp(_battery_acpi_path+'/info')
115  else:
116  raise NameError(_battery_acpi_path + ' does not exist')
117 
118  batt_info = yaml.load(o)
119  design_capacity = _strip_Ah(batt_info.get('design capacity', '0 mAh'))
120  last_full_capacity = _strip_Ah(batt_info.get('last full capacity', '0 mAh'))
121  else:
122  # Provided as Wh * 10e5
123  if os.path.exists(_battery_acpi_path + '/energy_full_design'):
124  design_capacity = _read_number(_battery_acpi_path + '/energy_full_design')/10e5
125  elif os.path.exists(_battery_acpi_path + '/charge_full_design'):
126  design_capacity = _read_number(_battery_acpi_path + '/charge_full_design')/10e5
127  else:
128  raise NameError(_battery_acpi_path + '/charge_full_design || ' + _battery_acpi_path + '/energy_full_design does not exist')
129 
130  if os.path.exists(_battery_acpi_path + '/energy_full'):
131  last_full_capacity = _read_number(_battery_acpi_path + '/energy_full')/10e5
132  elif os.path.exists(_battery_acpi_path + '/charge_full'):
133  last_full_capacity = _read_number(_battery_acpi_path + '/charge_full')/10e5
134  else:
135  raise NameError(_battery_acpi_path + '/charge_full || ' + _battery_acpi_path + '/energy_full does not exist')
136  return (design_capacity, last_full_capacity)
137 
138 state_to_val = {'charged': BatteryState.POWER_SUPPLY_STATUS_FULL,
139  'full': BatteryState.POWER_SUPPLY_STATUS_FULL,
140  'charging': BatteryState.POWER_SUPPLY_STATUS_CHARGING,
141  'discharging': BatteryState.POWER_SUPPLY_STATUS_DISCHARGING,
142  'unknown': BatteryState.POWER_SUPPLY_STATUS_UNKNOWN, }
143 
144 val_to_state = {BatteryState.POWER_SUPPLY_STATUS_FULL: 'charged',
145  BatteryState.POWER_SUPPLY_STATUS_CHARGING: 'charging',
146  BatteryState.POWER_SUPPLY_STATUS_DISCHARGING: 'discharging',
147  BatteryState.POWER_SUPPLY_STATUS_UNKNOWN: 'unknown', }
148 
149 diag_level_to_msg = { DiagnosticStatus.OK: 'OK',
150  DiagnosticStatus.WARN: 'Warning',
151  DiagnosticStatus.ERROR: 'Error' }
152 
153 def _check_battery_state(_battery_acpi_path):
154  """
155  @return BatteryState
156  """
157  rv = BatteryState()
158 
159  if _battery_acpi_path.startswith('/proc'):
160 
161  if os.access(_battery_acpi_path, os.F_OK):
162  o = slerp(_battery_acpi_path+'/state')
163  else:
164  raise Exception(_battery_acpi_path+' does not exist')
165 
166  batt_info = yaml.load(o)
167 
168  state = batt_info.get('charging state', 'discharging')
169  rv.power_supply_status = state_to_val.get(state, 0)
170  rv.current = _strip_A(batt_info.get('present rate', '-1 mA'))
171  if rv.power_supply_status == BatteryState.POWER_SUPPLY_STATUS_DISCHARGING:
172  rv.current = math.copysign(rv.current, -1) # Need to set discharging rate to negative
173 
174  rv.charge = _strip_Ah(batt_info.get('remaining capacity', '-1 mAh')) # /energy_now
175  rv.voltage = _strip_V(batt_info.get('present voltage', '-1 mV')) # /voltage_now
176  rv.present = batt_info.get('present', False) # /present
177 
178  rv.header.stamp = rospy.get_rostime()
179  else:
180 
181  # Charging state; make lowercase and remove trailing eol
182  state = _read_string(_battery_acpi_path+'/status', 'discharging').lower().rstrip()
183  rv.power_supply_status = state_to_val.get(state, 0)
184 
185  if os.path.exists(_battery_acpi_path + '/power_now'):
186  try:
187  rv.current = _read_number(_battery_acpi_path + '/power_now')/10e5 / \
188  _read_number(_battery_acpi_path + '/voltage_now')
189  except IOError:
190  rv.current = 0
191  else:
192  rv.current = _read_number(_battery_acpi_path + '/current_now')/10e5
193 
194  if rv.power_supply_status == BatteryState.POWER_SUPPLY_STATUS_DISCHARGING:
195  rv.current = math.copysign(rv.current, -1) # Need to set discharging rate to negative
196 
197  if os.path.exists(_battery_acpi_path + '/energy_now'):
198  rv.charge = _read_number(_battery_acpi_path + '/energy_now')/10e5
199  else:
200  rv.charge = _read_number(_battery_acpi_path + '/charge_now')/10e5
201 
202  rv.voltage = _read_number(_battery_acpi_path + '/voltage_now')/10e5
203  rv.present = _read_number(_battery_acpi_path + '/present') == 1
204 
205  rv.header.stamp = rospy.get_rostime()
206  return rv
207 
208 def _laptop_charge_to_diag(laptop_msg):
209  rv = DiagnosticStatus()
210  rv.level = DiagnosticStatus.OK
211  rv.message = 'OK'
212  rv.name = 'Laptop Battery'
213  rv.hardware_id = socket.gethostname()
214 
215  if not laptop_msg.present:
216  rv.level = DiagnosticStatus.ERROR
217  rv.message = 'Laptop battery missing'
218 
219  rv.values.append(KeyValue('Voltage (V)', str(laptop_msg.voltage)))
220  rv.values.append(KeyValue('Current (A)', str(laptop_msg.current)))
221  rv.values.append(KeyValue('Charge (Ah)', str(laptop_msg.charge)))
222  rv.values.append(KeyValue('Capacity (Ah)', str(laptop_msg.capacity)))
223  rv.values.append(KeyValue('Design Capacity (Ah)', str(laptop_msg.design_capacity)))
224  rv.values.append(KeyValue('Percentage (%)', str(laptop_msg.percentage)))
225  rv.values.append(KeyValue('Charging state', val_to_state[laptop_msg.power_supply_status]))
226 
227  return rv
228 
229 class LaptopBatteryMonitor(object):
230  def __init__(self):
231  self._mutex = threading.Lock()
232 
235  self._msg = BatteryState()
236 
237  self._power_pub = rospy.Publisher('laptop_charge', BatteryState, latch=True, queue_size=5)
238  self._diag_pub = rospy.Publisher('/diagnostics', DiagnosticArray, queue_size=5)
239 
240  # Battery info
241  self._batt_acpi_path = rospy.get_param('~acpi_path', "/sys/class/power_supply/BAT0")
244  self._last_info_update = 0
245 
246  self._batt_info_rate = 1 / 60.0
247  self._batt_info_thread = threading.Thread(target=self._check_batt_info)
248  self._batt_info_thread.daemon = True
249  self._batt_info_thread.start()
250 
251  # Battery state
252  self._batt_state_rate = 1 / 5.0
253  self._batt_state_thread = threading.Thread(target=self._check_batt_state)
254  self._batt_state_thread.daemon = True
255  self._batt_state_thread.start()
256 
257  def _check_batt_info(self):
258  rate = rospy.Rate(self._batt_info_rate)
259  while not rospy.is_shutdown():
260  try:
261  design_cap, last_full_cap = _check_battery_info(self._batt_acpi_path)
262  with self._mutex:
263  self._batt_last_full_capacity = last_full_cap
264  self._batt_design_capacity = design_cap
265  self._last_info_update = rospy.get_time()
266  except NameError as e:
267  rospy.logwarn('Battery : unable to check laptop battery info [%s]' % e)
268  rospy.signal_shutdown('Battery : unable to check laptop battery info [%s]' % e)
269  rate.sleep()
270 
271  def _check_batt_state(self):
272  rate = rospy.Rate(self._batt_state_rate)
273  while not rospy.is_shutdown():
274  try:
276  with self._mutex:
277  self._msg = msg
278  self._last_state_update = rospy.get_time()
279  except Exception, e:
280  rospy.logwarn('Battery : unable to check laptop battery state [%s]' % e)
281  rospy.signal_shutdown('Battery : unable to check laptop battery state [%s]' % e)
282 
283  rate.sleep()
284 
285  def update(self):
286  with self._mutex:
287  diag = DiagnosticArray()
288  diag.header.stamp = rospy.get_rostime()
289 
290  info_update_ok = rospy.get_time() - self._last_info_update < 5.0 / self._batt_info_rate
291  state_update_ok = rospy.get_time() - self._last_state_update < 5.0 / self._batt_state_rate
292 
293  if info_update_ok:
294  self._msg.design_capacity = self._batt_design_capacity
295  self._msg.capacity = self._batt_last_full_capacity
296  else:
297  self._msg.design_capacity = 0.0
298  self._msg.capacity = 0.0
299 
300  if info_update_ok and state_update_ok and self._msg.capacity != 0:
301  self._msg.percentage = int(self._msg.charge / self._msg.capacity * 100.0)
302 
303  diag_stat = _laptop_charge_to_diag(self._msg)
304  if not info_update_ok or not state_update_ok:
305  diag_stat.level = DiagnosticStatus.ERROR
306  diag_stat.message = 'Laptop battery data stale'
307 
308  diag.status.append(diag_stat)
309 
310  self._diag_pub.publish(diag)
311  self._power_pub.publish(self._msg)
312 
313 if __name__ == '__main__':
314  rospy.init_node('laptop_battery')
315 
317  try:
318  r = rospy.Rate(1.0)
319  while not rospy.is_shutdown():
320  bm.update()
321  r.sleep()
322  except KeyboardInterrupt:
323  pass
324  except Exception:
325  import traceback
326  traceback.print_exc()
def _laptop_charge_to_diag(laptop_msg)
def _read_string(filename, default="")
def _check_battery_info(_battery_acpi_path)
def slerp(filename)
def _check_battery_state(_battery_acpi_path)
def _strip_V(raw_val)
def _read_number(filename, default=0)
def _strip_A(raw_val)
def _strip_Ah(raw_val)


laptop_battery_monitor
Author(s): Daniel Stonier
autogenerated on Sun Nov 17 2019 03:20:09