Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How I (ab)used PHP with the help of ReactPHP

How I (ab)used PHP with the help of ReactPHP

PHP is one of the most accessible and commonly used languages in computer programming. It’s used for building websites, web applications, micro services and loads of other stuff. Normally we would not use it to control hardware. But let that be the showcase of this talk. I’ll show you how I (ab)used PHP to control hardware used during track and field competitions. It involves LED-panels, serial-to-ethernet-­convertors and the use of reactphp, an event-driven non-blocking I/O library.

Stijn Vannieuwenhuyse

January 27, 2017
Tweet

More Decks by Stijn Vannieuwenhuyse

Other Decks in Programming

Transcript

  1. how I (ab)used PHP
    with the help of reactphp

    View full-size slide

  2. Stijn Vannieuwenhuyse
    head of engineering at teamleader

    View full-size slide

  3. Stijn Vannieuwenhuyse
    track and field timer

    View full-size slide

  4. Track & Field
    it's all about precise numbers

    View full-size slide

  5. Team Arctic v.z.w.
    team for athletics' results communication, timing, information and coordination

    View full-size slide

  6. Track & Field
    it's all about precise numbers

    View full-size slide

  7. What's wrong with it?
    Why change?

    View full-size slide

  8. Problem 1
    How to integrate?

    View full-size slide

  9. FT01
    Long jump men



    Lambert Romain
    DAMP


    A6

    Rank: 1

    View full-size slide

  10. Problem 2
    How to connect?

    View full-size slide

  11. PHP
    developer

    View full-size slide

  12. reactphp
    event-driven, non-blocking I/O with PHP

    View full-size slide

  13. public function run()
    {
    $this->running = true;
    while ($this->running) {
    $this->nextTickQueue->tick();
    $this->futureTickQueue->tick();
    $this->timers->tick();
    if (!$this->running || !$this->nextTickQueue->isEmpty() || !$this->futureTickQueue->isEmpty()) {
    $timeout = 0;
    } elseif ($scheduledAt = $this->timers->getFirst()) {
    $timeout = $scheduledAt - $this->timers->getTime();
    if ($timeout < 0) {
    $timeout = 0;
    } else {
    $timeout *= self::MICROSECONDS_PER_SECOND;
    }
    } elseif ($this->readStreams || $this->writeStreams) {
    $timeout = null;
    } else {
    break;
    }
    $this->waitForStreamActivity($timeout);
    }
    }

    View full-size slide

  14. composer require react/react

    View full-size slide

  15. Problem 2
    How to connect?

    View full-size slide

  16. $loop = LoopFactory::create();
    $connector = new TcpConnector($loop);
    $connector->create('192.168.1.93', 6000)->then(
    function(Stream $stream) {
    echo "connected", PHP_EOL;
    }
    );
    $loop->run();

    View full-size slide

  17. composer require react/socket-client

    View full-size slide

  18. Problem 3
    How to communicate?

    View full-size slide

  19. TFF000000a2000700100030a012345678910\x0d

    View full-size slide

  20. TFF000000a2000700100030a012345678910\x0d
    TRRGGBBXXXXYYYYLLLLHHHHFDDDDDDDDDDDD\x0d
    T: command name
    RRGGBB: hex color value
    XXXX: x-position in hex, starting from left
    YYYY: y-position in hex, starting from top
    LLLL: length of the text in hex
    HHHH: heigth of the text in hex
    F: font-type
    D: data
    \x0d: end marker

    View full-size slide

  21. Domain Driven Design
    make the implicit explicit

    View full-size slide

  22. interface Instruction
    {
    /**
    * @return string
    */
    public function toData();
    }

    View full-size slide

  23. class Clear implements Instruction
    {
    public function toData()
    {
    return "C\x0d";
    }
    }

    View full-size slide

  24. class Text implements Instruction
    {
    private $data;
    private $x;
    private $y;
    private $height;
    private $length;
    private $colorCode;
    private $font;
    public function __construct($data, $x, $y, $height, $length, $colorCode, $font)
    {
    $this->data = $data;
    $this->x = $x;
    $this->y = $y;
    $this->height = $height;
    $this->length = $length;
    $this->colorCode = $colorCode;
    $this->font = $font;
    }
    public function toData()
    {
    return sprintf(
    "T%s%04X%04X%04X%04X%s%s\x0d",
    $this->getColor(), //returns hex string of red, green or yellow
    $this->x,
    $this->y,
    $this->length,
    $this->height,
    $this->font,
    $this->data
    );
    }
    }

    View full-size slide

  25. B00100028004700af0f0f0f0fff0fff0f\x0d

    View full-size slide

  26. BXXXXYYYYLLLLHHHHDDDDDDDDDDD\x0d
    B: command name
    XXXX: x-position in hex, starting from left
    YYYY: y-position in hex, starting from top
    LLLL: length of the text in hex
    HHHH: heigth of the text in hex
    D: data
    \x0d: end marker

    View full-size slide

  27. Domain Specific Language

    View full-size slide

  28. . . . . . . . . . . . Y Y Y Y Y Y Y . . . . . . . . . . . . . . . . . . . Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y
    R R R R R R R R R R R Y . . . . . Y G G G G G G G G G G G G G G G G G G . Y . . . . . . . . . . . . . . . . . . . . . . . . . .
    . . . . . R . . . . . Y . Y Y Y . Y . . . . . G . . . . . G . . . . . G . Y . . . . . . . . . . . . . . . . . . . . . . . . . .
    . . R . . R . R R R . Y . . . Y . Y . G . G . G . G G G . G . G G G . G . Y . . . . . . . . . . . . . . . . . . . . . . . . . .
    . . R . . R . . . R . Y . Y Y Y . Y . G . G . G . G . . . G . G . . . G . Y . . . . . . . . . . . . . . . . . . . . . . . . . .
    . . R . . R . R R R . Y . . . Y . Y . G G G . G . G G G . G . G G G . G . Y . . . . . . . . . . . . . . . . . . . . . . . . . .
    . . R . . R . R . . . Y . Y Y Y . Y . . . G . G . . . G . G . G . G . G . Y . . . . . . . . . . . . . . . . . . . . . . . . . .
    . . R . . R . R R R . Y . . . . . Y . . . G . G . G G G . G . G G G . G . Y . . . . . . . . . . . . . . . . . . . . . . . . . .
    . . . . . R . . . . . Y . . . . . Y . . . . . G . . . . . G . . . . . G . Y . . . . . . . . . . . . . . . . . . . . . . . . . .
    Y Y Y Y Y Y Y Y Y Y Y Y . . . . . Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y Y . . . . . . . . . . . . . . . . . . . . . . . . . .

    View full-size slide

  29. Value Objects

    View full-size slide

  30. new Bitmap()
    new BitmapBlock()
    new BitmapLine()

    View full-size slide

  31. Dot Matrix
    Fonts

    View full-size slide

  32. final class AdvancedDotDigital extends Font
    {
    protected static $chars = [
    "0" => "01111110\n11111111\n11000011\n11000011\n11000011\n11000011\n11000011\n11000011\n11000011\n11000011\n11111111\n01111110",
    "1" => "00011000\n00111000\n01111000\n11011000\n00011000\n00011000\n00011000\n00011000\n00011000\n00011000\n11111111\n11111111",
    "2" => "01111110\n11111111\n11000011\n00000011\n00000011\n01111111\n11111110\n11000000\n11000000\n11000000\n11111111\n11111111",
    "3" => "01111110\n11111111\n11000011\n00000011\n00000011\n00111110\n00111110\n00000011\n00000011\n11000011\n11111111\n01111110",
    "4" => "11000011\n11000011\n11000011\n11000011\n11000011\n11000011\n11111111\n11111111\n00000011\n00000011\n00000011\n00000011",
    "5" => "11111111\n11111111\n11000000\n11000000\n11111110\n11111111\n00000011\n00000011\n00000011\n11000011\n11111111\n01111110",
    "6" => "01111110\n11111111\n11000011\n11000000\n11111110\n11111111\n11000011\n11000011\n11000011\n11000011\n11111111\n01111110",
    "7" => "11111111\n11111111\n11000110\n00000110\n00001100\n00001100\n00011000\n00011000\n00110000\n00110000\n00110000\n00110000",
    "8" => "01111110\n11111111\n11000011\n11000011\n11000011\n01111110\n01111110\n11000011\n11000011\n11000011\n11111111\n01111110",
    "9" => "01111110\n11111111\n11000011\n11000011\n11000011\n11000011\n11111111\n01111111\n00000011\n11000011\n11111111\n01111110",
    "m" => "0000000000\n0000000000\n0000000000\n0000000000\n0000000000\n1111101110\n1111111111\n1100110011\n1100110011\n1100110011\n1100110011\n1100110011\n1100110011",
    "X" => "1100000000011\n0110000000110\n0011000001100\n0001100011000\n0000110110000\n0000011100000\n0000011100000\n0000110110000\n0001100011000\n0011000001100\n0110000000110\n1100000000011",
    "O" => "0000111110000\n0011111111100\n0111000001110\n0110000000110\n1100000000011\n1100000000011\n1100000000011\n1100000000011\n0110000000110\n0111000001110\n0011111111100\n0000111110000",
    ":" => "0000\n0000\n0110\n0110\n0000\n0000\n0000\n0000\n0110\n0110\n0000\n0000",
    "-" => "0000000000000\n0000000000000\n0000000000000\n0000000000000\n0000000000000\n0011111111100\n0011111111100\n0000000000000\n0000000000000\n0000000000000\n0000000000000\n0000000000000",
    ];
    }

    View full-size slide

  33. Problem 4
    How to get updates?

    View full-size slide

  34. inotify_init() : filedescriptor
    inotify_add_watch($fd, $path, $mask) : watchDescriptorId
    inotify_rm_watch($fd, $wdId)
    inotify_read($fd) : event[]
    IN_MODIFY
    IN_CREATE
    IN_ALL_EVENTS
    IN_OPEN
    ...

    View full-size slide

  35. class Watcher
    {
    public function __construct($dir, LoopInterface $loop)
    {
    $fd = inotify_init();
    stream_set_blocking(fd, 0);
    inotify_add_watch($fd, $dir, IN_MODIFY);
    $loop->addReadStream($fd,
    function($fd) {
    foreach(inotify_read($fd) as $events) {
    //process change
    }
    }
    )
    }
    }

    View full-size slide

  36. Problem 5
    How to combine I/O?

    View full-size slide

  37. interface Layout
    {
    /**
    * @return Instruction[]
    */
    public function toInstructions();
    }

    View full-size slide

  38. class NextAthlete implements Layout
    {
    private $name;
    private $team;
    private $attempt;
    private $place = null;
    private $height = null;
    public function __construct($name, $team, $attempt, $place = null, $height = null)
    {
    $this->name = $name;
    $this->team = $team;
    $this->attempt = $attempt;
    $this->place = $place;
    $this->height = $height;
    }
    /**
    * @return Instruction[]
    */
    public function toInstructions()
    {
    $instructions = [];
    $name = (new Text(strtoupper($this->name), RingMatrix::class, "Y"))->toBitmap();
    $instructions[] = new Bitmap(0, 0, $name);
    $team = (new Text(strtoupper($this->team), SquareDotDigital::class, "R"))->toBitmap();
    $instructions[] = new Bitmap(64 - $team->getLength(), 11, $team);
    //some other conversion from data to instructions
    return $instructions;
    }
    }

    View full-size slide

  39. Problem 6
    How to send own layouts?

    View full-size slide

  40. PHP TCP
    server

    View full-size slide

  41. $loop = Factory::create();
    $server = new Server($loop);
    $server->on('connection', function (Connection $connection) {
    echo sprintf("%s came online\n", $connection->getRemoteAddress());
    $connection->on('close', function(Stream $stream) use ($connection) {
    echo sprintf("%s went offline\n", $connection->getRemoteAddress());
    });
    });
    $server->listen('6000', '0.0.0.0');
    $loop->run();

    View full-size slide

  42. Command
    pattern

    View full-size slide

  43. {
    "commandName":"Nortuni.FieldScoreboardManager.Commands.ShowLayout",
    "payload": {
    "scoreBoardId": "LED01",
    "layoutName": "TwoLines",
    "data": {
    "line1": "TEAM",
    "line2": "ARCTIC"
    }
    }
    }

    View full-size slide

  44. $loop = Factory::create();
    $deviceServer = new Server($loop);
    $commandServer = new Server($loop);
    $deviceServer->on('connection', function (Connection $connection) {
    //do some stuff
    });
    $commandServer->on('connection', function (Connection $connection) {
    $connection->on('data', function() {
    //parse json to command
    //dispatch command
    })
    });
    $deviceServer->listen('6000', '0.0.0.0');
    $commandServer->listen('6001', '0.0.0.0');
    $loop->run();

    View full-size slide

  45. Problem 7
    How to open up control?

    View full-size slide

  46. $socketServer = new Server($loop);
    $webserver = new React\Http\Server($socketServer);
    $webserver->on('request', function ($request, $response) {
    //just like a normal controller method
    //$response is a writable stream
    });
    $socket->listen(8080);

    View full-size slide

  47. Problem 8
    How to get current state?

    View full-size slide

  48. composer require cboden/ratchet

    View full-size slide

  49. class WebSocketConnector implements MessageComponentInterface
    {
    private $clients;
    public function __construct() {
    $this->clients = new SplObjectStorage;
    }
    public function onOpen(ConnectionInterface $connection)
    {
    $this->clients->attach($conn);
    }
    public function onClose(ConnectionInterface $connection)
    {
    $this->clients->detach($connection);
    }
    public function onError(ConnectionInterface $connection, \Exception $e)
    {
    $connection->close();
    }
    public function onMessage(ConnectionInterface $from, $msg)
    {
    //process data
    }
    public function sendToClients($data)
    {
    foreach($this->clients as $client) {
    $client->send($data);
    }
    }
    }

    View full-size slide

  50. $socketServer = new Ratchet\App('192.168.15.100', 8080, '0.0.0.0', $loop);
    $socketServer->route('/field', new WebSocketConnector(), ['*']);

    View full-size slide

  51. var connection = null;
    var connect = function() {
    connection = new WebSocket('ws://192.168.15.100:8080/field');
    connection.onopen = function() {
    //log or ...
    };
    connection.onmessage = function(message) {
    //process message.data
    }
    connection.onclose = function() {
    connection = null;
    setTimeout(connect, 1000);
    };
    }
    var dispatch = function(data) {
    if(connection && connection.readyState == WebSocket.OPEN) {
    connection.send(data);
    }
    };

    View full-size slide

  52. Problem 9
    How to automate?

    View full-size slide

  53. {
    "commandName":"Nortuni.FieldScoreboardManager.Commands.ShowSlideshow",
    "payload": {
    "scoreBoardId":"LED01",
    "interval":"20",
    "screens":[
    {
    "layoutName":"MeetingName",
    "data": {
    "line1":"PHPGENT",
    "line2":"MEETUP",
    "line3":"",
    "line4":"WELCOME"
    }
    },
    {
    "layoutName":"MeetingName",
    "data": {
    "line1":"",
    "line2":"TEAMLEADER",
    "line3":"CRM",
    "line4":""
    }
    }
    ]
    }
    }

    View full-size slide

  54. $this->timer = $this->loop->addPeriodicTimer($interval, function() {
    $this->showNextSlide();
    });

    View full-size slide

  55. it works
    for now

    View full-size slide

  56. my experiences

    View full-size slide

  57. async programming in PHP
    is possible

    View full-size slide

  58. php is stable enough
    for long running processes

    View full-size slide

  59. reactphp is a
    mature and rich library

    View full-size slide

  60. I use reactphp
    in production

    View full-size slide

  61. I shouldn't
    use php for this

    View full-size slide

  62. Stijn Vannieuwenhuyse
    @stijnvnh
    https://joind.in/talk/822f4

    View full-size slide