File Coverage

blib/lib/Device/Serial/SLuRM.pm
Criterion Covered Total %
statement 232 247 93.9
branch 53 74 71.6
condition 22 34 64.7
subroutine 34 36 94.4
pod 6 6 100.0
total 347 397 87.4


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, 2022-2026 -- leonerd@leonerd.org.uk
5              
6 7     7   1387102 use v5.28; # delete %hash{@slice}
  7         51  
7 7     7   79 use warnings;
  7         22  
  7         486  
8 7     7   2715 use Object::Pad 0.807 ':experimental(adjust_params inherit_field)';
  7         24390  
  7         507  
9              
10             package Device::Serial::SLuRM 0.10;
11             class Device::Serial::SLuRM;
12              
13 7     7   3362 use Carp;
  7         26  
  7         580  
14              
15 7     7   5009 use Syntax::Keyword::Match;
  7         28702  
  7         51  
16              
17 7     7   742 use Future::AsyncAwait;
  7         13  
  7         65  
18 7     7   446 use Future::IO;
  7         37  
  7         388  
19 7     7   3697 use Future::Selector 0.02; # ->run_until_ready
  7         55494  
  7         804  
20              
21 7     7   477 use Time::HiRes qw( gettimeofday tv_interval );
  7         17  
  7         115  
22              
23 7   50 7   1365 use constant DEBUG => $ENV{SLURM_DEBUG} // 0;
  7         15  
  7         951  
24              
25             require Device::Serial::SLuRM::Protocol;
26 7     7   61 no warnings 'once';
  7         33  
  7         2638  
27             our $METRICS = $Device::Serial::SLuRM::Protocol::METRICS;
28              
29             # builtin:: only turned up at 5.36, grrrr
30             use constant {
31 7         738 true => !!1,
32             false => !!0,
33 7     7   49 };
  7         13  
34              
35             use constant {
36 7         946 SLURM_PKTCTRL_META => 0x00,
37             SLURM_PKTCTRL_META_RESET => 0x01,
38             SLURM_PKTCTRL_META_RESETACK => 0x02,
39              
40             SLURM_PKTCTRL_NOTIFY => 0x10,
41              
42             SLURM_PKTCTRL_REQUEST => 0x30,
43              
44             SLURM_PKTCTRL_RESPONSE => 0xB0,
45             SLURM_PKTCTRL_ACK => 0xC0,
46             SLURM_PKTCTRL_ERR => 0xE0,
47 7     7   45 };
  7         13  
