How to do Perl state machine (FSM) to parse bitstream (byte sequence)?

181 Views Asked by At

I'm currently using Perl to parse incoming command sequences which comes from RS232 serial port. I try to use state machine, and its expected behavior is: (1) Receive a series of bytes from serial port; (2) The state machine uses the bytes as input, and jump to appropriate states.

I came up with a simplified demo Perl code (posted below), but encountered a problem: When the code enters "while(1){}", it gets stuck here, and cannot get out. Consequently, the $din byte sequence assignments is blocked by "while(1){}", and is invisible to the state machine. So, the FSM is stuck in "INIT" state, and just do NOT jump at all.

I figured this should be a very easy or entry-level practice in Perl coding, but searching thru Google does not help me too much. Can anyone help me with this? Thanks in advance~

...
my %next_state = (
        "INIT" => sub{
                $din eq "AA" and return "HEADER0" ;
                return "INIT"                     ;
                },
        "HEADER0" => sub{
                $din eq "99" and return "HEADER1" ;
                return "INIT"                     ;
                },
        ...
        );

# Set state machine's initial state.
my $cur_state = "INIT"   ;

# Integer for debugging purpose.
my $itgi = 0;

# Run the state machine.
while(1){
        $cur_state = $next_state{$cur_state}();
        print "$itgi, will jump to: $cur_state\n\n";
        $itgi++;
        }

# Send in input byte sequence, which simulates
# incoming bytes from RS-232 COM port:
$din = "AA"     ;
sleep(1)        ;
...

========== 2020.10.09 22:10 Update ==========

Thanks to the help of @ikegami after some effort and debugging job, now I can get my little sweet Perl state machine up and running, code as posted below.

However, it still has a problem, that is:

The input byte sequence (viz. @seq) must be non-0x00 values; if I put a 0x00 into the command sequence, then the FSM will exit when when it encounters the 0x00.

Why is this? The code uses "$cur_byte >= 0", which seems to me should be capable of handling 0x00 just as it handles non-zero values.

Why does 0x00 pull the state machine out of running?

use strict      ;
use warnings    ;

# input to the state machine
my $din ;

#---------------------------------------------------------------------
# FSM's state table.
#---------------------------------------------------------------------
# Expected input sequence is:
#   AA 99 00 01 ....
# In which:
#   (1) Fixed pattern "AA" and "99" are two bytes of header,
#   (2) Following bytes are uart ID, etc.
my %next_state = (
        "INIT" => sub{
                # If receives "AA" from input,
                # then jumpt to "HEADER0" state:
                $din eq "AA" and return "HEADER0" ; 
                # Otherwise just stay here:
                return "INIT"                     ; 
                },
        "HEADER0" => sub{
                # If receives "99" from input,
                # then proceed to "HEADER1" state:
                $din eq "99" and return "HEADER1" ; 
                # Otherwise, return to initial state:
                return "INIT"                     ; 
                },
        "HEADER1" => sub{
                # Capture first byte of uart ID:
                return "UARTID0";
                },
        "UARTID0" => sub{
                # Capture second byte of uart ID:
                return "UARTID1";
                },
        "UARTID1" => sub{
                # Capture second byte of uart ID:
                return "FINISHED";
                },
        "FINISHED" => sub{
                return "INIT";
                },
        );

#---------------------------------------------------------------------
# Set state machine's initial state.
#---------------------------------------------------------------------
my $cur_state = "INIT"   ;

#---------------------------------------------------------------------
# Send in command sequence
#---------------------------------------------------------------------
my @seq = (-1, 0xAA, -1, 0x99, -1, 0x06, -1, 0x07,
           -1, 0x08, -1, 0x09, -1, 0x0a, -1, 0x0b,
           -1, 0x0c, -1, 0x0d
          );

sub get_next_byte {
        while (@seq) { #(A)
                my $cur_byte = shift(@seq);
                return $cur_byte if $cur_byte >= 0;
                #
                sleep(-$cur_byte);
                }
        return (); #(B)
        }

#---------------------------------------------------------------------
# Run the state machine.
#---------------------------------------------------------------------
# Integer for debugging purpose.
my $itgi = 0;

while( $din = get_next_byte() ){ #(C)
        $din = sprintf("%02X",$din);
        $cur_state = $next_state{$cur_state}();
        print "-- Iteration $itgi, will jump to: $cur_state\n";
        $itgi++;
        }

print "-- Program finish.\n";
2

There are 2 best solutions below

10
On BEST ANSWER

You enter the loop without ever changing $din. You need something like

# Run the state machine.
while ( my ($din) = get_next_byte() ) {
   $din = sprintf("%02X", $din);
   $cur_state = $next_state{$cur_state}();
   print "$itgi, will jump to: $cur_state\n\n";
   $itgi++;
}

