File Coverage

blib/lib/RPi/SysInfo.pm
Criterion Covered Total %
statement 144 154 93.5
branch 72 88 81.8
condition 14 21 66.6
subroutine 24 30 80.0
pod 10 10 100.0
total 264 303 87.1


line stmt bran cond sub pod time code
1             package RPi::SysInfo;
2              
3 11     11   401587 use strict;
  11         20  
  11         376  
4 11     11   91 use warnings;
  11         21  
  11         601  
5              
6 11     11   59 use Carp qw(croak);
  11         14  
  11         1184  
7              
8             our $VERSION = '1.02';
9              
10             require XSLoader;
11             XSLoader::load('RPi::SysInfo', $VERSION);
12              
13 11     11   61 use Exporter qw(import);
  11         31  
  11         30291  
14              
15             our @EXPORT_OK = qw(
16             core_temp
17             cpu_percent
18             mem_percent
19             gpio_info
20             raspi_config
21             network_info
22             file_system
23             pi_details
24             pi_model
25             );
26              
27             our %EXPORT_TAGS;
28             $EXPORT_TAGS{all} = [@EXPORT_OK];
29              
30             sub new {
31 0     0 1 0 return bless {}, shift;
32             }
33             sub core_temp {
34 19 50 66 19 1 7825 shift if $_[0] && $_[0] =~ /RPi::/;
35              
36 19         68 my ($degree) = @_;
37              
38 19   100     74 $degree //= 'c';
39              
40 19         43 my $temp = _core_temp_c();
41              
42 19 100       58 return '' if ! defined $temp;
43              
44 18 100 100     74 if ($degree eq 'f' || $degree eq 'F'){
45 9         75 $temp = ($temp * 1.8) + 32;
46             }
47              
48 18         82 chomp $temp;
49 18         123 return $temp;
50             }
51             sub cpu_percent {
52 0     0 1 0 return _format(cpuPercent());
53             }
54             sub gpio_info {
55 11 50 66 11 1 7039 shift if $_[0] && $_[0] =~ /RPi::/;
56              
57 11         35 my ($pins) = @_;
58              
59 11 100       84 $pins = ! defined $pins
60             ? ''
61             : join ",", @$pins;
62              
63             # raspi-gpio was removed from current Raspberry Pi OS in favour of pinctrl,
64             # and never existed for the Pi 5 / RP1. Prefer pinctrl, falling back to
65             # raspi-gpio on older systems that still ship it. Both accept the same
66             # "get [pin[,pin...]]" invocation.
67              
68 11         42 my $tool = _gpio_tool();
69              
70 11 100       67 return '' if ! defined $tool;
71              
72 10         37 my $info = _run("$tool get $pins");
73              
74 10         1204 chomp $info;
75 10         45 return $info;
76             }
77             sub file_system {
78 3     3 1 11 my $fs_info = _run('df') . "\n";
79 3   50     498 $fs_info .= _slurp('/proc/swaps') // '';
80 3         356 return $fs_info;
81             }
82             sub mem_percent {
83 0     0 1 0 return _format(memPercent());
84             }
85             sub network_info {
86              
87             # ifconfig (net-tools) is the legacy default, but modern Raspberry Pi OS
88             # Lite ships without it, so fall back to `ip addr`, which is always present.
89             # Both forms carry inet/inet6 lines.
90              
91 4     4 1 8078 my $tool = _net_tool();
92              
93 4 100       37 return '' if ! defined $tool;
94              
95 3         14 my $netinfo = _run($tool);
96              
97 3         479 chomp $netinfo;
98 3         36 return $netinfo;
99             }
100             sub pi_details {
101              
102 3     3 1 7465 my $details;
103              
104 3         16 $details = "\n"
105             . _run('cat /sys/firmware/devicetree/base/model')
106             . "\n\n"
107             . _run('cat /etc/os-release | head -4')
108             . "\n"
109             . _run('uname -a')
110             . "\n"
111             . _run('cat /proc/cpuinfo | tail -3')
112             . "Board : " . pi_model() . "\n"
113             . "SoC / RAM : " . _board_summary() . "\n"
114             . "Throttled flag : " . _run('vcgencmd get_throttled')
115             . "Camera : " . _camera_info() . "\n";
116              
117 3         15 return $details;
118             }
119             sub pi_model {
120 9 50 33 9 1 214740 shift if $_[0] && $_[0] =~ /RPi::/;
121              
122             # Normalized Raspberry Pi marketing name, e.g. "Raspberry Pi 5 Model B Rev
123             # 1.1". The devicetree model is authoritative on the Pi 0-5, so prefer it,
124             # falling back to a /proc/cpuinfo Revision-code decode, then to 'Unknown'.
125              
126 9         48 my $model = _slurp('/sys/firmware/devicetree/base/model');
127              
128 9 100       893 if (defined $model){
129 7         33 $model =~ s/\0//g; # Devicetree strings are NUL-terminated.
130 7         127 $model =~ s/^\s+|\s+$//g;
131 7 50       68 return $model if length $model;
132             }
133              
134 2         14 my $info = _decode_revision(_cpuinfo_field('Revision'));
135              
136 2 100       17 return $info->{name} if defined $info->{name};
137              
138 1         10 return 'Unknown';
139             }
140             sub raspi_config {
141 3     3 1 12257 my $config = _run('vcgencmd get_config int');
142 3         477 $config .= _run('vcgencmd get_config str');
143              
144             # config.txt moved from /boot to /boot/firmware on Bookworm and later (the
145             # old path now holds only a "this file has moved" stub), so resolve the
146             # real location before appending the user's non-comment directives.
147              
148 3         362 my $config_file = _config_file();
149              
150 3 50       23 if (defined $config_file){
151 3         14 $config .= _run("grep -E -v '^\\s*(#|^\$)' $config_file");
152             }
153              
154 3         335 chomp $config;
155 3         12 return $config;
156             }
157              
158             sub _board_summary {
159             # Human-readable decode of the SoC, RAM and RP1 presence pulled from the
160             # /proc/cpuinfo Revision code. Used to enrich pi_details(). Returns
161             # 'unknown' when the revision can't be decoded.
162              
163 6     6   25 my $info = _decode_revision(_cpuinfo_field('Revision'));
164              
165 6         25 my @parts;
166              
167 6 50       49 push @parts, $info->{soc} if defined $info->{soc};
168 6 50       22 push @parts, $info->{mem} if defined $info->{mem};
169 6 100       18 push @parts, 'RP1' if $info->{rp1};
170 6 50       19 push @parts, $info->{manufacturer} if defined $info->{manufacturer};
171              
172 6 50       60 return @parts ? join(', ', @parts) : 'unknown';
173             }
174             sub _camera_info {
175             # Legacy firmware (Pi 0-4 on Bullseye and earlier) answered
176             # `vcgencmd get_camera` with "supported=N detected=N". On Bookworm and the
177             # Pi 5 that command was removed and camera support moved to libcamera, so
178             # fall back to a libcamera probe. Returns a one-line string (no newline).
179              
180             local $SIG{__WARN__} = sub {
181 0     0   0 my $warning = shift;
182 0 0       0 warn $warning if $warning !~ /Can't exec "vcgencmd"/;
183 7     7   4184 };
184              
185 7         26 my $legacy = _run('vcgencmd get_camera');
186              
187 7 100       357 if ($legacy =~ /supported=/){
188 2         18 $legacy =~ s/\s+$//;
189 2         19 return $legacy;
190             }
191              
192 5         16 my $tool = _camera_tool();
193              
194 5 100       49 if (defined $tool){
195 4         15 my $list = _run("$tool --list-cameras 2>/dev/null");
196              
197 4 100       289 return 'detected (libcamera)' if $list =~ /Available cameras/i;
198 2         24 return 'none detected (libcamera)';
199             }
200              
201 1         27 return 'not detected';
202             }
203             sub _camera_tool {
204             # libcamera's listing utility: rpicam-hello on Bookworm and later, renamed
205             # from the Bullseye-era libcamera-hello.
206              
207 3     3   1591 return _first_tool(qw(rpicam-hello libcamera-hello));
208             }
209             sub _config_file {
210             # Locate the active config.txt. Bookworm and later moved it to
211             # /boot/firmware/config.txt; older systems keep it at /boot/config.txt.
212              
213 0     0   0 for my $file ('/boot/firmware/config.txt', '/boot/config.txt'){
214 0 0       0 return $file if -f $file;
215             }
216              
217 0         0 return undef;
218             }
219             sub _core_temp_c {
220             # Core temperature in Celsius as a number, or undef if unavailable. Prefers
221             # vcgencmd (the value the Pi tooling reports), falling back to the kernel
222             # thermal zone on systems without vcgencmd.
223              
224             local $SIG{__WARN__} = sub {
225 0     0   0 my $warning = shift;
226 0 0       0 warn $warning if $warning !~ /Can't exec "vcgencmd"/;
227 22     22   2833 };
228              
229 22         68 my $temp = _run('vcgencmd measure_temp');
230              
231 22 100       1584 return $1 if $temp =~ /temp=([\d.]+)/;
232              
233             # /sys/class/thermal/thermal_zone0/temp reports millidegrees Celsius.
234              
235 4         7 my $milli = _slurp('/sys/class/thermal/thermal_zone0/temp');
236              
237 4 100 66     32 return $1 / 1000 if defined $milli && $milli =~ /^(\d+)/;
238              
239 2         9 return undef;
240             }
241             sub _cpuinfo_field {
242 15     15   577 my ($field) = @_;
243              
244 15 100       265 croak "_cpuinfo_field() requires a field name\n" if ! defined $field;
245              
246 14         54 my $cpuinfo = _slurp('/proc/cpuinfo');
247              
248 14 100       770 return undef if ! defined $cpuinfo;
249              
250 12         97 for my $line (split /\n/, $cpuinfo){
251 141 100       1295 return $1 if $line =~ /^\Q$field\E\s*:\s*(.+?)\s*$/;
252             }
253              
254 1         7 return undef;
255             }
256             sub _decode_revision {
257 24     24   229919 my ($rev) = @_;
258              
259 24 100       103 return {} if ! defined $rev;
260              
261 22         145 $rev =~ s/^\s+|\s+$//g;
262              
263 22 100       141 return {} if $rev !~ /^[0-9a-fA-F]+$/;
264              
265 19         87 $rev = hex($rev);
266              
267 19         40 my %info;
268              
269 19 100       63 if ($rev & 0x800000){
270             # New-style revision code (Pi 2 and later, plus late Pi 1 boards).
271 17         42 my $type = ($rev >> 4) & 0xff;
272 17         34 my $proc = ($rev >> 12) & 0x0f;
273 17         61 my $mfr = ($rev >> 16) & 0x0f;
274 17         37 my $mem = ($rev >> 20) & 0x07;
275 17         37 my $minor = $rev & 0x0f;
276              
277 17         418 my %types = (
278             0x00 => 'A', 0x01 => 'B', 0x02 => 'A+',
279             0x03 => 'B+', 0x04 => '2 Model B', 0x06 => 'Compute Module 1',
280             0x08 => '3 Model B', 0x09 => 'Zero', 0x0a => 'Compute Module 3',
281             0x0c => 'Zero W', 0x0d => '3 Model B+', 0x0e => '3 Model A+',
282             0x10 => 'Compute Module 3+', 0x11 => '4 Model B',
283             0x12 => 'Zero 2 W', 0x13 => '400', 0x14 => 'Compute Module 4',
284             0x15 => 'Compute Module 4S', 0x17 => '5 Model B',
285             0x18 => 'Compute Module 5', 0x19 => '500',
286             );
287 17         82 my %procs = (
288             0 => 'BCM2835', 1 => 'BCM2836', 2 => 'BCM2837',
289             3 => 'BCM2711', 4 => 'BCM2712',
290             );
291 17         89 my %mfrs = (
292             0 => 'Sony UK', 1 => 'Egoman', 2 => 'Embest',
293             3 => 'Sony Japan', 4 => 'Embest', 5 => 'Stadium',
294             );
295              
296 17         54 $info{new_style} = 1;
297 17         46 $info{type} = $types{$type};
298 17         47 $info{soc} = $procs{$proc};
299 17         37 $info{manufacturer} = $mfrs{$mfr};
300 17         56 $info{revision} = $minor;
301 17         57 $info{mem} = _mem_human(256 << $mem);
302 17 100       75 $info{rp1} = $proc == 4 ? 1 : 0;
303 17 100       207 $info{name} = defined $types{$type}
304             ? "Raspberry Pi $types{$type}"
305             : undef;
306             }
307             else {
308             # Old-style revision code (original Pi 1 / early boards). A small
309             # lookup of the common ones; everything else is left to the devicetree
310             # model. All old-style boards are BCM2835, pre-RP1.
311 2         54 my %old = (
312             0x0002 => ['B', '256MB'], 0x0003 => ['B', '256MB'],
313             0x0004 => ['B', '256MB'], 0x0005 => ['B', '256MB'],
314             0x0006 => ['B', '256MB'], 0x0007 => ['A', '256MB'],
315             0x0008 => ['A', '256MB'], 0x0009 => ['A', '256MB'],
316             0x000d => ['B', '512MB'], 0x000e => ['B', '512MB'],
317             0x000f => ['B', '512MB'], 0x0010 => ['B+', '512MB'],
318             0x0011 => ['Compute Module 1', '512MB'],
319             0x0012 => ['A+', '256MB'], 0x0013 => ['B+', '512MB'],
320             0x0014 => ['Compute Module 1', '512MB'],
321             0x0015 => ['A+', '256MB'],
322             );
323              
324 2         7 $info{new_style} = 0;
325 2         5 $info{soc} = 'BCM2835';
326 2         4 $info{rp1} = 0;
327              
328 2 100       15 if (my $entry = $old{$rev}){
329 1         4 $info{type} = $entry->[0];
330 1         3 $info{mem} = $entry->[1];
331 1         10 $info{name} = "Raspberry Pi $entry->[0]";
332             }
333             }
334              
335 19         73 return \%info;
336             }
337             sub _first_tool {
338             # Returns the first of the given executable names found on PATH, else undef.
339              
340 4     4   2281 for my $tool (@_){
341 5   50     54 for my $dir (split /:/, $ENV{PATH} // ''){
342 4 100       144 return $tool if -x "$dir/$tool";
343             }
344             }
345              
346 2         13 return undef;
347             }
348             sub _format {
349 5 100   5   1073 croak "_format() requires a float/double sent in\n" if ! defined $_[0];
350 4         52 return sprintf("%.2f", $_[0]);
351             }
352             sub _gpio_tool {
353             # Locate a GPIO query tool on PATH. pinctrl is the current Raspberry Pi OS
354             # utility; raspi-gpio is the legacy one kept here as a fallback.
355              
356 3     3   2533 return _first_tool(qw(pinctrl raspi-gpio));
357             }
358             sub _mem_human {
359 25     25   7471 my ($mb) = @_;
360              
361 25 100       98 return undef if ! defined $mb;
362              
363 24 100       292 return $mb % 1024 == 0
364             ? ($mb / 1024) . 'GB'
365             : $mb . 'MB';
366             }
367             sub _net_tool {
368             # Locate a network-interface query command. ifconfig (net-tools) is the
369             # legacy default; fall back to `ip addr` on systems without it.
370              
371 3 100   3   515 return 'ifconfig' if defined _first_tool('ifconfig');
372 2 100       21 return 'ip addr' if defined _first_tool('ip');
373              
374 1         9 return undef;
375             }
376             sub _run {
377 4     4   1052 my ($cmd) = @_;
378              
379 4 100       346 croak "_run() requires a command string\n" if ! defined $cmd;
380              
381 3         19451 my $out = `$cmd`;
382              
383 3 50       269 return defined $out ? $out : '';
384             }
385             sub _slurp {
386 3     3   1663 my ($file) = @_;
387              
388 3 100       145 croak "_slurp() requires a file path\n" if ! defined $file;
389              
390 2 100       97 open my $fh, '<', $file or return undef;
391 1         9 local $/;
392 1         14 my $data = <$fh>;
393 1         8 close $fh;
394              
395 1         16 return $data;
396             }
397              
398             1;
399             __END__