48              
49             =encoding UTF-8
50              
51             =head1 NAME
52              
53             C - communicate the SLµRM protocol over a serial port
54              
55             =head1 SYNOPSIS
56              
57             =for highlighter language=perl
58              
59             use v5.36;
60             use Device::Serial::SLuRM;
61              
62             my $slurm = Device::Serial::SLuRM->new(
63             dev => "/dev/ttyUSB0",
64             baud => 19200,
65             );
66              
67             $slurm->run(
68             on_notify => sub ($payload) {
69             printf "NOTIFY: %v02X\n", $payload;
70             }
71             )->await;
72              
73             =head1 DESCRIPTION
74              
75             This module provides a L-based interface for communicating with
76             a peer device on a serial port (or similar device handle) which talks the
77             SLµRM messaging protocol. It supports sending and receiving of NOTIFY
78             packets, and sending of REQUEST packets that receive a RESPONSE.
79              
80             It currently does not support receiving REQUESTs, though this could be added
81             relatively easily.
82              
83             Optionally, this module supports being the controller for a multi-drop
84             ("MSLµRM") bus. See the L subclass.
85              
86             =head2 SLµRM
87              
88             SLµRM ("Serial Link Microcontroller Reliable Messaging") is a simple
89             bidirectional communication protocol for adding reliable message framing and
90             request/response semantics to byte-based data links (such as asynchronous
91             serial ports), which may themselves be somewhat unreliable. SLµRM can tolerate
92             bytes arriving corrupted or going missing altogether, or additional noise
93             bytes being received, while still maintaining a reliable bidirectional flow of
94             messages. There are two main kinds of message flows - NOTIFYs and REQUESTs. In
95             all cases, packet payloads can be of a variable length (including zero bytes),
96             and the protocol itself does not put semantic meaning on those bytes - they
97             are free for the application to use as required.
98              
99             A NOTIFY message is a simple notification from one peer to the other, that
100             does not yield a response.
101              
102             A REQUEST message carries typically some sort of command instruction, to which
103             the peer should respond with a RESPONSE or ERR packet. Replies to a REQUEST
104             message do not have to be sent sequentially.
105              
106             The F directory of this distribution contains more detailed protocol
107             documentation which may be useful for writing other implementations.
108              
109             The F directory of this distribution contains a reference
110             implementation in C for 8-bit microcontrollers, such as AVR ATtiny and ATmega
111             chips.
112              
113             =cut
114              
115             =head2 Metrics
116              
117             If L is available, this module additionally provides metrics
118             under the namespace prefix of C. The following metrics are provided:
119              
120             =over 4
121              
122             =item discards
123              
124             An unlabelled counter tracking the number of times a received packet is
125             discarded due to failing CRC check.
126              
127             =item packets
128              
129             A counter, labelled by direction and packet type, tracking the number of
130             packets sent and received of each type.
131              
132             =item request_success_attempts
133              
134             A distribution that tracks how many attempts it took to get a response to each
135             request.
136              
137             =item request_duration
138              
139             A timer that tracks how long it took to get a response to each request.
140              
141             =item retransmits
142              
143             An unlabelled counter tracking the number of times a (REQUEST) packet had to
144             be retransmitted after the initial one timed out.
145              
146             =item serial_bytes
147              
148             A counter, labelled by direction, tracking the number of bytes sent and
149             received directly over the serial port. The rate of this can be used to
150             calculate overall serial link utilisation.
151              
152             =item timeouts
153              
154             An unlabelled counter tracking the number of times a request transaction was
155             abandoned entirely due to a timeout. This does I count transactions that
156             eventually succeeded after intermediate timeouts and retransmissions.
157              
158             =back
159              
160             =cut
161              
162             =head1 PARAMETERS
163              
164             =head2 dev
165              
166             dev => PATH
167              
168             Path to the F device node representing the serial port used for this
169             communication. This will be opened via L and configured into the
170             appropriate mode and baud rate.
171              
172             =head2 baud
173              
174             baud => NUM
175              
176             Optional baud rate to set for communication when opening a device node.
177              
178             SLµRM does not specify a particular rate, but a default value of 115.2k will
179             apply if left unspecified.
180              
181             =head2 fh
182              
183             fh => IO
184              
185             An IO handle directly to the the serial port device to be used for reading and
186             writing. It will be assumed to be set up correctly; no further setup will be
187             performed.
188              
189             Either C or C are required.
190              
191             =head2 retransmit_delay
192              
193             retransmit_delay => NUM
194              
195             Optional delay in seconds to wait after a non-response of a REQUEST packet
196             before sending it again.
197              
198             A default value will be calculated if not specified. This is based on the
199             serial link baud rate. At the default 115.2k baud it will be 50msec (0.05);
200             the delay will be scaled appropriately for other baud rates, to maintain a
201             timeout of the time it would take to send 576 bytes.
202              
203             Applications that transfer large amounts of data over slow links, or for which
204             responding to a command may take a long time, should increase this value.
205              
206             =head2 retransmit_count
207              
208             retransmit_count => NUM
209              
210             Optional number of additional attempts to try sending REQUEST packets before
211             giving up entirely. A default of 2 will apply if not specified (thus each
212             C method will make up to 3 attempts).
213              
214             =cut
215              
216 7     7   36 use constant is_multidrop => 0;
  7         38  
  7         5351  
