├── .gitignore
├── LICENSE
├── README.md
├── breadboard.png
└── esp-opc-server.ino
/.gitignore:
--------------------------------------------------------------------------------
1 | *~
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2017, Patrick Pelletier
2 |
3 | All rights reserved.
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 |
8 | * Redistributions of source code must retain the above copyright
9 | notice, this list of conditions and the following disclaimer.
10 |
11 | * Redistributions in binary form must reproduce the above
12 | copyright notice, this list of conditions and the following
13 | disclaimer in the documentation and/or other materials provided
14 | with the distribution.
15 |
16 | * Neither the name of Patrick Pelletier nor the names of other
17 | contributors may be used to endorse or promote products derived
18 | from this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This is an [Arduino][1] sketch which implements an
2 | [Open Pixel Control][2] server for controlling [NeoPixels][3] from an
3 | [ESP8266][5], such as the [Adafruit Feather HUZZAH][4].
4 |
5 | Here is an example circuit:
6 |
7 |
8 |
9 | Bill of materials:
10 |
11 | * [Adafruit Feather HUZZAH ESP8266][4]
12 | * [74AHCT125 - Quad Level-Shifter][6]
13 | * [470 ohm resistor][7]
14 | * [Big Freaking Capacitor - 4700uF][8]
15 | * [NeoPixel stick][9]
16 |
17 | You will need to edit the sketch to set the number of pixels you have,
18 | and your WiFi SSID and password.
19 |
20 | Once the sketch is running, you'll need to figure out which IP address
21 | it has been assigned. Generally, your router's web interface will
22 | have a way to look at DHCP leases, and you can find it from there.
23 |
24 | It may also be useful to call [WiFi.hostname()][13] to assign a
25 | hostname to your ESP board. This may show up as `hostname.lan` in
26 | your DNS, although it [depends on your router][14].
27 |
28 | Once you know the IP address or hostname, you can connect with any
29 | Open Pixel Control client. The [openpixelcontrol repository][10] has
30 | some [clients in Python][11], and I have [a client in Haskell][12].
31 |
32 | ## Caveats
33 |
34 | This example is a nice proof-of-concept, but there are a couple of
35 | reasons it isn't a robust solution.
36 |
37 | First, interrupts are disabled while writing to NeoPixels. Interrupts
38 | are necessary for WiFi to work, so the server may eventually crash and
39 | need to be rebooted, especially if you have a lot of NeoPixels. (This
40 | problem could be alleviated by using [DotStars][15] instead of
41 | NeoPixels, because DotStars are not timing-sensitive and do not need
42 | to disable interrupts.)
43 |
44 | Second, Open Pixel Control runs over TCP, and
45 | [TCP is not the best for real-time applications][16]. If a packet is
46 | lost, TCP will retransmit it, delaying the packets after it.
47 | [UDP][17] would be more suitable for this application.
48 |
49 | [1]: https://www.arduino.cc/
50 | [2]: http://openpixelcontrol.org/
51 | [3]: https://www.adafruit.com/category/168
52 | [4]: https://www.adafruit.com/products/2821
53 | [5]: https://github.com/esp8266/Arduino
54 | [6]: https://www.adafruit.com/products/1787
55 | [7]: https://www.adafruit.com/products/2781
56 | [8]: https://www.adafruit.com/products/1589
57 | [9]: https://www.adafruit.com/products/1426
58 | [10]: https://github.com/zestyping/openpixelcontrol/
59 | [11]: https://github.com/zestyping/openpixelcontrol/tree/master/python
60 | [12]: https://github.com/ppelleti/hs-opc-client
61 | [13]: https://arduino-esp8266.readthedocs.io/en/latest/esp8266wifi/station-class.html#hostname
62 | [14]: https://unix.stackexchange.com/questions/92441/whats-the-difference-between-local-home-and-lan
63 | [15]: https://www.adafruit.com/category/885
64 | [16]: https://en.wikipedia.org/wiki/Transmission_Control_Protocol#Alternatives
65 | [17]: https://en.wikipedia.org/wiki/User_Datagram_Protocol
66 |
--------------------------------------------------------------------------------
/breadboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mignon-p/esp-opc-server/646e8a4c82740fdb7acd331ee065ebe853d3a776/breadboard.png
--------------------------------------------------------------------------------
/esp-opc-server.ino:
--------------------------------------------------------------------------------
1 | /* This Arduino sketch implements an Open Pixel Control server
2 | * for controlling NeoPixels from an ESP8266, such as the Adafruit
3 | * Feather HUZZAH.
4 | */
5 |
6 | #include
7 | #include
8 |
9 | /* Edit this to configure the number of pixels you have, and your SSID and password */
10 | #define PIN 2 // pin that NeoPixels are connected to
11 | #define N_PIXELS 8 // number of NeoPixels on your strip
12 | #define WIFI_SSID "Your SSID"
13 | #define WIFI_PASSWD "Your password"
14 |
15 | Adafruit_NeoPixel strip = Adafruit_NeoPixel(N_PIXELS, PIN, NEO_GRB + NEO_KHZ800);
16 | WiFiServer server(7890);
17 |
18 | void setup() {
19 | uint16_t i = 0;
20 |
21 | strip.begin();
22 | strip.show(); // Initialize all pixels to 'off'
23 |
24 | // display a moving red pixel while connecting to WiFi
25 | WiFi.begin(WIFI_SSID, WIFI_PASSWD);
26 | while (WiFi.status() != WL_CONNECTED) {
27 | strip.setPixelColor(i, strip.Color(32, 0, 0));
28 | strip.show();
29 | delay(200);
30 | strip.setPixelColor(i, strip.Color(0, 0, 0));
31 | i++;
32 | if (i >= strip.numPixels())
33 | i = 0;
34 | }
35 |
36 | // all pixels green once we've connected to WiFi
37 | for (i = 0; i < strip.numPixels(); i++)
38 | strip.setPixelColor(i, strip.Color(0, 32, 0));
39 | strip.show();
40 |
41 | server.begin();
42 | }
43 |
44 | void loop() {
45 | WiFiClient client = server.available();
46 | if (client) {
47 | while (client.connected()) {
48 | readFrame(client);
49 | yield();
50 | }
51 | client.stop();
52 | }
53 | }
54 |
55 | int readFully (WiFiClient &client, uint8_t *buf, size_t len) {
56 | size_t i;
57 |
58 | for (i = 0; i < len; i++) {
59 | int b;
60 |
61 | if ((b = blockingRead(client)) < 0)
62 | return -1;
63 |
64 | buf[i] = (uint8_t) b;
65 | }
66 |
67 | return 0;
68 | }
69 |
70 | int blockingRead (WiFiClient &client) {
71 | int b;
72 | while ((b = client.read()) < 0) {
73 | yield();
74 | if (! client.connected())
75 | return -1;
76 | }
77 | return b;
78 | }
79 |
80 | void readFrame(WiFiClient &client) {
81 | uint8_t buf4[4];
82 | uint8_t cmd;
83 | size_t payload_length, leds_in_payload, i;
84 |
85 | // read channel number (ignored), command, and length
86 | if (readFully (client, buf4, sizeof (buf4)) < 0)
87 | return;
88 |
89 | cmd = buf4[1];
90 | payload_length = (((size_t) buf4[2]) << 8) + (size_t) buf4[3];
91 | leds_in_payload = payload_length / 3;
92 | if (leds_in_payload > strip.numPixels())
93 | leds_in_payload = strip.numPixels();
94 | if (cmd != 0) // we only support command 0, set pixel colors
95 | leds_in_payload = 0;
96 |
97 | // read pixel data; 3 bytes per pixel
98 | for (i = 0; i < leds_in_payload; i++) {
99 | if (readFully (client, buf4, 3) < 0)
100 | return;
101 | strip.setPixelColor(i, strip.Color(buf4[0], buf4[1], buf4[2]));
102 | }
103 | strip.show();
104 |
105 | // discard any remaining data (e. g. if they sent us more pixels than we have)
106 | payload_length -= leds_in_payload * 3;
107 |
108 | for (; payload_length != 0; payload_length--) {
109 | if (blockingRead(client) < 0)
110 | return;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------