Add role for daq-node.

Commit
239de739d538966ccb900eb391759b5e14b8a217
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
roles/daq-node/bin/fapg-daq-node
index 00000000..55a6d599 000000..100755
@@ -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
index 00000000..1fa1a83f 000000..100755
@@ -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
index 00000000..83466d01 000000..100644
@@ -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
index 00000000..8a84da34 000000..100644
@@ -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: }