217              
218             field $_protocol :inheritable; # TODO
219             ADJUST :params ( %params )
220             {
221             $_protocol = Device::Serial::SLuRM::Protocol->new(
222             multidrop => __CLASS__->is_multidrop,
223             delete %params{qw( fh dev baud )},
224             );
225             }
226              
227             field $_retransmit_delay :param = undef;
228             field $_retransmit_count :param //= 2;
229              
230             ADJUST
231             {
232             if( !defined $_retransmit_delay ) {
233             # At 115200baud (being 11520 bytes/sec presuming 1 start, no parity,
234             # 1 stop) we should get 0.05 sec delay; this is the time taken to
235             # transmit 576 bytes.
236             $_retransmit_delay = 576 / $_protocol->bps;
237             }
238             }
239              
240             field $_on_notify;
241             field $_on_request;
242              
243             class Device::Serial::SLuRM::_NodeState {
244 32     32   112 field $did_reset :mutator;
  32         256  
245              
246 45     45   118 field $seqno_tx :mutator = 0;
247 45     51   183 field $seqno_rx :mutator = 0;
  51         7262  
248              
249 51         157 field @_pending_slots; # [$seqno] = { payload, response_f }
250              
251 44     44   83 method pending_slot ( $seqno ) { return $_pending_slots[ $seqno ] }
  44         155  
  44         86  
  44         60  
  44         181  
252 13     13   219 method set_pending_slot ( $seqno, $data ) { $_pending_slots[ $seqno ] = $data; }
  13         59  
  13         24  
  13         28  
  13         24  
  13         48  
253 12     12   30 method clear_pending_slot ( $seqno ) { undef $_pending_slots[ $seqno ]; }
  12         54  
  12         29  
  12         26  
  12         97  
254             }
255              
256             field @_nodestate; # keyed per peer node ID
257              
258             field $_rx_nodestate; # a second set of pending slots for received REQUESTs
259              
260             =head1 METHODS
261              
262             =cut
263              
264             =head2 recv_packet
265              
266             ( $pktctrl, $payload ) = await $slurm->recv_packet;
267              
268             Waits for and returns the next packet to be received from the serial port.
269              
270             =cut
271              
272             field $_next_resetack_f;
273              
274 0     0 1 0 async method recv_packet ()
  0         0  
  0         0  
