[Perl] DAQ system for the FAPG.
Add role for daq-node.
Changed files
roles/daq-node/bin/fapg-daq-node
@@ -0,0 +1,35 @@
1
Added:
#!/usr/bin/env perl
2
Added:
# -*- mode: perl-ts; -*-
3
Added:
4
Added:
use 5.40.1;
5
Added:
use strict;
6
Added:
use warnings;
7
Added:
8
Added:
use FindBin;
9
Added:
use lib "${FindBin::Bin}/../lib/perl5";
10
Added:
11
Added:
use FAPG::DAQ::EZO;
12
Added:
13
Added:
=head1 NAME
14
Added:
15
Added:
fapg-daq - read sensor data and send over MQTT
16
Added:
17
Added:
=cut
18
Added:
19
Added:
my $probe = FAPG::DAQ::EZO::identify_probe();
20
Added:
21
Added:
print "Found probe "
22
Added:
. $probe->type
23
Added:
. " on "
24
Added:
. $probe->device
25
Added:
. ".\n";
26
Added:
27
Added:
while (1) {
28
Added:
my $reading = $probe->get_reading();
29
Added:
30
Added:
print "Reading: " . $reading->value . " " . $reading->unit . "\n";
31
Added:
32
Added:
$reading->publish_mqtt();
33
Added:
34
Added:
sleep 5;
35
Added:
}
roles/daq-node/deploy
@@ -0,0 +1,190 @@
1
Added:
#!/usr/bin/env bash
2
Added:
3
Added:
set -eEuo pipefail
4
Added:
5
Added:
readonly REPO_DIR="/opt/fapg/fapg-daq"
6
Added:
readonly NODE_BIN="${REPO_DIR}/roles/sensor-node/bin/fapg-daq-node"
7
Added:
8
Added:
readonly SERVICE_NAME="fapg-daq-node.service"
9
Added:
readonly SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}"
10
Added:
11
Added:
readonly ENV_DIR="/etc/fapg-daq"
12
Added:
readonly ENV_FILE="${ENV_DIR}/daq.conf"
13
Added:
14
Added:
readonly STATE_DIR="/var/lib/fapg-daq"
15
Added:
readonly RUN_DIR="/run/fapg-daq"
16
Added:
17
Added:
readonly RUN_USER='fapg-daq'
18
Added:
readonly RUN_GROUP='fapg-daq'
19
Added:
20
Added:
readonly SERIAL_DEVICE='/dev/ttyUSB0'
21
Added:
readonly READ_INTERVAL=5
22
Added:
readonly MQTT_HOST='fapg-daq-five-01'
23
Added:
readonly MQTT_PORT='1883'
24
Added:
readonly MQTT_USERNAME='fapg_zero'
25
Added:
MQTT_PASSWORD=''
26
Added:
27
Added:
function die() {
28
Added:
echo "ERROR: $*" >&2
29
Added:
exit 1
30
Added:
}
31
Added:
32
Added:
function require_root() {
33
Added:
if [[ "$(id -u)" -ne 0 ]]; then
34
Added:
die "$0 requires superuser privileges."
35
Added:
fi
36
Added:
}
37
Added:
38
Added:
function install_os_dependencies() {
39
Added:
local dependencies='perl libnet-mqtt-simple-perl libdevice-serialport-perl'
40
Added:
local missing_dependencies=''
41
Added:
42
Added:
for dep in ${dependencies}; do
43
Added:
if dpkg --get-selections "${dep}" | grep -w install > /dev/null 2>&1; then
44
Added:
echo "${dep} is already installed."
45
Added:
else
46
Added:
echo "${dep} is not installed."
47
Added:
missing_dependencies+=" ${dep}"
48
Added:
fi
49
Added:
done
50
Added:
51
Added:
if [[ -n "${missing_dependencies}" ]]; then
52
Added:
apt-get install -y ${missing_dependencies}
53
Added:
fi
54
Added:
55
Added:
echo "Installed all OS dependencies."
56
Added:
}
57
Added:
58
Added:
# function install_perl_dependencies() {
59
Added:
# local dependencies="Net::MQTT::Simple"
60
Added:
# for dep in ${dependencies}; do
61
Added:
# if ! perl -M"${dep}" -e '1' > /dev/null 2>&1; then
62
Added:
# cpanm "${dep}"
63
Added:
# fi
64
Added:
# done
65
Added:
66
Added:
# echo "Installed all Perl dependencies."
67
Added:
# }
68
Added:
69
Added:
create_runtime_user() {
70
Added:
if ! getent group "${RUN_GROUP}" > /dev/null; then
71
Added:
addgroup --system "${RUN_GROUP}"
72
Added:
fi
73
Added:
74
Added:
if ! id "${RUN_USER}" > /dev/null 2>&1; then
75
Added:
adduser \
76
Added:
--system \
77
Added:
--ingroup "${RUN_GROUP}" \
78
Added:
--home "${STATE_DIR}" \
79
Added:
--no-create-home \
80
Added:
--disabled-login \
81
Added:
"${RUN_USER}"
82
Added:
fi
83
Added:
84
Added:
# USB serial devices are normally accessible to members of dialout.
85
Added:
usermod -aG dialout "${RUN_USER}"
86
Added:
87
Added:
echo "Created runtime user ${RUN_USER}, group ${RUN_GROUP}."
88
Added:
}
89
Added:
90
Added:
check_repo_layout() {
91
Added:
[[ -d "${REPO_DIR}" ]] || die "Repo not found at ${REPO_DIR}."
92
Added:
[[ -x "${NODE_BIN}" ]] || die "${NODE_BIN} does not exist or is not executable."
93
Added:
94
Added:
mkdir -p "${STATE_DIR}" "${RUN_DIR}"
95
Added:
chown "${RUN_USER}:${RUN_GROUP}" "${STATE_DIR}" "${RUN_DIR}"
96
Added:
97
Added:
echo "Verified layout of repo at ${REPO_DIR}."
98
Added:
}
99
Added:
100
Added:
function write_env_file() {
101
Added:
mkdir -p "${ENV_DIR}"
102
Added:
103
Added:
# TODO: print diff of existing and proposed env file
104
Added:
if [[ -f "${ENV_FILE}" ]]; then
105
Added:
echo "${ENV_FILE} already exists, remove or edit manually if necessary."
106
Added:
return
107
Added:
fi
108
Added:
109
Added:
if [[ -z "${MQTT_PASSWORD:-}" ]]; then
110
Added:
read -rsp "MQTT password for user '${MQTT_USERNAME}': " MQTT_PASSWORD
111
Added:
fi
112
Added:
113
Added:
[[ -n "${MQTT_PASSWORD:-}" ]] || die "MQTT_PASSWORD is required."
114
Added:
115
Added:
# TODO: unsafe!
116
Added:
cat > "${ENV_FILE}" <<EOF
117
Added:
MQTT_SIMPLE_ALLOW_INSECURE_LOGIN=1
118
Added:
119
Added:
SERIAL_DEVICE="${SERIAL_DEVICE}"
120
Added:
READ_INTERVAL=${READ_INTERVAL}
121
Added:
MQTT_HOST="${MQTT_HOST}"
122
Added:
MQTT_PORT="${MQTT_PORT}"
123
Added:
MQTT_USERNAME="${MQTT_USERNAME}"
124
Added:
MQTT_PASSWORD="${MQTT_PASSWORD}"
125
Added:
EOF
126
Added:
127
Added:
chown root:"${RUN_GROUP}" "${ENV_FILE}"
128
Added:
chmod 0640 "${ENV_FILE}"
129
Added:
}
130
Added:
131
Added:
function write_systemd_service() {
132
Added:
cat > "${SERVICE_FILE}" <<EOF
133
Added:
[Unit]
134
Added:
Description=FAPG DAQ sensor node
135
Added:
Wants=network-online.target
136
Added:
After=network-online.target
137
Added:
138
Added:
[Service]
139
Added:
Type=simple
140
Added:
User=${RUN_USER}
141
Added:
Group=${RUN_GROUP}
142
Added:
SupplementaryGroups=dialout
143
Added:
144
Added:
WorkingDirectory=${REPO_DIR}
145
Added:
EnvironmentFile=${ENV_FILE}
146
Added:
147
Added:
ExecStart=${NODE_BIN}
148
Added:
149
Added:
Restart=always
150
Added:
RestartSec=5
151
Added:
152
Added:
# Basic hardening. Do not enable PrivateDevices=true because this service needs /dev/ttyUSB0.
153
Added:
NoNewPrivileges=true
154
Added:
ProtectHome=true
155
Added:
ProtectSystem=strict
156
Added:
ReadWritePaths=${STATE_DIR} ${RUN_DIR}
157
Added:
158
Added:
[Install]
159
Added:
WantedBy=multi-user.target
160
Added:
EOF
161
Added:
162
Added:
chmod 0644 "$SERVICE_FILE"
163
Added:
}
164
Added:
165
Added:
function enable_service() {
166
Added:
systemctl daemon-reload
167
Added:
systemctl enable --now "${SERVICE_NAME}"
168
Added:
}
169
Added:
170
Added:
show_status() {
171
Added:
cat <<"EOF"
172
Added:
Deployment complete.
173
Added:
Current service state:
174
Added:
EOF
175
Added:
systemctl --no-pager --full status "${SERVICE_NAME}"
176
Added:
}
177
Added:
178
Added:
function main() {
179
Added:
require_root
180
Added:
install_os_dependencies
181
Added:
# install_perl_dependencies
182
Added:
create_runtime_user
183
Added:
check_repo_layout
184
Added:
write_env_file
185
Added:
write_systemd_service
186
Added:
enable_service
187
Added:
show_status
188
Added:
}
189
Added:
190
Added:
main "$@"
roles/daq-node/lib/perl5/FAPG/DAQ/EZO.pm
@@ -0,0 +1,362 @@
1
Added:
package FAPG::DAQ::EZO;
2
Added:
# -*- mode: perl-ts; -*-
3
Added:
4
Added:
use 5.40.1;
5
Added:
use strict;
6
Added:
use warnings;
7
Added:
8
Added:
use Device::SerialPort;
9
Added:
use Time::HiRes qw(time sleep);
10
Added:
11
Added:
my %SUPPORTED_PROBES = (
12
Added:
ph => { unit => 'pH' },
13
Added:
do => { unit => 'mg/L' },
14
Added:
orp => { unit => 'mV' },
15
Added:
ec => { unit => 'uS/cm' },
16
Added:
);
17
Added:
18
Added:
sub identify_probe {
19
Added:
my $device = _find_usb_device();
20
Added:
my $port = _open_serial_port($device);
21
Added:
22
Added:
# Stop continuous mode if enabled.
23
Added:
# This makes later "R" commands deterministic.
24
Added:
_drain_serial($port, 0.25);
25
Added:
_write_ezo_command($port, 'C,0');
26
Added:
sleep 0.4;
27
Added:
_drain_serial($port, 0.25);
28
Added:
29
Added:
my @lines = _ezo_command($port, 'I', 2.0);
30
Added:
31
Added:
my ($raw_type, $firmware, $raw_reply);
32
Added:
33
Added:
for my $line (@lines) {
34
Added:
if ($line =~ /^\?I,([^,\r\n]+),([^,\r\n]+)/i) {
35
Added:
$raw_type = $1;
36
Added:
$firmware = $2;
37
Added:
$raw_reply = $line;
38
Added:
last;
39
Added:
}
40
Added:
}
41
Added:
42
Added:
die "Could not identify EZO probe.\n"
43
Added:
unless defined $raw_type;
44
Added:
45
Added:
my $type = _normalize_probe_type($raw_type);
46
Added:
47
Added:
die "Unsupported EZO probe type '$raw_type'.\n"
48
Added:
unless exists $SUPPORTED_PROBES{$type};
49
Added:
50
Added:
return FAPG::DAQ::EZO::Probe->new(
51
Added:
device => $device,
52
Added:
port => $port,
53
Added:
type => $type,
54
Added:
raw_type => $raw_type,
55
Added:
firmware => $firmware,
56
Added:
unit => $SUPPORTED_PROBES{$type}->{unit},
57
Added:
raw_reply => $raw_reply,
58
Added:
);
59
Added:
}
60
Added:
61
Added:
sub _find_usb_device {
62
Added:
return $ENV{EZO_SERIAL_DEVICE}
63
Added:
if defined $ENV{EZO_SERIAL_DEVICE} && length $ENV{EZO_SERIAL_DEVICE};
64
Added:
65
Added:
my @devices = grep { /UART|FTDI|Atlas/i } glob '/dev/serial/by-id/*';
66
Added:
67
Added:
die "Found no EZO USB serial device under /dev/serial/by-id/.\n"
68
Added:
unless @devices;
69
Added:
70
Added:
die "Found multiple USB serial devices. Set EZO_SERIAL_DEVICE explicitly.\n"
71
Added:
if @devices > 1;
72
Added:
73
Added:
return $devices[0];
74
Added:
}
75
Added:
76
Added:
sub _open_serial_port {
77
Added:
my ($device) = @_;
78
Added:
79
Added:
my $port = Device::SerialPort->new($device)
80
Added:
or die "Cannot open serial port $device: $!";
81
Added:
82
Added:
$port->baudrate(9600);
83
Added:
$port->databits(8);
84
Added:
$port->parity('none');
85
Added:
$port->stopbits(1);
86
Added:
$port->handshake('none');
87
Added:
88
Added:
$port->read_const_time(100);
89
Added:
$port->read_char_time(0);
90
Added:
91
Added:
$port->write_settings
92
Added:
or die "Cannot apply serial settings to $device.\n";
93
Added:
94
Added:
return $port;
95
Added:
}
96
Added:
97
Added:
sub _ezo_command {
98
Added:
my ($port, $command, $timeout) = @_;
99
Added:
100
Added:
_drain_serial($port, 0.05);
101
Added:
_write_ezo_command($port, $command);
102
Added:
103
Added:
return _read_ezo_lines($port, $timeout);
104
Added:
}
105
Added:
106
Added:
sub _write_ezo_command {
107
Added:
my ($port, $command) = @_;
108
Added:
109
Added:
my $payload = "$command\r";
110
Added:
my $written = $port->write($payload);
111
Added:
112
Added:
die "Could not write EZO command '$command'.\n"
113
Added:
unless defined $written && $written == length($payload);
114
Added:
115
Added:
return 1;
116
Added:
}
117
Added:
118
Added:
sub _read_ezo_lines {
119
Added:
my ($port, $timeout) = @_;
120
Added:
121
Added:
my $deadline = time + $timeout;
122
Added:
my $buffer = '';
123
Added:
my @lines;
124
Added:
125
Added:
while (time < $deadline) {
126
Added:
my ($count, $chunk) = $port->read(128);
127
Added:
128
Added:
if ($count && defined $chunk) {
129
Added:
$buffer .= $chunk;
130
Added:
131
Added:
while ($buffer =~ s/^([^\r]*)\r//) {
132
Added:
my $line = $1;
133
Added:
$line =~ s/^\s+//;
134
Added:
$line =~ s/\s+$//;
135
Added:
136
Added:
push @lines, $line if length $line;
137
Added:
}
138
Added:
}
139
Added:
140
Added:
sleep 0.02;
141
Added:
}
142
Added:
143
Added:
return @lines;
144
Added:
}
145
Added:
146
Added:
sub _drain_serial {
147
Added:
my ($port, $seconds) = @_;
148
Added:
149
Added:
my $deadline = time + $seconds;
150
Added:
151
Added:
while (time < $deadline) {
152
Added:
my ($count, undef) = $port->read(255);
153
Added:
sleep($count ? 0.01 : 0.05);
154
Added:
}
155
Added:
}
156
Added:
157
Added:
sub _normalize_probe_type {
158
Added:
my ($raw_type) = @_;
159
Added:
160
Added:
my $type = lc $raw_type;
161
Added:
$type =~ s/[^a-z0-9]//g;
162
Added:
163
Added:
return $type;
164
Added:
}
165
Added:
166
Added:
sub _parse_first_number {
167
Added:
my ($line) = @_;
168
Added:
169
Added:
my ($first_field) = split /,/, $line;
170
Added:
171
Added:
return undef unless defined $first_field;
172
Added:
173
Added:
$first_field =~ s/^\s+//;
174
Added:
$first_field =~ s/\s+$//;
175
Added:
176
Added:
return undef
177
Added:
unless $first_field =~ /^([+-]?(?:\d+(?:\.\d*)?|\.\d+))$/;
178
Added:
179
Added:
return 0 + $1;
180
Added:
}
181
Added:
182
Added:
1;
183
Added:
184
Added:
package FAPG::DAQ::EZO::Probe;
185
Added:
# -*- mode: perl-ts; -*-
186
Added:
187
Added:
use 5.40.1;
188
Added:
use strict;
189
Added:
use warnings;
190
Added:
191
Added:
sub new {
192
Added:
my ($class, %args) = @_;
193
Added:
return bless \%args, $class;
194
Added:
}
195
Added:
196
Added:
sub device {
197
Added:
my ($self) = @_;
198
Added:
return $self->{device};
199
Added:
}
200
Added:
201
Added:
sub port {
202
Added:
my ($self) = @_;
203
Added:
return $self->{port};
204
Added:
}
205
Added:
206
Added:
sub type {
207
Added:
my ($self) = @_;
208
Added:
return $self->{type};
209
Added:
}
210
Added:
211
Added:
sub raw_type {
212
Added:
my ($self) = @_;
213
Added:
return $self->{raw_type};
214
Added:
}
215
Added:
216
Added:
sub firmware {
217
Added:
my ($self) = @_;
218
Added:
return $self->{firmware};
219
Added:
}
220
Added:
221
Added:
sub unit {
222
Added:
my ($self) = @_;
223
Added:
return $self->{unit};
224
Added:
}
225
Added:
226
Added:
sub get_reading {
227
Added:
my ($self) = @_;
228
Added:
229
Added:
my @lines = FAPG::DAQ::EZO::_ezo_command($self->port, 'R', 3.0);
230
Added:
231
Added:
for my $line (@lines) {
232
Added:
die "EZO probe returned error while reading: $line\n"
233
Added:
if $line =~ /^\*ER/i;
234
Added:
235
Added:
next if $line =~ /^\*/;
236
Added:
next if $line =~ /^\?/;
237
Added:
238
Added:
my $value = FAPG::DAQ::EZO::_parse_first_number($line);
239
Added:
240
Added:
next unless defined $value;
241
Added:
242
Added:
return FAPG::DAQ::EZO::Reading->new(
243
Added:
probe => $self->type,
244
Added:
value => $value,
245
Added:
unit => $self->unit,
246
Added:
device => $self->device,
247
Added:
raw_reply => $line,
248
Added:
);
249
Added:
}
250
Added:
251
Added:
die "Could not parse EZO reading from response: " . join(' | ', @lines) . "\n";
252
Added:
}
253
Added:
254
Added:
1;
255
Added:
256
Added:
package FAPG::DAQ::EZO::Reading;
257
Added:
# -*- mode: perl-ts; -*-
258
Added:
259
Added:
use 5.40.1;
260
Added:
use strict;
261
Added:
use warnings;
262
Added:
263
Added:
use JSON::PP qw(encode_json);
264
Added:
use Net::MQTT::Simple;
265
Added:
use POSIX qw(strftime);
266
Added:
use Sys::Hostname qw(hostname);
267
Added:
268
Added:
sub new {
269
Added:
my ($class, %args) = @_;
270
Added:
271
Added:
$args{timestamp} //= _utc_timestamp();
272
Added:
$args{node} //= $ENV{DAQ_NODE} || hostname();
273
Added:
274
Added:
return bless \%args, $class;
275
Added:
}
276
Added:
277
Added:
sub probe {
278
Added:
my ($self) = @_;
279
Added:
return $self->{probe};
280
Added:
}
281
Added:
282
Added:
sub value {
283
Added:
my ($self) = @_;
284
Added:
return $self->{value};
285
Added:
}
286
Added:
287
Added:
sub unit {
288
Added:
my ($self) = @_;
289
Added:
return $self->{unit};
290
Added:
}
291
Added:
292
Added:
sub node {
293
Added:
my ($self) = @_;
294
Added:
return $self->{node};
295
Added:
}
296
Added:
297
Added:
sub timestamp {
298
Added:
my ($self) = @_;
299
Added:
return $self->{timestamp};
300
Added:
}
301
Added:
302
Added:
sub topic {
303
Added:
my ($self) = @_;
304
Added:
305
Added:
return join '/',
306
Added:
'fapg',
307
Added:
'daq',
308
Added:
$self->probe,
309
Added:
$self->node,
310
Added:
'reading';
311
Added:
}
312
Added:
313
Added:
sub as_hash {
314
Added:
my ($self) = @_;
315
Added:
316
Added:
return {
317
Added:
timestamp => $self->timestamp,
318
Added:
probe => $self->probe,
319
Added:
value => $self->value,
320
Added:
unit => $self->unit,
321
Added:
node => $self->node,
322
Added:
};
323
Added:
}
324
Added:
325
Added:
sub as_json {
326
Added:
my ($self) = @_;
327
Added:
return encode_json($self->as_hash);
328
Added:
}
329
Added:
330
Added:
sub publish_mqtt {
331
Added:
my ($self) = @_;
332
Added:
333
Added:
my $host = $ENV{MQTT_HOST} // 'fapg-daq-five-01';
334
Added:
my $port = $ENV{MQTT_PORT} // 1883;
335
Added:
336
Added:
my $server = "$host:$port";
337
Added:
338
Added:
my $mqtt = Net::MQTT::Simple->new($server);
339
Added:
340
Added:
my $username = exists $ENV{MQTT_USERNAME}
341
Added:
? $ENV{MQTT_USERNAME}
342
Added:
: 'fapg_zero';
343
Added:
344
Added:
if (defined $username && length $username) {
345
Added:
my $password = $ENV{MQTT_PASSWORD};
346
Added:
347
Added:
die "MQTT_USERNAME is set but MQTT_PASSWORD is missing.\n"
348
Added:
unless defined $password && length $password;
349
Added:
350
Added:
$mqtt->login($username, $password);
351
Added:
}
352
Added:
353
Added:
$mqtt->publish($self->topic => $self->as_json);
354
Added:
355
Added:
return 1;
356
Added:
}
357
Added:
358
Added:
sub _utc_timestamp {
359
Added:
return strftime('%Y-%m-%dT%H:%M:%SZ', gmtime);
360
Added:
}
361
Added:
362
Added:
1;
roles/daq-node/t/01-probe.t
@@ -0,0 +1,100 @@
1
Added:
#!/usr/bin/env perl
2
Added:
# -*- mode: perl-ts; -*-
3
Added:
4
Added:
use strict;
5
Added:
use warnings;
6
Added:
7
Added:
use Test::More tests => 3;
8
Added:
use Time::HiRes qw(usleep);
9
Added:
10
Added:
# Basic Atlas Scientific EZO probe smoke test.
11
Added:
#
12
Added:
# Run on a daq-node:
13
Added:
# prove -v t/probe.t
14
Added:
#
15
Added:
# Optional overrides:
16
Added:
# FAPG_DAQ_SERIAL_PORT=/dev/ttyUSB1 FAPG_DAQ_SERIAL_BAUD=9600 prove -v t/probe.t
17
Added:
18
Added:
my $port = $ENV{FAPG_DAQ_SERIAL_PORT} // $ARGV[0] // '/dev/ttyUSB0';
19
Added:
my $baud = $ENV{FAPG_DAQ_SERIAL_BAUD} // $ARGV[1] // 9600;
20
Added:
21
Added:
my $serial_module_ok = eval {
22
Added:
require Device::SerialPort;
23
Added:
Device::SerialPort->import;
24
Added:
1;
25
Added:
};
26
Added:
27
Added:
BAIL_OUT("Device::SerialPort is required: $@") unless $serial_module_ok;
28
Added:
BAIL_OUT("Serial port does not exist: $port") unless -e $port;
29
Added:
30
Added:
my $serial = Device::SerialPort->new($port)
31
Added:
or BAIL_OUT("Cannot open serial port: $port");
32
Added:
33
Added:
$serial->baudrate($baud) or BAIL_OUT("Cannot set baudrate to $baud");
34
Added:
$serial->databits(8) or BAIL_OUT('Cannot set databits to 8');
35
Added:
$serial->parity('none') or BAIL_OUT('Cannot set parity to none');
36
Added:
$serial->stopbits(1) or BAIL_OUT('Cannot set stopbits to 1');
37
Added:
38
Added:
$serial->read_char_time(0);
39
Added:
$serial->read_const_time(100);
40
Added:
41
Added:
note("Testing EZO probe on $port at $baud baud");
42
Added:
43
Added:
my $info = ezo_command($serial, 'I', 500_000);
44
Added:
note("I => " . printable($info));
45
Added:
46
Added:
ok($info ne '', 'probe is reachable');
47
Added:
like($info, qr/(?:\?I,|EZO|PH|EC|DO|ORP)/i, 'probe is identifiable');
48
Added:
49
Added:
my $reading = ezo_command($serial, 'R', 1_500_000);
50
Added:
note("R => " . printable($reading));
51
Added:
52
Added:
like(
53
Added:
$reading,
54
Added:
qr/OK/,
55
Added:
'probe returns a single reading'
56
Added:
);
57
Added:
58
Added:
sub ezo_command {
59
Added:
my ($serial, $command, $wait_us) = @_;
60
Added:
61
Added:
drain_serial($serial);
62
Added:
63
Added:
my $written = $serial->write("$command\r");
64
Added:
return '' unless defined $written && $written > 0;
65
Added:
66
Added:
usleep($wait_us);
67
Added:
68
Added:
my $reply = '';
69
Added:
while (1) {
70
Added:
my ($count, $buffer) = $serial->read(255);
71
Added:
last unless $count;
72
Added:
$reply .= $buffer;
73
Added:
}
74
Added:
75
Added:
return clean_reply($reply);
76
Added:
}
77
Added:
78
Added:
sub drain_serial {
79
Added:
my ($serial) = @_;
80
Added:
81
Added:
while (1) {
82
Added:
my ($count, undef) = $serial->read(255);
83
Added:
last unless $count;
84
Added:
}
85
Added:
}
86
Added:
87
Added:
sub clean_reply {
88
Added:
my ($reply) = @_;
89
Added:
90
Added:
$reply =~ s/\r/\n/g;
91
Added:
$reply =~ s/\n+/\n/g;
92
Added:
$reply =~ s/^\n|\n$//g;
93
Added:
94
Added:
return $reply;
95
Added:
}
96
Added:
97
Added:
sub printable {
98
Added:
my ($value) = @_;
99
Added:
return $value eq '' ? '<no response>' : $value;
100
Added:
}