For testing purposes, you could use

my @seq = (-1, 0xAA, -1, 0x99);

sub get_next_byte {
   while (@seq) {
      my $next = shift(@seq);
      return $next if $next >= 0;
      sleep(-$next);
   }

   return ();
}
0
On

Thanks to the help @zdim and @ikegami, now I finally completely worked out this program. I'll post my working code as below, in case someone may have the same question.

Following code is inspired by zdim:

use strict      ;
use warnings    ;

# input to the state machine
my $din ;

# FSM's state table.
# Expected input sequence is:
#   AA 99 00 01 ....
# In which:
#   "AA" and "99" are two bytes of header,
#   "00" and "01" are two bytes of uart ID.
my %next_state = (
        "INIT" => sub{
                # If receives "AA" from input,
                # then jumpt to "HEADER0" state:
                $din eq "AA" and return "HEADER0" ;
                # Otherwise just stay here:
                return "INIT"                     ;
                },
        "HEADER0" => sub{
                # If receives "99" from input,
                # then proceed to "HEADER1" state:
                $din eq "99" and return "HEADER1" ;
                # Otherwise, return to initial state:
                return "INIT"                     ;
                },
        "HEADER1" => sub{
                # Capture first byte of uart ID:
                return "UARTID0";
                },
        "UARTID0" => sub{
                # Capture second byte of uart ID:
                return "UARTID1";
                },
#        "UARTID1" => sub{
#                return "FINISHED";
#                },
        "FINISHED" => sub{
                return "INIT";
                },
        );

# Set state machine's initial state.
my $cur_state = "INIT"   ;

# Integer for debugging purpose.
my $itgi = 0;

# Run the state machine.
while($din = <>){
        chomp $din ;
        $cur_state = $next_state{$cur_state}();
        print "$itgi, will jump to: $cur_state\n\n";
        $itgi++;
        }

# Send in input bytes:
$din = "AA"     ;
sleep(1)        ;
$din = "99"     ;
sleep(1)        ;

Following dode is inspired by ikegami, and pay attention to the difference between ($din) and simply $din without parentheses: with parens, we get a TRUE or FALSE result; without parens, we get the actual element value of @seq, and if this value is 0x00 then while will become while(0) and will exit.

use strict      ;
use warnings    ;

# input to the state machine
my $din ;

#---------------------------------------------------------------------
# FSM's state table.
#---------------------------------------------------------------------
# Expected input sequence is:
#   AA 99 00 01 ....
# In which:
#   (1) Fixed pattern "AA" and "99" are two bytes of header,
#   (2) Following bytes are uart ID, etc.
my %next_state = (
        "INIT" => sub{
                # If receives "AA" from input,
                # then jumpt to "HEADER0" state:
                $din eq "AA" and return "HEADER0" ; #(D)
                # Otherwise just stay here:
                return "INIT"                     ; 
                },
        "HEADER0" => sub{
                # If receives "99" from input,
                # then proceed to "HEADER1" state:
                $din eq "99" and return "HEADER1" ; 
                # Otherwise, return to initial state:
                return "INIT"                     ; 
                },
        "HEADER1" => sub{
                # Capture first byte of uart ID:
                return "UARTID0";
                },
        "UARTID0" => sub{
                # Capture second byte of uart ID:
                return "UARTID1";
                },
        "UARTID1" => sub{
                # Capture second byte of uart ID:
                return "FINISHED";
                },
        "FINISHED" => sub{
                return "INIT";
                },
        );

#---------------------------------------------------------------------
# Set state machine's initial state.
#---------------------------------------------------------------------
my $cur_state = "INIT"   ;

#---------------------------------------------------------------------
# Send in command sequence
#---------------------------------------------------------------------
my @seq = (-1, 0xAA, -1, 0x99, -1, 0x00, -1, 0x00,
           -1, 0x00, -1, 0x00, -1, 0x0a, -1, 0x0b,
           -1, 0x0c, -1, 0x0d
           );

sub get_next_byte {
        while (@seq) { #(A)
                my $cur_byte = shift(@seq);
                return $cur_byte if $cur_byte >= 0;
                #
                sleep(-$cur_byte);
                }
        return (); #(B)
        }

#---------------------------------------------------------------------
# Run the state machine.
#---------------------------------------------------------------------
# Integer for debugging purpose.
my $itgi = 0;

##--while( my ($din) = get_next_byte() ){ #(C)
    while(    ($din) = get_next_byte() ){ #(C)
        $din = sprintf("%02X",$din);
        $cur_state = $next_state{$cur_state}();
        print "-- Iteration $itgi, will jump to: $cur_state\n";
        $itgi++;
        }

print "-- Program finish.\n";