275 0         0 {
276 0         0 my ( $pktctrl, undef, $payload ) = await $_protocol->recv;
277 0         0 return ( $pktctrl, $payload );
278             }
279              
280             field $_run_f;
281              
282             async method _run
283 22     22   91 {
284 22         106 while(1) {
285 57 50       3204 my ( $pktctrl, $addr, $payload ) = await $_protocol->recv
286             or return; # EOF
287              
288 35 50       5105 redo if $addr & 0x80; # controller reflection
289              
290 35         79 my $node_id = $addr;
291              
292 35         77 my $seqno = $pktctrl & 0x0F;
293 35         108 $pktctrl &= 0xF0;
294              
295 35   66     154 my $nodestate = $_nodestate[ $node_id ] //= Device::Serial::SLuRM::_NodeState->new;
296              
297 35 100       129 if( $pktctrl == SLURM_PKTCTRL_META ) {
298             match( $seqno : == ) {
299             case( SLURM_PKTCTRL_META_RESET ),
300             case( SLURM_PKTCTRL_META_RESETACK ) {
301 8         94 ( $nodestate->seqno_rx ) = unpack "C", $payload;
302              
303 8 100       31 if( $seqno == SLURM_PKTCTRL_META_RESET ) {
304 1         5 await $self->send_packet( SLURM_PKTCTRL_META_RESETACK, pack "C", $nodestate->seqno_tx );
305             }
306             else {
307 7 50       60 $_next_resetack_f->done if $_next_resetack_f;
308             }
309             }
310 8 50 66     66 default {
311 0         0 warn sprintf "No idea what to do with pktctrl(meta) = %02X\n", $seqno;
312             }
313             }
314              
315 8         2520 next;
316             }
317              
318 27         53 my $is_dup;
319 27 100       92 if( !( $pktctrl & 0x80 ) ) {
320 14 50       53 if( defined $nodestate->seqno_rx ) {
321 14         39 my $seqdiff = $seqno - $nodestate->seqno_rx;
322 14 100       43 $seqdiff += 16 if $seqdiff < 0;
323 14   100     75 $is_dup = !$seqdiff || $seqdiff > 8; # suppress duplicates / backsteps
324             }
325              
326 14         69 $nodestate->seqno_rx = $seqno;
327             }
328              
329             match( $pktctrl : == ) {
330             case( SLURM_PKTCTRL_NOTIFY ) {
331 11 100       32 next if $is_dup;
332              
333 9         18 printf STDERR "SLuRM rx-NOTIFY(%d): %v02X\n", $seqno, $payload
334             if DEBUG;
335              
336 9 100       87 $_on_notify ? $_on_notify->( ( __CLASS__->is_multidrop ? ( $node_id ) : () ), $payload )
    50          
337             : warn "Received NOTIFY packet with no handler\n";
338             }
339              
340             case( SLURM_PKTCTRL_REQUEST ) {
341 3         4 printf STDERR "SLuRM rx-REQUEST(%d): %v02X\n", $seqno, $payload
342             if DEBUG;
343              
344 3   66     21 $_rx_nodestate //= Device::Serial::SLuRM::_NodeState->new;
345 3 100       13 if( my $slot = $_rx_nodestate->pending_slot( $seqno ) ) {
346             await $self->_send_response( $node_id, $seqno, $slot->{payload} )
347 1 50       8 if defined $slot->{payload};
348             }
349             else {
350 2 50       6 next if $is_dup;
351              
352             # TODO: If multidrop we need to ignore requests except for us
353              
354 2         20 $_rx_nodestate->set_pending_slot( $seqno,
355             {
356             # no payload yet
357             start_time => [ gettimeofday ],
358             }
359             );
360              
361 2 50       5 if( $_on_request ) {
362 2         7 $_on_request->( $seqno, $payload );
363             }
364             else {
365 0         0 warn "Received REQUEST packet with no handler\n";
366             }
367             }
368             }
369              
370             case( SLURM_PKTCTRL_RESPONSE ),
371             case( SLURM_PKTCTRL_ERR ) {
372 9         42 my $slot = $nodestate->pending_slot( $seqno );
373 9 50       83 unless( $slot ) {
374 0         0 warn "Received reply to unsent request seqno=$seqno\n";
375 0         0 next;
376             }
377              
378             $METRICS and
379 9 50       32 $METRICS->report_timer( request_duration => tv_interval $slot->{start_time} );
380              
381             # Send the first ACK before completing the future
382 9         15 printf STDERR "SLuRM tx-ACK(%d)\n", $seqno
383             if DEBUG;
384              
385 9         55 await $_protocol->send( SLURM_PKTCTRL_ACK | $seqno, $node_id | 0x80, "" );
386              
387 9 100       20463 if( $pktctrl == SLURM_PKTCTRL_RESPONSE ) {
388 8         20 printf STDERR "SLuRM rx-RESPONSE(%d): %v02X\n", $seqno, $payload
389             if DEBUG;
390              
391 8         46 $slot->{response_f}->done( $payload );
392             }
393             else {
394 1         3 printf STDERR "SLuRM rx-ERR(%d): %v02X\n", $seqno, $payload
395             if DEBUG;
396              
397 1 50       14 my $message = sprintf "Received ERR packet <%v02X%s>",
398             substr( $payload, 0, 3 ),
399             length $payload > 3 ? "..." : "";
400 1         15 $slot->{response_f}->fail( $message, slurm => $payload );
401             }
402 9         3381 $slot->{retransmit_f}->cancel;
403              
404             $METRICS and
405 9 50       511 $METRICS->report_distribution( request_success_attempts => 1 + $_retransmit_count - $slot->{retransmit_count} );
406              
407             # Second ACK
408 9         89 await $_protocol->send( SLURM_PKTCTRL_ACK | $seqno, $node_id | 0x80, "",
409             linestuff => true,
410             );
411              
412 9         554 $nodestate->clear_pending_slot( $seqno );
413             }
414              
415             case( SLURM_PKTCTRL_ACK ) {
416 4 50       66 $_rx_nodestate or next;
417 4 100       16 my $slot = $_rx_nodestate->pending_slot( $seqno ) or next;
418              
419 2         9 $_rx_nodestate->clear_pending_slot( $seqno );
420             }
421              
422             default {
423 0         0 warn sprintf "Received unrecognised packet type=%02X\n", $pktctrl;
424             }
425             }
426 27 100 100     166 }
    100          
    100          
    50          
427             }
428              
429             field $_selector;
430             method _selector
431             {
432             return $_selector if $_selector;
433              
434             $_selector = Future::Selector->new;
435             $_selector->add(
436             data => "runloop",
437             f => $_run_f = $self->_run
438             ->set_label( "Device::Serial::SLuRM runloop" )
439 0     0   0 ->on_fail( sub { die "Device::Serial::SLuRM runloop failed: $_[0]" } ),
440             );
441              
442             return $_selector;
443             }
444              
445             =head2 run
446              
447             $run_f = $slurm->run( %args );
448              
449             Starts the receiver run-loop, which can be used to wait for incoming NOTIFY
450             packets. This method returns a future, but the returned future will not
451             complete in normal circumstances. It will remain pending while the run-loop is
452             running. If an unrecoverable error happens (such as an IO error on the
453             underlying serial port device) then this future will fail.
454              
455             Takes the following named arguments:
456              
457             =over 4
458              
459             =item on_notify => CODE
460              
461             $on_notify->( $payload )
462              
463             Optional. Invoked on receipt of a NOTIFY packet.
464              
465             =back
466              
467             Will automatically L first if required.
468              
469             =cut
470              
471             async method _autoreset
472 10     10   45 {
473 10   66     71 my $nodestate = $_nodestate[0] //= Device::Serial::SLuRM::_NodeState->new;
474              
475 10 100       44 $nodestate->did_reset or
476             await $self->_reset( 0 );
477             }
478              
479 11     11 1 61583 async method run ( %args )
  11         60  
  11         34  
  11         20  
