File Coverage

blib/lib/Device/Serial/SLuRM/Protocol.pm
Criterion Covered Total %
statement 112 113 99.1
branch 26 36 72.2
condition 4 13 30.7
subroutine 19 19 100.0
pod 0 5 0.0
total 161 186 86.5


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2023-2026 -- leonerd@leonerd.org.uk
5              
6 10     10   1474701 use v5.26;
  10         38  
7 10     10   71 use warnings;
  10         19  
  10         753  
8 10     10   2977 use Object::Pad 0.800 ':experimental(adjust_params)';
  10         28202  
  10         603  
9 10     10   4259 use Sublike::Extended 0.29 qw( method );
  10         2955  
  10         62  
10              
11             package Device::Serial::SLuRM::Protocol 0.10;
12             class Device::Serial::SLuRM::Protocol;
13              
14 10     10   3661 use Carp;
  10         40  
  10         904  
15              
16 10     10   780 use Future::AsyncAwait;
  10         27336  
  10         98  
17 10     10   6037 use Future::Buffer 0.03;
  10         32480  
  10         815  
18 10     10   832 use Future::IO;
  10         48790  
  10         680  
19              
20 10     10   6635 use Digest::CRC qw( crc8 );
  10         49815  
  10         1287  
21              
22 10   50 10   79 use constant DEBUG => $ENV{SLURM_DEBUG} // 0;
  10         19  
  10         1068  
23              
24             # builtin::false only turned up at 5.36, grrrr
25 10     10   62 use constant false => !!0;
  10         22  
  10         1088  
26              
27             =encoding UTF-8
28              
29             =head1 NAME
30              
31             C - implements the lower-level packet format of the SLµRM protocol
32              
33             =head1 DESCRIPTION
34              
35             This class provides the inner logic used by L and
36             L.
37              
38             =cut
39              
40             use constant {
41 10         53111 SLURM_PKTCTRL_META => 0x00,
42             SLURM_PKTCTRL_META_RESET => 0x01,
43             SLURM_PKTCTRL_META_RESETACK => 0x02,
44              
45             SLURM_PKTCTRL_NOTIFY => 0x10,
46              
47             SLURM_PKTCTRL_REQUEST => 0x30,
48              
49             SLURM_PKTCTRL_RESPONSE => 0xB0,
50             SLURM_PKTCTRL_ACK => 0xC0,
51             SLURM_PKTCTRL_ERR => 0xE0,
52 10     10   71 };
  10         20  
53              
54             # Metrics support is entirely optional
55             our $METRICS;
56             eval {
57             require Metrics::Any and Metrics::Any->VERSION( '0.05' ) and
58             Metrics::Any->import( '$METRICS', name_prefix => [ 'slurm' ] );
59             };
60              
61             my %PKTTYPE_NAME;
62              
63             if( defined $METRICS ) {
64             $METRICS->make_counter( discards =>
65             description => "Number of received packets discarded due to CRC check",
66             );
67              
68             $METRICS->make_counter( packets =>
69             description => "Number of packets sent and received, by type",
70             labels => [qw( dir type )],
71             );
72              
73             $METRICS->make_distribution( request_success_attempts =>
74             description => "How many requests eventually succeeded after a given number of transmissions",
75             units => "",
76             buckets => [ 1 .. 3 ],
77             );
78              
79             $METRICS->make_timer( request_duration =>
80             description => "How long it took to get a response to each request",
81             buckets => [ 0.001, 0.002, 0.005, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5 ],
82             );
83              
84             $METRICS->make_counter( retransmits =>
85             description => "Number of retransmits of packets",
86             );
87              
88             $METRICS->make_counter( serial_bytes =>
89             description => "Total number of bytes sent and received on the serial link",
90             labels => [qw( dir )],
91             );
92              
93             $METRICS->make_counter( timeouts =>
94             description => "Number of transactions that were abandoned due to eventual timeout",
95             );
96              
97             %PKTTYPE_NAME = map { __PACKAGE__->can( "SLURM_PKTCTRL_$_" )->() => $_ }
98             qw( META NOTIFY REQUEST RESPONSE ERR ACK );
99              
100             # Keep prometheus increase() happy by initialising all the counters to zero
101             $METRICS->inc_counter_by( discards => 0 );
102             foreach my $dir (qw( rx tx )) {
103             $METRICS->inc_counter_by( packets => 0, [ dir => $dir, type => $_ ] ) for values %PKTTYPE_NAME;
104             $METRICS->inc_counter_by( serial_bytes => 0, [ dir => $dir ] );
105             }
106             $METRICS->inc_counter_by( retransmits => 0 );
107             $METRICS->inc_counter_by( timeouts => 0 );
108             }
109              
110             field $_fh :param = undef;
111              
112             field $_multidrop :param = 0;
113              
114             # To calculate baud-independent timeout values we need a rough estimate of the
115             # time to send each byte
116 7     7 0 22 field $_bps :reader;
  7         80  
