├── .gitignore
├── LICENSE
├── README.md
├── composer.json
├── examples
├── client.html
└── server.php
└── src
├── ForExampleOnlyHTTPServer.php
└── RatchetEventBridge.php
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | .idea/
3 |
4 | composer.lock
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Michael Calcinai
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # phpi-websocket
2 | Scaffolding and demo for interacting with [phpi](https://github.com/calcinai/phpi) via websockets.
3 |
4 | This should be enough to get started with a WS server and client. The included demo will let you monitor the pin headers, select alternate
5 | functions and change pin levels.
6 |
7 | ## Instalation
8 |
9 | Installation should be done using composer as there are a few dependencies (especially for ratchet - sorry about this but they are all just tiny symphony components).
10 |
11 | If you don't have composer already:
12 |
13 | ```$ curl -sS https://getcomposer.org/installer | php```
14 |
15 | Then install
16 |
17 | ```$ ./composer.phar create-project calcinai/phpi-websocket```
18 |
19 | Alternatively, you can download the current release with all its dependencies from the releases page.
20 |
21 | ## Usage
22 |
23 | You can test the server by running
24 |
25 | ```php examples/server.php```
26 |
27 | This will run a simple HTTP & WebSocket server on port 9999. If the hostname of the pi is not the default (raspberrypi.local), you'll need
28 | to set it to something that can be accessed in `server.php`
29 |
30 | All going well, you should be able to access the example at http://raspberrypi.local:9999/ and see something like this:
31 |
32 | 
33 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "calcinai/phpi-websocket",
3 | "description": "Scaffolding for Websocket server using Ratchet & PHPi",
4 | "type": "application",
5 | "keywords": ["raspberry", "pi", "rpi", "gpio", "websocket"],
6 | "require": {
7 | "calcinai/phpi": "^0.3.0",
8 | "cboden/ratchet": "^0.3.5",
9 | "evenement/evenement": "^2.0"
10 | },
11 | "license": "MIT",
12 | "authors": [
13 | {
14 | "name": "Michael Calcinai",
15 | "email": "michael@calcin.ai"
16 | }
17 | ],
18 | "autoload": {
19 | "psr-4": {
20 | "": "src/"
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/examples/client.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
138 |
142 |
143 |
144 |
145 |
146 |
312 |
313 |
314 |
315 |
--------------------------------------------------------------------------------
/examples/server.php:
--------------------------------------------------------------------------------
1 |
5 | */
6 |
7 | //TODO make this work from vendor dir
8 | include __DIR__.'/../vendor/autoload.php';
9 |
10 | use Calcinai\PHPi\Board;
11 | use Calcinai\PHPi\Pin\PinFunction;
12 | use Ratchet\ConnectionInterface;
13 |
14 | //The actual WS construction is a bit messy, but it shows the general idea.
15 | $loop = \React\EventLoop\Factory::create();
16 | $board = \Calcinai\PHPi\Factory::create($loop);
17 |
18 | $http_server = new ForExampleOnlyHTTPServer(__DIR__.'/client.html');
19 | $controller = new RatchetEventBridge();
20 |
21 | //This is like a vhost, if it donesn't match the host header you'll get a 404
22 | $app = new Ratchet\App('raspberrypi.local', 9999, '0.0.0.0', $loop);
23 | //Route the root to serve the html file
24 | $app->route('/', $http_server, ['*']);
25 | //Route to ws server
26 | $app->route('/phpi', $controller, ['*']);
27 |
28 |
29 |
30 | //Some sort of visible heartbeat. No functional use - display only.
31 | $loop->addPeriodicTimer(1, function() use($controller){
32 | $controller->broadcast('time', date('r'));
33 | });
34 |
35 |
36 |
37 |
38 | //The following are the events triggered by the client:
39 | /**
40 | * Send the pins and their functions on connect
41 | * Also send board meta
42 | */
43 | $controller->on('client.connect', function(ConnectionInterface $connection) use($controller, $board) {
44 |
45 | //Prepare the pins in a useful format for the client
46 | $headers = $board->getPhysicalPins();
47 | foreach($headers as &$header){
48 | foreach($header as $pin_number => &$physical_pin){
49 | if($physical_pin->gpio_number !== null){
50 | $pin = $board->getPin($physical_pin->gpio_number);
51 | $physical_pin->function = $pin->getFunction();
52 | $physical_pin->alternate_functions = $pin->getAltFunctions();
53 | $physical_pin->level = $pin->getLevel();
54 | }
55 | }
56 | }
57 |
58 | $controller->send($connection, 'board.headers', $headers);
59 | $controller->send($connection, 'board.meta', $board->getMeta());
60 | });
61 |
62 |
63 | /**
64 | * Handle pin function changes from the client.
65 | */
66 | $controller->on('pin.function', function($data) use($board) {
67 | $board->getPin($data['pin'])->setFunction($data['func']);
68 | });
69 |
70 |
71 | /**
72 | * Handle pin level changes from the client.
73 | * The pin is automatically set to output if it isn't already.
74 | */
75 | $controller->on('pin.level', function($data) use($board) {
76 | $pin = $board->getPin($data['pin']);
77 |
78 | //Just to help out!
79 | if($pin->getFunction() !== PinFunction::OUTPUT){
80 | $pin->setFunction(PinFunction::OUTPUT);
81 | }
82 |
83 | $data['level'] ? $pin->high() : $pin->low();
84 | });
85 |
86 |
87 |
88 |
89 |
90 |
91 | //Set up the update events server -> client
92 | //This is a bit of a duplicate of above but it's for clarity as an example.
93 | $headers = $board->getPhysicalPins();
94 | foreach($headers as $header) {
95 | foreach($header as $pin_number => $physical_pin) {
96 | if($physical_pin->gpio_number !== null) {
97 | $pin = $board->getPin($physical_pin->gpio_number);
98 |
99 | //Need to add a callback on these pins to the client too for level and function changes.
100 | $pin->on(\Calcinai\PHPi\Pin::EVENT_FUNCTION_CHANGE, function($new_function) use($pin, $controller){
101 | $controller->broadcast('pin.function', ['pin' => $pin->getPinNumber(), 'func' => $new_function]);
102 | });
103 |
104 | $pin->on(\Calcinai\PHPi\Pin::EVENT_LEVEL_CHANGE, function() use($pin, $controller){
105 | $controller->broadcast('pin.level', ['pin' => $pin->getPinNumber(), 'level' => $pin->getLevel()]);
106 | });
107 | }
108 | }
109 | }
110 |
111 | $app->run();
112 |
--------------------------------------------------------------------------------
/src/ForExampleOnlyHTTPServer.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | *
9 | * This class only exists to make the example easy to use. It needs a lot more thought if/before it's actually used for a project.
10 | * I'd strongly recommend just using apache or nginx to serve the static content.
11 | *
12 | * You could alternatively use php's built in webserver if you're using a late enough version.
13 | *
14 | */
15 | class ForExampleOnlyHTTPServer implements \Ratchet\Http\HttpServerInterface {
16 |
17 | private $path;
18 |
19 | public function __construct($path) {
20 | $this->path = $path;
21 | }
22 |
23 |
24 | /**
25 | * This is called before or after a socket is closed (depends on how it's closed). SendMessage to $conn will not result in an error if it has already been closed.
26 | * @param ConnectionInterface $conn The socket/connection that is closing/closed
27 | * @throws \Exception
28 | */
29 | function onClose(ConnectionInterface $conn) {
30 | // TODO: Implement onClose() method.
31 | }
32 |
33 | /**
34 | * If there is an error with one of the sockets, or somewhere in the application where an Exception is thrown,
35 | * the Exception is sent back down the stack, handled by the Server and bubbled back up the application through this method
36 | * @param ConnectionInterface $conn
37 | * @param \Exception $e
38 | * @throws \Exception
39 | */
40 | function onError(ConnectionInterface $conn, \Exception $e) {
41 | // TODO: Implement onError() method.
42 | }
43 |
44 | /**
45 | * Triggered when a client sends data through the socket
46 | * @param \Ratchet\ConnectionInterface $from The socket/connection that sent the message to your application
47 | * @param string $msg The message received
48 | * @throws \Exception
49 | */
50 | function onMessage(ConnectionInterface $from, $msg) {
51 | // TODO: Implement onMessage() method.
52 | }
53 |
54 | /**
55 | * @param \Ratchet\ConnectionInterface $conn
56 | * @param \Guzzle\Http\Message\RequestInterface $request null is default because PHP won't let me overload; don't pass null!!!
57 | * @throws \UnexpectedValueException if a RequestInterface is not passed
58 | */
59 | public function onOpen(ConnectionInterface $conn, \Guzzle\Http\Message\RequestInterface $request = null) {
60 | $response = new \Guzzle\Http\Message\Response(200);
61 | $response->setBody(file_get_contents($this->path));
62 | $conn->send($response);
63 | $conn->close();
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/RatchetEventBridge.php:
--------------------------------------------------------------------------------
1 |
6 | */
7 | class RatchetEventBridge implements \Ratchet\MessageComponentInterface {
8 |
9 | use \Evenement\EventEmitterTrait;
10 |
11 | /**
12 | * @var SplObjectStorage
13 | */
14 | private $connections;
15 |
16 |
17 | public function __construct() {
18 | $this->connections = new SplObjectStorage();
19 | }
20 |
21 |
22 | /**
23 | * When a new connection is opened it will be passed to this method
24 | * @param \Ratchet\ConnectionInterface $connection The socket/connection that just connected to your application
25 | * @throws \Exception
26 | */
27 | function onOpen(\Ratchet\ConnectionInterface $connection) {
28 |
29 | //Do some checking here to check authorization
30 |
31 |
32 | $this->connections->attach($connection);
33 | $this->emit('client.connect', [$connection]);
34 | }
35 |
36 | /**
37 | * This is called before or after a socket is closed (depends on how it's closed). SendMessage to $connection will not result in an error if it has already been closed.
38 | * @param \Ratchet\ConnectionInterface $connection The socket/connection that is closing/closed
39 | * @throws \Exception
40 | */
41 | function onClose(\Ratchet\ConnectionInterface $connection) {
42 | $this->emit('client.close', [$connection]);
43 | $this->connections->detach($connection);
44 | }
45 |
46 | /**
47 | * If there is an error with one of the sockets, or somewhere in the application where an Exception is thrown,
48 | * the Exception is sent back down the stack, handled by the Server and bubbled back up the application through this method
49 | * @param \Ratchet\ConnectionInterface $connection
50 | * @param \Exception $e
51 | * @throws \Exception
52 | */
53 | function onError(\Ratchet\ConnectionInterface $connection, \Exception $e) {
54 | $this->emit('client.error', [$connection]);
55 | $this->connections->detach($connection);
56 | }
57 |
58 | /**
59 | * Triggered when a client sends data through the socket
60 | * @param \Ratchet\ConnectionInterface $from The socket/connection that sent the message to your application
61 | * @param string $message The message received
62 | * @throws \Exception
63 | */
64 | function onMessage(\Ratchet\ConnectionInterface $from, $message) {
65 | $components = @json_decode($message, true);
66 |
67 | if($message === false || !isset($components['event'])){
68 | throw new \Exception('Bad message payload received');
69 | }
70 |
71 | $this->emit($components['event'], [$components['data'], $from]);
72 |
73 | }
74 |
75 | public function send(\Ratchet\ConnectionInterface $to, $event, $data = null){
76 | $to->send(json_encode([
77 | 'event' => $event,
78 | 'data' => $data
79 | ]));
80 | }
81 |
82 | public function broadcast($event, $data){
83 | foreach($this->connections as $connection){
84 | $this->send($connection, $event, $data);
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------