480 11         33 {
481 11         30 $_on_notify = $args{on_notify}; # TODO: save old, restore on exit?
482              
483 11         81 my $s = $self->_selector;
484 11         30 my $f = $_run_f;
485              
486             # Ugh this is a terrible way to do this
487 11 100       54 if( my $handle_request = $args{handle_request} ) {
488             # Currently undocumented pending an idea of how to do a receiver subclass
489             $_on_request = sub {
490 2     2   7 my ( $seqno, $payload ) = @_;
491              
492             my $ret_f = $handle_request->( $payload )
493             ->then(
494             sub { # on_done
495 2         513 my ( $response ) = @_;
496             # TODO: insert my own node ID
497             # TODO: Consider reporting a metric?
498 2         11 return $self->_send_response( 0, $seqno, $response );
499             },
500             sub { # on_fail
501             # TODO: Consider reporting a metric?
502 0         0 warn "TODO: handle_request failed, we should send ERR somehow";
503             },
504 2         10 );
505              
506 2         4298 $s->add( f => $ret_f, data => undef );
507 2         18 };
508             }
509              
510 11         49 await $self->_autoreset;
511              
512 9         548 await $s->run_until_ready( $f );
513             }
514              
515             =head2 stop
516              
517             $slurm->stop;
518              
519             Stops the receiver run-loop, if running, causing its future to be cancelled.
520              
521             It is not an error to call this method if the run loop is not running.
522              
523             =cut
524              
525             method stop
526             {
527             return unless $_run_f;
528              
529             eval { $_run_f->cancel } or warn "Failed to ->cancel the runloop future - $@";
530             undef $_run_f;
531             undef $_selector;
532             }
533              
534             =head2 send_packet
535              
536             await $slurm->send_packet( $pktctrl, $payload );
537              
538             Sends a packet to the serial port.
539              
540             =cut
541              
542 1     1 1 4 async method send_packet ( $pktctrl, $payload ) { await $_protocol->send( $pktctrl, undef, $payload ); }
  1         2  
  1         4  
  1         2  
  1         3  
  1         2  
  1         6  
