2018-02-18
I learned event-based programming recently
On 8 and 9 February last week I attended the Surf Security and Privacy conference. SURFcert, the incident response team of SURF, had its own 'side event' within this conference, an escape room. Since the members of SURFcert like to visit escape rooms themselves, the idea was to build our own escape room. A simple one as teams of 2 or 3 people had to solve it within 15 minutes. The best scores were indeed just over 5 minutes so it was doable.The theme of this escape room was the trip Snowden made: from the US to Hongkong to Moscow. Each location had a puzzle and like Snowden the only thing you could take to the next location was knowledge. In this case a 4-digit code to open a lock. Someone else in the SURFcert team did most of the hardware work and I decided to dive into some programming to support this effort. The escape room needed a countdown clock that could only be stopped by the right code. My idea was to use a barcode scanner to link the stop action to scanning the barcode on an object. So I installed a Raspberry Pi with a raspbian desktop and found out how to set up the autorun on the Pi so my program would be started at startup when the user 'pi' logs in automatically. This was done by starting it from ~/.config/lxsession/LXDE-pi/autorun. The program I wrote had three inputs:
The escape room clockFor the barcodes I used an usb barcode scanner I have lying around. It behaves like a usb keyboard so scanning a barcode will cause the code to be entered as keystrokes with an enter key at the end, But all programming I do is sequential. This is different, I needed to write an event-based program. It needs to react to time events, enter events and needs to check the state of gpio bits on time events. And on certain events it needs to change the global state (reset, running, stopped). The last time I did any event-based programming was an irc-bot written in Perl 4. So with a lot of google searches, copypasting bits of code, searching a lot for which input bits would be default high and go low when connected to earth and a lot of trying I wrote a program. It uses WxPerl to have a graphical interface and use events. I'm not saying its a good program, but it did the job. Notable things:
- A reset switch connected to GPIO pin 11 and ground
- A start button connected to GPIO pin 03 and ground
- Entering the right barcode to stop the time. In the end this was the barcode of a real Russian bottle of vodka, so my program needed vodka as input
- The OnInit function sets up everything: a window with minimal decorations, tries to set it full-screen, a text box that will show the time and starts at 15:00 as static text. A handler for time events that will be called 10 times per second. And an input box and a handler for when the enter key is pressed.
- The onTimer function that looks at global state and decides which inputs are valid in that state and handles them
- The onenter function that calculates a sha256 hash of the input line and checks which inputs can change the global state. The hash was to make sure that someone who could have a look at the source still had no idea what the commands were to control it all via keyboard. And no keyboard was connected anyway. The input for a shutdown is the barcode from one of the loyalty cards I carry around.
#!/usr/bin/env perl use warnings; use strict; use Wx; use Wx::Event qw(EVT_TIMER EVT_TEXT_ENTER); package theclock; use Digest::SHA qw(sha256 sha256_hex); use Device::BCM2835; Device::BCM2835::init() || die "Can't init lib"; # set pins 11 and 16 as input with default high, pull low Device::BCM2835::gpio_fsel(&Device::BCM2835::RPI_BPLUS_GPIO_J8_11, &Device::BCM2835::BCM2835_GPIO_FSEL_INPT); Device::BCM2835::gpio_fsel(&Device::BCM2835::RPI_BPLUS_GPIO_J8_11, &Device::BCM2835::BCM2835_GPIO_PUD_UP); Device::BCM2835::gpio_fsel(&Device::BCM2835::RPI_BPLUS_GPIO_J8_03, &Device::BCM2835::BCM2835_GPIO_FSEL_INPT); Device::BCM2835::gpio_fsel(&Device::BCM2835::RPI_BPLUS_GPIO_J8_03, &Device::BCM2835::BCM2835_GPIO_PUD_UP); my $pps = 10; my $playseconds = 900; use base 'Wx::App'; sub OnInit { my $self = shift; my $frame = Wx::Frame->new( undef, -1, 'SURFcert Escape Room clock', [0,0], [1280,1024], [-1,-1], &Wx::wxFULLSCREEN_ALL | &Wx::wxMAXIMIZE_BOX | &Wx::wxCLOSE_BOX, ); $self->{frame}=$frame; $frame->SetBackgroundColour(&Wx::wxBLACK); my $timetext = Wx::StaticText->new( $frame, "-1", "15:00.00" ); $timetext->SetFont( Wx::Font->new( 300)); $timetext->SetForegroundColour(&Wx::wxWHITE); $self->{timetext} = $timetext; &resettime($self); &showthetime($self); my $timer = Wx::Timer->new( $self ); $timer->Start( 1000/$pps ); # in millisec &Wx::Event::EVT_TIMER($self, -1, \&onTimer); $self->{counting} = 0; my $textctrl = Wx::TextCtrl->new( $frame, -1, "", [300,850], [600,10], &Wx::wxTE_PROCESS_ENTER, ); $self->{inputfornumber} = $textctrl; &Wx::Event::EVT_TEXT_ENTER($self, -1, \&onenter); $frame->Show; $frame->ShowFullScreen(1); return 1; } sub showthetime { my($self) = @_; my $readable = $self->{timeleft}/$pps; $self->{timetext}->SetLabel(sprintf("%02d:%02d",$readable / 60,$readable % 60)); } sub resettime { my($self) = @_; $self->{timeleft} = $playseconds*$pps; $self->{counting} = 0; } sub onTimer { # draait $pps keer per seconde # taken # 1: controleer inputs # sleutel: reset # knopje: start tellen # getal ingevoerd: is het het juiste? # 2: aftellen indien counting # stoppen als tijd op my($self, $event) = @_; # pin 11 = reset if (Device::BCM2835::gpio_lev(&Device::BCM2835::RPI_BPLUS_GPIO_J8_11) == LOW){ &resettime($self); } # pin 03 = start if (($self->{counting} == 0) and ($self->{timeleft} == $pps*$playseconds) and (Device::BCM2835::gpio_lev(&Device::BCM2835::RPI_BPLUS_GPIO_J8_03) == LOW)){ $self->{counting} = 1; } if ($self->{counting} == 1){ # en tel af $self->{timeleft}--; if ($self->{timeleft} > 0){ $self->{timetext}->SetForegroundColour(&Wx::wxWHITE); &showthetime($self); } else { $self->{timetext}->SetForegroundColour(&Wx::wxRED); &showthetime($self); # en stop $self->{counting} = 0; } } else { # niet aan het tellen, maar aan het begin? if ($self->{timeleft} == $pps*$playseconds){ $self->{timetext}->SetForegroundColour(&Wx::wxGREEN); &showthetime($self); } } } sub onenter { my $self = shift; my $enteredtext = $self->{inputfornumber}->GetValue(); my $entereddigest = sha256_hex($enteredtext); #print "Text : ".$enteredtext." digest ".$entereddigest."\n"; if (($self->{counting} == 0) and ($self->{timeleft} == $pps*$playseconds) and ($entereddigest eq "cced28c6dc3f99c2396a5eaad732bf6b28142335892b1cd0e6af6cdb53f5ccfa")){ # fake startknop is geldig $self->{counting} = 1; } if ($entereddigest eq "01be30bb4a27765c37462e6bf2a0bf8b6c109f9be9d81e6fd56455db1a736a43"){ # fake resetknop &resettime($self); } if ($entereddigest eq "0d8c402fe9991f3e85487c244f2016704ee1ac53c855f1164033d06cef08a0a9"){ system("sudo shutdown -h now"); } if ($entereddigest eq "8577da2ea54085708b3b851bc50315a36bb740ba5135e747cfb12457b5d3060f"){ $self->ExitMainLoop; } if ($self->{counting} == 1){ # wel aan het tellen # controleer textinput if ($entereddigest eq "746665e59eb0b2cef285bf153ca8c31626e33275fecc1d3a259a6f1576d1a9c5"){ # correcte textinvoer $self->{counting} = 0; $self->{timetext}->SetForegroundColour(&Wx::wxBLUE); &showthetime($self); } } $self->{inputfornumber}->SetValue(""); } theclock->new->MainLoop;