117              
118             ADJUST :params (
119             :$dev = undef,
120             :$baud //= 115200,
121             ) {
122             if( defined $_fh ) {
123             # fine
124             $_bps = $baud / 10;
125             }
126             elsif( defined $dev ) {
127             require IO::Termios;
128              
129             $_fh = IO::Termios->open( $dev, "$baud,8,n,1" ) or
130             croak "Cannot open device $dev - $!";
131              
132             $_fh->cfmakeraw;
133              
134             $_bps = $_fh->getobaud / 10;
135             }
136             else {
137             croak "Require either a 'dev' or 'fh' parameter";
138             }
139             }
140              
141             field $_recv_buffer;
142              
143             field @_linestuff_queue;
144              
145 9     9   26 async method _drain1_linestuff ()
  9         41  
  9         19  
146 9         24 {
147 9         53 my $bytes = shift @_linestuff_queue;
148              
149 9         41 my ( $pktctrl ) = unpack "C", $bytes;
150              
151             $METRICS and
152 9 50 0     34 $METRICS->inc_counter( packets => [ dir => "tx", type => $PKTTYPE_NAME{ $pktctrl & 0xF0 } // "UNKNOWN" ] );
153 9 50       27 $METRICS and
154             $METRICS->inc_counter_by( serial_bytes => 1 + length $bytes, [ dir => "tx" ] );
155              
156 9         18 printf STDERR "SLuRM DEV WRITE: %v02X\n", "\x55" . $bytes
157             if DEBUG > 2;
158              
159 9         58 await Future::IO->syswrite_exactly( $_fh, "\x55" . $bytes );
160             }
161              
162             async method recv
163 70     70 0 43136 {
164             $_recv_buffer //= Future::Buffer->new(
165             fill => sub {
166 70     70   3504 my $f = Future::IO->sysread( $_fh, 8192 );
167 0         0 $f->on_done( sub { $METRICS->inc_counter_by( serial_bytes => length $_[0], [ dir => "rx" ] ) } )
168 70 50       181222 if $METRICS;
169             $f->on_done( sub { printf STDERR "SLuRM DEV READ: %v02X\n", $_[0] } )
170 70         151 if DEBUG > 2;
171 70         367 $f;
172             },
173 70   66     356 );
174              
175 70         336 my $headerlen = 3 + !!$_multidrop;
176              
177             PACKET: {
178             # await start-of-frame while line-stuffing
179 70         128 while(1) {
  73         126  
180 82         20533 my $f = $_recv_buffer->read_until( qr/\x55/ );
181 82 100       8225 if( @_linestuff_queue ) {
182 11         49 $f = Future->wait_any( $f,
183             $self->interpacket_delay->then_done( "" ),
184             );
185             }
186              
187 82 100       22352 last if length await $f;
188              
189 9         6020 await $self->_drain1_linestuff;
190             }
191              
192 51 50       32035 defined( my $pkt = await $_recv_buffer->read_exactly( $headerlen ) )
193             or return; # EOF
194              
195 51         4081 my ( $pktctrl, $addr, $len );
196 51 100       319 $_multidrop ? ( ( $pktctrl, $addr, $len ) = unpack "C C C", $pkt )
197             : ( ( $addr, $pktctrl, $len ) = ( 0, unpack "C C", $pkt ) );
198              
199 51 100       192 if( crc8( $pkt ) != 0 ) {
200             # Header checksum failed
201 1 50       39 $METRICS and
202             $METRICS->inc_counter( discards => );
203              
204 1 50       7 $pkt =~ m/\x55/ and
205             $_recv_buffer->unread( substr $pkt, $-[0] );
206 1         4 redo PACKET;
207             }
208              
209 50         1893 $pkt .= await $_recv_buffer->read_exactly( $len + 1 );
210              
211 50 100       4167 if( crc8( $pkt ) != 0 ) {
212             # Body checksum failed
213 2 50       64 $METRICS and
214             $METRICS->inc_counter( discards => );
215              
216 2 100       15 $pkt =~ m/\x55/ and
217             $_recv_buffer->unread( substr $pkt, $-[0] );
218 2         26 redo PACKET;
219             }
220              
221 48         1419 my $payload = substr( $pkt, $headerlen, $len );
222              
223 48         89 printf STDERR "SLuRM <-RX%s {%02X/%v02X}\n",
224             ( $_multidrop ? sprintf "(%d)", $addr : "" ), $pktctrl, $payload
225             if DEBUG > 1;
226              
227             $METRICS and
228 48 50 0     139 $METRICS->inc_counter( packets => [ dir => "rx", type => $PKTTYPE_NAME{ $pktctrl & 0xF0 } // "UNKNOWN" ] );
229              
230 48         495 return $pktctrl, $addr, $payload;
231             }
232             }
233              
234 60     60 0 12543 async method send ( $pktctrl, $addr, $payload, :$linestuff = false )
  60         309  
  60         113  
  60         110  
  60         158  
  60         215  
  60         101  
235 60         149 {
236 60         154 printf STDERR "SLuRM TX%s-> {%02X/%v02X}\n",
237             ( $_multidrop ? sprintf "(%d)", $addr & 0x7F : "" ), $pktctrl, $payload
238             if DEBUG > 1;
239              
240 60 100 50     1020 my $bytes = $_multidrop
241             ? pack( "C C C", $pktctrl, $addr // die( "ADDR must be defined for multidrop" ), length $payload )
242             : pack( "C C", $pktctrl, length $payload );
243 60         245 $bytes .= pack( "C", crc8( $bytes ) );
244              
245 60         9386 $bytes .= $payload;
246 60         183 $bytes .= pack( "C", crc8( $bytes ) );
247              
248 60 100       1551 if( $linestuff ) {
249 9         27 push @_linestuff_queue, $bytes;
250 9         152 return;
251             }
252              
253             $METRICS and
254 51 50 0     149 $METRICS->inc_counter( packets => [ dir => "tx", type => $PKTTYPE_NAME{ $pktctrl & 0xF0 } // "UNKNOWN" ] );
255 51 50       137 $METRICS and
256             $METRICS->inc_counter_by( serial_bytes => 1 + length $bytes, [ dir => "tx" ] );
257              
258 51         94 printf STDERR "SLuRM DEV WRITE: %v02X\n", "\x55" . $bytes
259             if DEBUG > 2;
260              
261 51         444 return await Future::IO->syswrite_exactly( $_fh, "\x55" . $bytes );
262             }
263              
264 22     22 0 79 async method interpacket_delay ()
  22         180  
  22         36  
265 22         54 {
266             # wait 20-ish byte times as a gap between packets
267 22         182 await Future::IO->sleep( 20 / $_bps );
268             }
269              
270 11     11 0 32 async method send_twice ( $pktctrl, $node_id, $payload )
  11         45  
  11         22  
  11         23  
  11         28  
  11         22  
271 11         28 {
272 11         58 await $self->send( $pktctrl, $node_id, $payload );
273              
274 11         32338 await $self->interpacket_delay;
275              
276 11         10844 await $self->send( $pktctrl, $node_id, $payload );
277             }
278              
279             =head1 AUTHOR
280              
281             Paul Evans
282              
283             =cut
284              
285             0x55AA;