543              
544             =head2 reset
545              
546             $slurm->reset;
547              
548             Resets the transmitter sequence number and sends a META-RESET packet.
549              
550             It is not normally required to explicitly call this, as the first call to
551             L, L or L will do it if required.
552              
553             =cut
554              
555 2     2 1 9004 method reset () { $self->_reset( 0 ); }
  2         10  
  2         3  
  2         11  
556              
557 7     7   20 async method _reset ( $node_id )
  7         33  
  7         17  
  7         14  
558 7         62 {
559 7         50 my $s = $self->_selector;
560              
561 7   66     78 my $nodestate = $_nodestate[ $node_id ] //= Device::Serial::SLuRM::_NodeState->new;
562              
563 7         36 $nodestate->seqno_tx = 0;
564              
565             # Need to create this before sending because of unit testing
566 7         43 $_next_resetack_f = $_run_f->new;
567              
568 7         91 await $_protocol->send_twice( SLURM_PKTCTRL_META_RESET, $node_id | 0x80, pack "C", $nodestate->seqno_tx );
569 7         10478 $nodestate->did_reset = 1;
570              
571             # TODO: These might collide, do we need a Queue?
572 7         70 await $s->run_until_ready( Future->wait_any(
573             $_next_resetack_f,
574             Future::IO->sleep( $_retransmit_delay * 3 ),
575             ) );
576 5 50       13221 die "Timed out waiting for reset\n"
577             unless $_next_resetack_f->is_done;
578 5         66 undef $_next_resetack_f;
579             }
580              
581             =head2 send_notify
582              
583             await $slurm->send_notify( $payload );
584              
585             Sends a NOTIFY packet.
586              
587             Will automatically L first if required.
588              
589             =cut
590              
591 2     2 1 17802 method send_notify ( $payload ) { $self->_send_notify( 0, $payload ); }
  2         15  
  2         6  
  2         4  
  2         8  
592              
593 4     4   12 async method _send_notify ( $node_id, $payload )
  4         18  
  4         9  
  4         75  
  4         12  
594 4         10 {
595 4   33     24 my $nodestate = $_nodestate[ $node_id ] //= Device::Serial::SLuRM::_NodeState->new;
596              
597 4 50       22 $nodestate->did_reset or
598             await $self->_reset( $node_id );
599              
600 4         19 ( $nodestate->seqno_tx += 1 ) &= 0x0F;
601 4         12 my $seqno = $nodestate->seqno_tx;
602              
603 4         10 printf STDERR "SLuRM tx-NOTIFY(%d): %v02X\n", $seqno, $payload
604             if DEBUG;
605              
606 4         12 my $pktctrl = SLURM_PKTCTRL_NOTIFY | $seqno;
607              
608 4         48 await $_protocol->send_twice( $pktctrl, $node_id | 0x80, $payload );
609             }
610              
611             =head2 request
612              
613             $data_in = await $slurm->request( $data_out );
614              
615             Sends a REQUEST packet, and waits for a response to it.
616              
617             If the peer responds with an ERR packet, the returned future will fail with
618             an error message, the category of C, and the payload body of the ERR
619             packet in the message details:
620              
621             $f->fail( $message, slurm => $payload );
622              
623             If the peer does not respond at all and all retransmit attempts end in a
624             timeout, the returned future will fail the same way but with C as the
625             message details:
626              
627             $f->fail( $message, slurm => undef );
628              
629             Will automatically L first if required.
630              
631             =cut
632              
633 8     8 1 13735 method request ( $payload ) { $self->_request( 0, $payload ); }
  8         41  
  8         22  
  8         14  
  8         38  
634              
635 11     11   24 async method _request ( $node_id, $payload )
  11         46  
  11         21  
  11         24  
  11         20  
