Skip to content

Commit bcd191f

Browse files
authored
refactor(sensors_monitor): ros2 port #339 (#367)
thanks to https://github.com/reinzor Signed-off-by: Christian Henkel <[email protected]> (cherry picked from commit e67a69c)
1 parent f331ee3 commit bcd191f

File tree

4 files changed

+271
-1
lines changed

4 files changed

+271
-1
lines changed

diagnostic_common_diagnostics/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ ament_python_install_package(${PROJECT_NAME})
1010
install(PROGRAMS
1111
${PROJECT_NAME}/ntp_monitor.py
1212
${PROJECT_NAME}/ram_monitor.py
13+
${PROJECT_NAME}/sensors_monitor.py
1314
DESTINATION lib/${PROJECT_NAME}
1415
)
1516

diagnostic_common_diagnostics/README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,20 @@ warning percentage threshold.
7373
Length of RAM readings queue.
7474

7575
## sensors_monitor.py
76-
**To be ported**
76+
The `sensors_monitor` module allows users to monitor the temperature, volt and fan speeds of their system in real-time.
77+
It uses the [`LM Sensors` package](https://packages.debian.org/sid/utils/lm-sensors) to get the data.
78+
79+
* Name of the node is "sensors_monitor_" + hostname.
80+
81+
### Published Topics
82+
#### /diagnostics
83+
diagnostic_msgs/DiagnosticArray
84+
The diagnostics information.
85+
86+
### Parameters
87+
#### ignore_fans
88+
(default: false)
89+
Whether to ignore the fan speed.
7790

7891
## tf_monitor.py
7992
**To be ported**
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
#!/usr/bin/env python3
2+
3+
# Software License Agreement (BSD License)
4+
#
5+
# Copyright (c) 2012, 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+
from __future__ import division, with_statement
36+
37+
from io import StringIO
38+
import math
39+
import re
40+
import socket
41+
import subprocess
42+
43+
from diagnostic_msgs.msg import DiagnosticStatus
44+
45+
import diagnostic_updater as DIAG
46+
47+
import rclpy
48+
from rclpy.node import Node
49+
50+
51+
class Sensor(object):
52+
53+
def __init__(self):
54+
self.critical = None
55+
self.min = None
56+
self.max = None
57+
self.input = None
58+
self.name = None
59+
self.type = None
60+
self.high = None
61+
self.alarm = None
62+
63+
def __repr__(self):
64+
return 'Sensor object (name: {}, type: {})'.format(self.name, self.type)
65+
66+
def getCrit(self):
67+
return self.critical
68+
69+
def getMin(self):
70+
return self.min
71+
72+
def getMax(self):
73+
return self.max
74+
75+
def getInput(self):
76+
return self.input
77+
78+
def getName(self):
79+
return self.name
80+
81+
def getType(self):
82+
return self.type
83+
84+
def getHigh(self):
85+
return self.high
86+
87+
def getAlarm(self):
88+
return self.alarm
89+
90+
def __str__(self):
91+
lines = []
92+
lines.append(str(self.name))
93+
lines.append('\t' + 'Type: ' + str(self.type))
94+
if self.input:
95+
lines.append('\t' + 'Input: ' + str(self.input))
96+
if self.min:
97+
lines.append('\t' + 'Min: ' + str(self.min))
98+
if self.max:
99+
lines.append('\t' + 'Max: ' + str(self.max))
100+
if self.high:
101+
lines.append('\t' + 'High: ' + str(self.high))
102+
if self.critical:
103+
lines.append('\t' + 'Crit: ' + str(self.critical))
104+
lines.append('\t' + 'Alarm: ' + str(self.alarm))
105+
return '\n'.join(lines)
106+
107+
108+
def parse_sensor_line(line):
109+
sensor = Sensor()
110+
line = line.lstrip()
111+
[name, reading] = line.split(':')
112+
113+
try:
114+
[sensor.name, sensor.type] = name.rsplit(' ', 1)
115+
except ValueError:
116+
return None
117+
118+
if sensor.name == 'Core':
119+
sensor.name = name
120+
sensor.type = 'Temperature'
121+
elif sensor.name.find('Physical id') != -1:
122+
sensor.name = name
123+
sensor.type = 'Temperature'
124+
125+
try:
126+
[reading, params] = reading.lstrip().split('(')
127+
except ValueError:
128+
return None
129+
130+
sensor.alarm = False
131+
if line.find('ALARM') != -1:
132+
sensor.alarm = True
133+
134+
if reading.find('°C') == -1:
135+
sensor.input = float(reading.split()[0])
136+
else:
137+
sensor.input = float(reading.split('°C')[0])
138+
139+
params = params.split(',')
140+
for param in params:
141+
m = re.search('[0-9]+.[0-9]*', param)
142+
if param.find('min') != -1:
143+
sensor.min = float(m.group(0))
144+
elif param.find('max') != -1:
145+
sensor.max = float(m.group(0))
146+
elif param.find('high') != -1:
147+
sensor.high = float(m.group(0))
148+
elif param.find('crit') != -1:
149+
sensor.critical = float(m.group(0))
150+
151+
return sensor
152+
153+
154+
def _rads_to_rpm(rads):
155+
return rads / (2 * math.pi) * 60
156+
157+
158+
def _rpm_to_rads(rpm):
159+
return rpm * (2 * math.pi) / 60
160+
161+
162+
def parse_sensors_output(node: Node, output):
163+
out = StringIO(output if isinstance(output, str) else output.decode('utf-8'))
164+
165+
sensorList = []
166+
for line in out.readlines():
167+
# Check for a colon
168+
if ':' in line and 'Adapter' not in line:
169+
s = None
170+
try:
171+
s = parse_sensor_line(line)
172+
except Exception as exc:
173+
node.get_logger().warn(
174+
'Unable to parse line "%s", due to %s', line, exc
175+
)
176+
if s is not None:
177+
sensorList.append(s)
178+
return sensorList
179+
180+
181+
def get_sensors():
182+
p = subprocess.Popen(
183+
'sensors', stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True
184+
)
185+
(o, e) = p.communicate()
186+
if not p.returncode == 0:
187+
return ''
188+
if not o:
189+
return ''
190+
return o
191+
192+
193+
class SensorsMonitor(object):
194+
195+
def __init__(self, node: Node, hostname):
196+
self.node = node
197+
self.hostname = hostname
198+
self.ignore_fans = node.declare_parameter('ignore_fans', False).value
199+
node.get_logger().info('Ignore fanspeed warnings: %s' % self.ignore_fans)
200+
201+
self.updater = DIAG.Updater(node)
202+
self.updater.setHardwareID('none')
203+
self.updater.add('%s Sensor Status' % self.hostname, self.monitor)
204+
205+
def monitor(self, stat):
206+
try:
207+
stat.summary(DiagnosticStatus.OK, 'OK')
208+
for sensor in parse_sensors_output(self.node, get_sensors()):
209+
if sensor.getType() == 'Temperature':
210+
if sensor.getInput() > sensor.getCrit():
211+
stat.mergeSummary(
212+
DiagnosticStatus.ERROR, 'Critical Temperature'
213+
)
214+
elif sensor.getInput() > sensor.getHigh():
215+
stat.mergeSummary(DiagnosticStatus.WARN, 'High Temperature')
216+
stat.add(
217+
' '.join([sensor.getName(), sensor.getType()]),
218+
str(sensor.getInput()),
219+
)
220+
elif sensor.getType() == 'Voltage':
221+
if sensor.getInput() < sensor.getMin():
222+
stat.mergeSummary(DiagnosticStatus.ERROR, 'Low Voltage')
223+
elif sensor.getInput() > sensor.getMax():
224+
stat.mergeSummary(DiagnosticStatus.ERROR, 'High Voltage')
225+
stat.add(
226+
' '.join([sensor.getName(), sensor.getType()]),
227+
str(sensor.getInput()),
228+
)
229+
elif sensor.getType() == 'Speed':
230+
if not self.ignore_fans:
231+
if sensor.getInput() < sensor.getMin():
232+
stat.mergeSummary(DiagnosticStatus.ERROR, 'No Fan Speed')
233+
stat.add(
234+
' '.join([sensor.getName(), sensor.getType()]),
235+
str(sensor.getInput()),
236+
)
237+
except Exception:
238+
import traceback
239+
240+
self.node.get_logger().error('Unable to process lm-sensors data')
241+
self.node.get_logger().error(traceback.format_exc())
242+
return stat
243+
244+
245+
if __name__ == '__main__':
246+
rclpy.init()
247+
hostname = socket.gethostname()
248+
hostname_clean = hostname.translate(hostname.maketrans('-', '_'))
249+
node = rclpy.create_node('sensors_monitor_%s' % hostname_clean)
250+
251+
monitor = SensorsMonitor(node, hostname)
252+
try:
253+
rclpy.spin(node)
254+
except KeyboardInterrupt:
255+
pass

diagnostic_common_diagnostics/package.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
<exec_depend>rclpy</exec_depend>
2222
<exec_depend>diagnostic_updater</exec_depend>
23+
<exec_depend>lm-sensors</exec_depend>
2324
<exec_depend>python3-ntplib</exec_depend>
2425

2526
<test_depend>ament_lint_auto</test_depend>

0 commit comments

Comments
 (0)