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.

0cab45c75f109eb044564efbd28af603?s=128

Stijn Vannieuwenhuyse

January 27, 2017
Tweet

Transcript

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

  2. Stijn Vannieuwenhuyse head of engineering at teamleader

  3. Stijn Vannieuwenhuyse track and field timer

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

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

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

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

  9. Problem 1 How to integrate?

  10. FT01<TR><TD> Long jump men </TD></TR> <TR> <TD CLASS="DAMP"><span class="teamflag"></span></TD> <TD

    CLASS="name">Lambert Romain</TD> <TD CLASS="team">DAMP</TD> </TR> <TR> <TD CLASS="attempt">A6</TD> <TD CLASS="result"> </TD> <TD CLASS="rank">Rank:&nbsp;1</TD> </TR>
  11. RS232

  12. None
  13. Problem 2 How to connect?

  14. PHP developer

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

  16. 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); } }
  17. composer require react/react

  18. Problem 2 How to connect?

  19. $loop = LoopFactory::create(); $connector = new TcpConnector($loop); $connector->create('192.168.1.93', 6000)->then( function(Stream

    $stream) { echo "connected", PHP_EOL; } ); $loop->run();
  20. composer require react/socket-client

  21. Problem 3 How to communicate?

  22. C\x0d

  23. TFF000000a2000700100030a012345678910\x0d

  24. 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
  25. Domain Driven Design make the implicit explicit

  26. interface Instruction { /** * @return string */ public function

    toData(); }
  27. class Clear implements Instruction { public function toData() { return

    "C\x0d"; } }
  28. 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 ); } }
  29. B00100028004700af0f0f0f0fff0fff0f\x0d

  30. 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
  31. Domain Specific Language

  32. . . . . . . . . . .

    . 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 . . . . . . . . . . . . . . . . . . . . . . . . . .
  33. Value Objects

  34. new Bitmap() new BitmapBlock() new BitmapLine()

  35. Dot Matrix Fonts

  36. 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", ]; }
  37. Problem 4 How to get updates?

  38. inotify

  39. 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 ...
  40. 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 } } ) } }
  41. Problem 5 How to combine I/O?

  42. interface Layout { /** * @return Instruction[] */ public function

    toInstructions(); }
  43. 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; } }
  44. Problem 6 How to send own layouts?

  45. PHP TCP server

  46. $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();
  47. Command pattern

  48. { "commandName":"Nortuni.FieldScoreboardManager.Commands.ShowLayout", "payload": { "scoreBoardId": "LED01", "layoutName": "TwoLines", "data": {

    "line1": "TEAM", "line2": "ARCTIC" } } }
  49. $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();
  50. Problem 7 How to open up control?

  51. $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);
  52. Problem 8 How to get current state?

  53. websockets

  54. composer require cboden/ratchet

  55. 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); } } }
  56. $socketServer = new Ratchet\App('192.168.15.100', 8080, '0.0.0.0', $loop); $socketServer->route('/field', new WebSocketConnector(),

    ['*']);
  57. 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); } };
  58. Problem 9 How to automate?

  59. { "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":"" } } ] } }
  60. timers

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

  62. it works for now

  63. my experiences

  64. async programming in PHP is possible

  65. php is stable enough for long running processes

  66. reactphp is a mature and rich library

  67. I use reactphp in production

  68. I shouldn't use php for this

  69. Questions?

  70. sponsorship info@team-arctic.be

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