636 11         26 {
637 11         84 my $s = $self->_selector;
638              
639 11   66     89 my $nodestate = $_nodestate[ $node_id ] //= Device::Serial::SLuRM::_NodeState->new;
640              
641 11 100       49 $nodestate->did_reset or
642             await $self->_reset( $node_id );
643              
644 11         296 ( $nodestate->seqno_tx += 1 ) &= 0x0F;
645 11         33 my $seqno = $nodestate->seqno_tx;
646              
647 11         45 printf STDERR "SLuRM tx-REQUEST(%d): %v02X\n", $seqno, $payload
648             if DEBUG;
649              
650 11 50       46 $nodestate->pending_slot( $seqno ) and croak "TODO: Request seqno collision - pick a new one?";
651              
652 11         33 my $pktctrl = SLURM_PKTCTRL_REQUEST | $seqno;
653              
654 11         66 await $_protocol->send( $pktctrl, $node_id | 0x80, $payload );
655              
656 11         18786 $nodestate->set_pending_slot( $seqno,
657             {
658             payload => $payload,
659             response_f => my $f = $_run_f->new,
660             retransmit_count => $_retransmit_count,
661             start_time => [ gettimeofday ],
662             }
663             );
664              
665 11         70 $self->_set_retransmit( $node_id, $seqno );
666              
667 11         16092 return await $f;
668             }
669              
670 3     3   91 async method _send_response ( $node_id, $seqno, $payload )
  3         14  
  3         6  
  3         6  
  3         7  
  3         5  
671 3         9 {
672 3         4 printf STDERR "SLuRM tx-RESPONSE(%d): %v02X\n", $seqno, $payload
673             if DEBUG;
674              
675 3         9 my $pktctrl = SLURM_PKTCTRL_RESPONSE | $seqno;
676              
677 3   50     15 my $slot = ( $_rx_nodestate // die "ARGH cannot _send_response without a valid _rx_nodestate" )
678             ->pending_slot( $seqno );
679              
680 3         9 $slot->{payload} = $payload;
681              
682 3         15 await $_protocol->send( $pktctrl, $node_id, $payload );
683             }
684              
685 14     14   30 method _set_retransmit ( $node_id, $seqno )
  14         52  
  14         26  
  14         29  
  14         51  
686             {
687 14   33     100 my $nodestate = $_nodestate[ $node_id ] //= Device::Serial::SLuRM::_NodeState->new;
688              
689 14 50       59 my $slot = $nodestate->pending_slot( $seqno ) or die "ARG expected $seqno request";
690              
691             $slot->{retransmit_f} = Future::IO->sleep( $_retransmit_delay )
692             ->on_done( sub {
693 4 100   4   3891 if( $slot->{retransmit_count}-- ) {
694 3         10 printf STDERR "SLuRM retransmit REQUEST(%d)\n", $seqno
695             if DEBUG;
696              
697 3         7 my $pktctrl = SLURM_PKTCTRL_REQUEST | $seqno;
698             $slot->{retransmit_f} = $_protocol->send( $pktctrl, $node_id | 0x80, $slot->{payload} )
699             ->on_fail( sub {
700 0         0 warn "Retransmit failed: @_";
701 0         0 $slot->{response_f}->fail( @_ );
702             } )
703             ->on_done( sub {
704 3         1039 $self->_set_retransmit( $node_id, $seqno );
705 3         24 } );
706              
707 3 50       5669 $METRICS and
708             $METRICS->inc_counter( retransmits => );
709             }
710             else {
711 1         3 printf STDERR "SLuRM timeout REQUEST(%d)\n", $seqno
712             if DEBUG;
713              
714 1         8 my $message = sprintf "Request timed out after %d attempts\n", 1 + $_retransmit_count;
715 1         36 $slot->{response_f}->fail( $message, slurm => undef );
716              
717 1 50       261 $METRICS and
718             $METRICS->inc_counter( timeouts => );
719              
720 1         6 $nodestate->clear_pending_slot( $seqno );
721             }
722 14         78 });
723             }
724              
725             =head1 AUTHOR
726              
727             Paul Evans
728              
729             =cut
730              
731             0x55AA;