├── .gitignore ├── .travis.yml ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── demos ├── clock.py ├── fill.py ├── fonts │ ├── EuropeanTeletextNuevo.ttf │ ├── LICENSE │ └── PixelOperator8.ttf ├── gameoflife.py ├── image.py ├── lib │ ├── __init__.py │ └── imageToBinary.py ├── video.py └── webcam.py ├── include ├── Hanover_Flipdot.h ├── README └── main.h ├── lib └── README ├── platformio.ini ├── src ├── Hanover_FlipDot.cpp └── main.cpp └── test └── README /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore list for PlatformIO 2 | .pio 3 | .vscode/.browse.c_cpp.db* 4 | .vscode/c_cpp_properties.json 5 | .vscode/launch.json 6 | .vscode/ipch 7 | 8 | # Byte-compiled / optimized / DLL files 9 | __pycache__/ 10 | *.py[cod] 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Continuous Integration (CI) is the practice, in software 2 | # engineering, of merging all developer working copies with a shared mainline 3 | # several times a day < https://docs.platformio.org/page/ci/index.html > 4 | # 5 | # Documentation: 6 | # 7 | # * Travis CI Embedded Builds with PlatformIO 8 | # < https://docs.travis-ci.com/user/integration/platformio/ > 9 | # 10 | # * PlatformIO integration with Travis CI 11 | # < https://docs.platformio.org/page/ci/travis.html > 12 | # 13 | # * User Guide for `platformio ci` command 14 | # < https://docs.platformio.org/page/userguide/cmd_ci.html > 15 | # 16 | # 17 | # Please choose one of the following templates (proposed below) and uncomment 18 | # it (remove "# " before each line) or use own configuration according to the 19 | # Travis CI documentation (see above). 20 | # 21 | 22 | 23 | # 24 | # Template #1: General project. Test it using existing `platformio.ini`. 25 | # 26 | 27 | # language: python 28 | # python: 29 | # - "2.7" 30 | # 31 | # sudo: false 32 | # cache: 33 | # directories: 34 | # - "~/.platformio" 35 | # 36 | # install: 37 | # - pip install -U platformio 38 | # - platformio update 39 | # 40 | # script: 41 | # - platformio run 42 | 43 | 44 | # 45 | # Template #2: The project is intended to be used as a library with examples. 46 | # 47 | 48 | # language: python 49 | # python: 50 | # - "2.7" 51 | # 52 | # sudo: false 53 | # cache: 54 | # directories: 55 | # - "~/.platformio" 56 | # 57 | # env: 58 | # - PLATFORMIO_CI_SRC=path/to/test/file.c 59 | # - PLATFORMIO_CI_SRC=examples/file.ino 60 | # - PLATFORMIO_CI_SRC=path/to/test/directory 61 | # 62 | # install: 63 | # - pip install -U platformio 64 | # - platformio update 65 | # 66 | # script: 67 | # - platformio ci --lib="." --board=ID_1 --board=ID_2 --board=ID_N 68 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "platformio.platformio-ide" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cmake.configureOnOpen": false 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Andrew Yong 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ESP32 Hanover Flip-dot Sign Controller 2 | 3 | ## PCB 4 | 5 | Reference PCB is available at [ndoo/esp32-hanover-flipdot](https://github.com/ndoo/esp32-hanover-flipdot) 6 | 7 | ## Demo Video 8 | 9 | [ESP32 Hanover Flip-dot Sign Controller - Demos](https://youtu.be/oSv1yEUelBg) 10 | 11 | ## Firmware 12 | 13 | ### IDE Prerequisites 14 | 15 | You will need a working [VS Code](https://code.visualstudio.com/) and [PlatformIO](https://platformio.org/) development environment, with ESP32 support installed. 16 | 17 | Libraries should automatically download as they have been specified in `platformio.ini`. 18 | 19 | ### First Use 20 | 21 | #### Powering the Board 22 | 23 | A freshly assembled board will not have any sketch running on it, and will be in bootloader bode waiting for firmware upload over serial. You will need a TTL USB-serial adapter, and one of the following methods of powering the board: 24 | 25 | * Connect Tx, Rx, VCC and GND to USB-serial adapter - **USB-serial adapter must be set to 3.3V VCC or the ESP32 SIP will be irreversibly damaged** 26 | * Connect Tx, Rx and GND to USB-serial adapter, then connect 18VDC power supply - **ground PCB to USB-serial adapter before connecting DC supply to avoid ESD** 27 | 28 | **Do not connect RS-232 level signalling to the board, the ESP32 SIP will be damaged by signalling above TTL levels.** 29 | 30 | #### Compiling and Uploading Sketch 31 | 32 | The default `platformio.ini` provided here is configured to OTA upload for your convenience, however that means you need to edit this file for the initial flash, in order to program over USB-serial, as the board is not running firmware capable of OTA updates. 33 | 34 | Comment these 2 lines out by preceding them with a semicolon (`;`): 35 | 36 | ``` 37 | upload_protocol = espota 38 | upload_port = Hanover-Flipdot.local 39 | ``` 40 | 41 | Then, test the build by clicking the tick in the lower left status bar of VS Code + PlatformIO. 42 | 43 | If the build succeeds, click the rightwards-pointing arrow in the lower left status bar of VS Code + PlatformIO. This will link the firmware and attempt to upload it to the board's ESP32 over USB-serial. 44 | 45 | #### Configuring Wi-Fi 46 | 47 | You should see a new wireless AP with the SSID `ESP_`. Connect to it and you should be automatically redirected to a portal that allows you to connect the ESP32 to your Wi-Fi. 48 | 49 | If the connection is successful, you should be able to send data to the board using the Python scripts found in the `demos` folder. If the connection fails, the ESP32 will automatically time out and re-start the temporary wireless AP for you to continue Wi-Fi setup. 50 | 51 | Don't forget to uncomment the previously commented-out lines in `platformio.ini` to re-enable OTA uploads. 52 | 53 | #### Rescue Programming 54 | 55 | If you need to re-upload a sketch over serial, take note that this board does not have automatic boot and reset circuitry, so you will need to press the two buttons on the board in the following sequence to enter download mode, before uploading a sketch: 56 | 57 | 1. Hold down RST button 58 | 2. Hold down PGM button 59 | 3. Release RST button 60 | 4. Release PGM button 61 | 62 | This may take a few tries to get the timing right, as the buttons are tiny and placed close together. 63 | 64 | #### Troubleshooting 65 | 66 | * Board not receiving UDP packets 67 | * The board, by default, listends on multicast IP 239.1.2.3 on UDP port 8080 (port can be changed in `include/main.h`) 68 | * Your LAN routers, switches or Wi-Fi access points may be configured to drop multicast packets, or may need IGMP snooping to be enabled 69 | * OTA not working on Windows due to Hanover-Flipdot.local not resolving 70 | * You may need to install an mDNS resolver software such as [Apple's Bonjour Print Services for Windows](https://support.apple.com/kb/DL999) 71 | -------------------------------------------------------------------------------- /demos/clock.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from PIL import Image 4 | from PIL import ImageDraw 5 | from PIL import ImageFont 6 | from datetime import datetime 7 | 8 | from os import path 9 | from time import sleep 10 | import numpy 11 | 12 | import socket 13 | import struct 14 | import sys 15 | import time 16 | 17 | import cv2 18 | 19 | import lib.imageToBinary as i2b 20 | 21 | MULTICAST_GROUP = ('239.1.2.3', 8080) 22 | WIDTH = 128 23 | HEIGHT = 32 24 | WHITE = 1 25 | BLACK = 0 26 | 27 | sock = socket.socket(socket.AF_INET, # Internet 28 | socket.SOCK_DGRAM) # UDP 29 | ttl = struct.pack('b', 1) 30 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) 31 | 32 | image = Image.new('1', (WIDTH, HEIGHT)) 33 | draw = ImageDraw.Draw(image) 34 | clock_font = ImageFont.truetype('fonts/PixelOperator8.ttf', 8) 35 | clock_font_large = ImageFont.truetype('fonts/EuropeanTeletextNuevo.ttf', 16) 36 | 37 | def suffix(d): 38 | return 'th' if 11<=d<=13 else {1:'st',2:'nd',3:'rd'}.get(d%10, 'th') 39 | 40 | def custom_strftime(format, t): 41 | return t.strftime(format).replace('{S}', str(t.day) + suffix(t.day)) 42 | 43 | previous_second = -1 44 | 45 | while True: 46 | time.sleep(datetime.today().microsecond / 1000000) 47 | now = datetime.today() 48 | 49 | draw.rectangle((0, 0, WIDTH, HEIGHT), fill=WHITE, outline=WHITE) 50 | 51 | text = now.strftime('%H:%M') 52 | w, h = draw.textsize(text, clock_font_large) 53 | draw.text(((WIDTH - w) / 2, 4), text, fill=BLACK, font=clock_font_large) 54 | 55 | text = custom_strftime('%A, {S} %b', now) 56 | w, h = draw.textsize(text, clock_font) 57 | draw.text(((WIDTH - w) / 2, 20), text, fill=BLACK, font=clock_font) 58 | 59 | cv2_im = numpy.array(image.convert('RGB')) 60 | cv2_im = cv2_im[:, :, ::-1].copy() 61 | sock.sendto(i2b.imageToBinary(cv2_im, WIDTH, HEIGHT, False), MULTICAST_GROUP) 62 | -------------------------------------------------------------------------------- /demos/fill.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import numpy as np 4 | import socket 5 | import struct 6 | from time import sleep 7 | 8 | MULTICAST_GROUP = ('239.1.2.3', 8080) 9 | 10 | WIDTH = 64 11 | HEIGHT = 32 12 | 13 | board_white = np.packbits(np.ones((WIDTH, HEIGHT), dtype=int)).tobytes() 14 | board_black = np.packbits(np.zeros((WIDTH, HEIGHT), dtype=int)).tobytes() 15 | 16 | sock = socket.socket(socket.AF_INET, # Internet 17 | socket.SOCK_DGRAM) # UDP 18 | ttl = struct.pack('b', 1) 19 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) 20 | 21 | while True: 22 | sock.sendto(board_white, MULTICAST_GROUP) 23 | sleep(3) 24 | sock.sendto(board_black, MULTICAST_GROUP) 25 | sleep(3) 26 | -------------------------------------------------------------------------------- /demos/fonts/EuropeanTeletextNuevo.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ndoo/esp32-hanover-flipdot-firmware/ff6ca67b2d3a7f4b2edad4ee015f693024e9a245/demos/fonts/EuropeanTeletextNuevo.ttf -------------------------------------------------------------------------------- /demos/fonts/LICENSE: -------------------------------------------------------------------------------- 1 | PixelOperator8.ttf, EuropeanTeletextNuevo.ttf: 2 | 3 | Creative Commons Legal Code 4 | 5 | CC0 1.0 Universal 6 | 7 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 8 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 9 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 10 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 11 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 12 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 13 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 14 | HEREUNDER. 15 | 16 | Statement of Purpose 17 | 18 | The laws of most jurisdictions throughout the world automatically confer 19 | exclusive Copyright and Related Rights (defined below) upon the creator 20 | and subsequent owner(s) (each and all, an "owner") of an original work of 21 | authorship and/or a database (each, a "Work"). 22 | 23 | Certain owners wish to permanently relinquish those rights to a Work for 24 | the purpose of contributing to a commons of creative, cultural and 25 | scientific works ("Commons") that the public can reliably and without fear 26 | of later claims of infringement build upon, modify, incorporate in other 27 | works, reuse and redistribute as freely as possible in any form whatsoever 28 | and for any purposes, including without limitation commercial purposes. 29 | These owners may contribute to the Commons to promote the ideal of a free 30 | culture and the further production of creative, cultural and scientific 31 | works, or to gain reputation or greater distribution for their Work in 32 | part through the use and efforts of others. 33 | 34 | For these and/or other purposes and motivations, and without any 35 | expectation of additional consideration or compensation, the person 36 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 37 | is an owner of Copyright and Related Rights in the Work, voluntarily 38 | elects to apply CC0 to the Work and publicly distribute the Work under its 39 | terms, with knowledge of his or her Copyright and Related Rights in the 40 | Work and the meaning and intended legal effect of CC0 on those rights. 41 | 42 | 1. Copyright and Related Rights. A Work made available under CC0 may be 43 | protected by copyright and related or neighboring rights ("Copyright and 44 | Related Rights"). Copyright and Related Rights include, but are not 45 | limited to, the following: 46 | 47 | i. the right to reproduce, adapt, distribute, perform, display, 48 | communicate, and translate a Work; 49 | ii. moral rights retained by the original author(s) and/or performer(s); 50 | iii. publicity and privacy rights pertaining to a person's image or 51 | likeness depicted in a Work; 52 | iv. rights protecting against unfair competition in regards to a Work, 53 | subject to the limitations in paragraph 4(a), below; 54 | v. rights protecting the extraction, dissemination, use and reuse of data 55 | in a Work; 56 | vi. database rights (such as those arising under Directive 96/9/EC of the 57 | European Parliament and of the Council of 11 March 1996 on the legal 58 | protection of databases, and under any national implementation 59 | thereof, including any amended or successor version of such 60 | directive); and 61 | vii. other similar, equivalent or corresponding rights throughout the 62 | world based on applicable law or treaty, and any national 63 | implementations thereof. 64 | 65 | 2. Waiver. To the greatest extent permitted by, but not in contravention 66 | of, applicable law, Affirmer hereby overtly, fully, permanently, 67 | irrevocably and unconditionally waives, abandons, and surrenders all of 68 | Affirmer's Copyright and Related Rights and associated claims and causes 69 | of action, whether now known or unknown (including existing as well as 70 | future claims and causes of action), in the Work (i) in all territories 71 | worldwide, (ii) for the maximum duration provided by applicable law or 72 | treaty (including future time extensions), (iii) in any current or future 73 | medium and for any number of copies, and (iv) for any purpose whatsoever, 74 | including without limitation commercial, advertising or promotional 75 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 76 | member of the public at large and to the detriment of Affirmer's heirs and 77 | successors, fully intending that such Waiver shall not be subject to 78 | revocation, rescission, cancellation, termination, or any other legal or 79 | equitable action to disrupt the quiet enjoyment of the Work by the public 80 | as contemplated by Affirmer's express Statement of Purpose. 81 | 82 | 3. Public License Fallback. Should any part of the Waiver for any reason 83 | be judged legally invalid or ineffective under applicable law, then the 84 | Waiver shall be preserved to the maximum extent permitted taking into 85 | account Affirmer's express Statement of Purpose. In addition, to the 86 | extent the Waiver is so judged Affirmer hereby grants to each affected 87 | person a royalty-free, non transferable, non sublicensable, non exclusive, 88 | irrevocable and unconditional license to exercise Affirmer's Copyright and 89 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 90 | maximum duration provided by applicable law or treaty (including future 91 | time extensions), (iii) in any current or future medium and for any number 92 | of copies, and (iv) for any purpose whatsoever, including without 93 | limitation commercial, advertising or promotional purposes (the 94 | "License"). The License shall be deemed effective as of the date CC0 was 95 | applied by Affirmer to the Work. Should any part of the License for any 96 | reason be judged legally invalid or ineffective under applicable law, such 97 | partial invalidity or ineffectiveness shall not invalidate the remainder 98 | of the License, and in such case Affirmer hereby affirms that he or she 99 | will not (i) exercise any of his or her remaining Copyright and Related 100 | Rights in the Work or (ii) assert any associated claims and causes of 101 | action with respect to the Work, in either case contrary to Affirmer's 102 | express Statement of Purpose. 103 | 104 | 4. Limitations and Disclaimers. 105 | 106 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 107 | surrendered, licensed or otherwise affected by this document. 108 | b. Affirmer offers the Work as-is and makes no representations or 109 | warranties of any kind concerning the Work, express, implied, 110 | statutory or otherwise, including without limitation warranties of 111 | title, merchantability, fitness for a particular purpose, non 112 | infringement, or the absence of latent or other defects, accuracy, or 113 | the present or absence of errors, whether or not discoverable, all to 114 | the greatest extent permissible under applicable law. 115 | c. Affirmer disclaims responsibility for clearing rights of other persons 116 | that may apply to the Work or any use thereof, including without 117 | limitation any person's Copyright and Related Rights in the Work. 118 | Further, Affirmer disclaims responsibility for obtaining any necessary 119 | consents, permissions or other rights required for any use of the 120 | Work. 121 | d. Affirmer understands and acknowledges that Creative Commons is not a 122 | party to this document and has no duty or obligation with respect to 123 | this CC0 or use of the Work. 124 | -------------------------------------------------------------------------------- /demos/fonts/PixelOperator8.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ndoo/esp32-hanover-flipdot-firmware/ff6ca67b2d3a7f4b2edad4ee015f693024e9a245/demos/fonts/PixelOperator8.ttf -------------------------------------------------------------------------------- /demos/gameoflife.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from os import path 4 | 5 | import socket 6 | import struct 7 | import sys 8 | import time 9 | import seagull as sg 10 | import cv2 11 | import seagull.lifeforms as lf 12 | 13 | import numpy as np 14 | 15 | from lib import imageToBinary as i2b 16 | 17 | MULTICAST_GROUP = ('239.1.2.3', 8080) 18 | 19 | WIDTH = 64 20 | HEIGHT = 32 21 | ITERS = 900 22 | 23 | board = sg.Board(size=(HEIGHT, WIDTH)) 24 | 25 | np.random.seed(42) 26 | noise = np.random.choice([0,1], size=(HEIGHT,WIDTH)) 27 | custom_lf = lf.Custom(noise) 28 | board.add(custom_lf, loc=(0,0)) 29 | 30 | sim = sg.Simulator(board) 31 | 32 | sock = socket.socket(socket.AF_INET, # Internet 33 | socket.SOCK_DGRAM) # UDP 34 | ttl = struct.pack('b', 1) 35 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) 36 | 37 | sim.run(sg.rules.conway_classic, iters=ITERS) 38 | 39 | for i in range(ITERS): 40 | 41 | final = np.asarray(sim.get_history()[i] * 255, dtype=np.uint8) 42 | final = cv2.cvtColor(final, cv2.COLOR_GRAY2BGR) 43 | sock.sendto(i2b.imageToBinary(final, WIDTH, HEIGHT), MULTICAST_GROUP) 44 | 45 | time.sleep(0.05) 46 | -------------------------------------------------------------------------------- /demos/image.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from os import path 4 | 5 | import socket 6 | import struct 7 | import sys 8 | import time 9 | import cv2 10 | 11 | from lib import imageToBinary as i2b 12 | 13 | MULTICAST_GROUP = ('239.1.2.3', 8080) 14 | 15 | WIDTH = 128 16 | HEIGHT = 32 17 | 18 | if len(sys.argv) != 2: 19 | print("Usage: " + sys.argv[0] + " [image file]") 20 | exit(1) 21 | 22 | cv2_im = cv2.imread(sys.argv[-1]) 23 | 24 | sock = socket.socket(socket.AF_INET, # Internet 25 | socket.SOCK_DGRAM) # UDP 26 | ttl = struct.pack('b', 1) 27 | 28 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) 29 | sock.sendto(i2b.imageToBinary(cv2.bitwise_not(cv2_im), WIDTH, HEIGHT, False, False), MULTICAST_GROUP) 30 | -------------------------------------------------------------------------------- /demos/lib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ndoo/esp32-hanover-flipdot-firmware/ff6ca67b2d3a7f4b2edad4ee015f693024e9a245/demos/lib/__init__.py -------------------------------------------------------------------------------- /demos/lib/imageToBinary.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import cv2 4 | import numpy as np 5 | 6 | # Convert image to monochrome bytes 7 | 8 | def imageToBinary(cv2_im, width: int, height: int, adaptive: bool = False, debug_imshow: bool = False): 9 | 10 | if debug_imshow: cv2.imshow('Input', cv2_im) 11 | cv2.waitKey(1) 12 | 13 | cv2_im = cv2.resize(cv2_im, (width, height), cv2.INTER_NEAREST) 14 | cv2_im = cv2.cvtColor(cv2_im, cv2.COLOR_BGR2GRAY) 15 | if adaptive: 16 | cv2_im = cv2.adaptiveThreshold(cv2_im,255,cv2.ADAPTIVE_THRESH_GAUSSIAN_C,\ 17 | cv2.THRESH_BINARY,11,2) 18 | else: 19 | (ret, cv2_im) = cv2.threshold(cv2_im,127,255,cv2.THRESH_BINARY) 20 | 21 | cv2_im = cv2.bitwise_not(cv2_im) 22 | 23 | if debug_imshow: cv2.imshow('Output (8x scale)', cv2.resize(cv2_im, (width*8, height*8), cv2.INTER_NEAREST)) 24 | cv2.waitKey(1) 25 | 26 | cv2_im[cv2_im >= 1] = 1 27 | 28 | cv2_im = np.rot90(cv2_im, 3); 29 | cv2_im = np.fliplr(cv2_im); 30 | cv2_im = np.packbits(cv2_im, bitorder='little') 31 | 32 | return cv2_im.tobytes() 33 | -------------------------------------------------------------------------------- /demos/video.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from os import path 4 | 5 | import socket 6 | import struct 7 | import sys 8 | import time 9 | import cv2 10 | import numpy as np 11 | 12 | from lib import imageToBinary as i2b 13 | 14 | MULTICAST_GROUP = ('239.1.2.3', 8080) 15 | 16 | WIDTH = 64 17 | HEIGHT = 32 18 | 19 | if len(sys.argv) != 2: 20 | print("Usage: " + sys.argv[0] + " [video file]") 21 | exit(1) 22 | 23 | vidcap = cv2.VideoCapture(sys.argv[-1]) 24 | success,cv2_im = vidcap.read() 25 | 26 | sock = socket.socket(socket.AF_INET, # Internet 27 | socket.SOCK_DGRAM) # UDP 28 | ttl = struct.pack('b', 1) 29 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) 30 | 31 | count = 0 32 | start = time.time() 33 | 34 | while success: 35 | 36 | sock.sendto(i2b.imageToBinary(cv2_im, WIDTH, HEIGHT, True, False), MULTICAST_GROUP) 37 | vidcap.set(cv2.CAP_PROP_POS_MSEC,(time.time()-start)*1000) 38 | success,cv2_im = vidcap.read() 39 | 40 | if not success and time.time()-start > 0: 41 | success = True 42 | start = time.time() 43 | vidcap.set(cv2.CAP_PROP_POS_MSEC,0) 44 | success,cv2_im = vidcap.read() 45 | 46 | time.sleep(0.05) 47 | -------------------------------------------------------------------------------- /demos/webcam.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | from os import path 4 | 5 | import socket 6 | import struct 7 | import sys 8 | import time 9 | import cv2 10 | 11 | from lib import imageToBinary as i2b 12 | 13 | MULTICAST_GROUP = ('239.1.2.3', 8080) 14 | 15 | WIDTH = 96 16 | HEIGHT = 32 17 | 18 | vidcap = cv2.VideoCapture(cv2.CAP_DSHOW) 19 | 20 | sock = socket.socket(socket.AF_INET, # Internet 21 | socket.SOCK_DGRAM) # UDP 22 | ttl = struct.pack('b', 1) 23 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl) 24 | 25 | 26 | while True: 27 | success,cv2_im = vidcap.read() 28 | if success: sock.sendto(i2b.imageToBinary(cv2_im, WIDTH, HEIGHT, True, True), MULTICAST_GROUP) 29 | time.sleep(0.1) -------------------------------------------------------------------------------- /include/Hanover_Flipdot.h: -------------------------------------------------------------------------------- 1 | #ifndef Hanover_Flipdot_h 2 | #define Hanover_Flipdot_h 3 | 4 | #include 5 | #include 6 | #include "SPI.h" 7 | #include "Wire.h" 8 | 9 | /* 10 | PCB Revisions 11 | * Issue A - PCB color is green 12 | * Issue B - PCB color is black 13 | * Issue C - PCB color is blue 14 | */ 15 | #define PCB_ISSUE_C 16 | 17 | // Dimensions 18 | 19 | // Dot-board dimensions 20 | #define DB_ROWS 32 21 | #define DB_COLS 32 22 | 23 | // Number of dot-boards 24 | #define DB_BOARDS 4 25 | 26 | // Other dot board settings 27 | #define DB_INVERT 1 28 | #define DB_THROTTLE 0 // For aesthetics, to save power, or to give time for core magnetic saturation 29 | 30 | // Delay to ensure stability for >1 dot board (exponential for every additional board) 31 | #define DB_SEQ_THROTTLE_COIL 15 // Extra pulse time for each dot board coil pulse 32 | 33 | // Timings (µS) - you can decrease coil pulse timings for an increase in speed, but dots may not reliably flip 34 | #define PULSE_COIL_ON 230 // Note that these are after inversion 35 | #define PULSE_COIL_OFF 220 36 | 37 | #if defined(PCB_ISSUE_B) || defined(PCB_ISSUE_C) 38 | 39 | // GPIO pin definitions - dot board 40 | #define PIN_ENABLE1 15 41 | #define PIN_ENABLE2 13 42 | #define PIN_ENABLE3 12 43 | #define PIN_ENABLE4 14 44 | #define PIN_ROW_ADVANCE 33 45 | #define PIN_COL_ADVANCE_ROW_RESET 27 46 | #define PIN_COL_RESET 26 47 | #define PIN_SET_UNSET 25 48 | #define PIN_COIL_DRIVE 2 49 | 50 | // GPIO pin definitions - status LEDs 51 | #define LED_DEBUG true 52 | #define PIN_LED_COIL 23 53 | #define PIN_LED_ROW 19 54 | #define PIN_LED_COL 22 55 | 56 | #endif 57 | 58 | #ifdef PCB_ISSUE_A // Green PCB 59 | 60 | // Other dot board settings 61 | #define DB_INVERT 1 62 | #define DB_THROTTLE 0 // For aesthetics, to save power, or to give time for core magnetic saturation 63 | 64 | // GPIO pin definitions - dot board 65 | #define PIN_ENABLE1 2 66 | #define PIN_ENABLE2 15 67 | #define PIN_ENABLE3 13 68 | #define PIN_ENABLE4 12 69 | #define PIN_ROW_ADVANCE 25 70 | #define PIN_COL_ADVANCE_ROW_RESET 14 71 | #define PIN_COL_RESET 27 72 | #define PIN_SET_UNSET 26 73 | #define PIN_COIL_DRIVE 33 74 | 75 | // GPIO pin definitions - status LEDs 76 | #define LED_DEBUG true 77 | #define PIN_LED_COIL 32 // Actually wired to IO34 but it is input-only 78 | #define PIN_LED_ROW 32 // Actually wired to IO35 but it is input-only 79 | #define PIN_LED_COL 32 80 | 81 | #endif 82 | 83 | typedef uint32_t db_t[DB_COLS * DB_BOARDS]; 84 | 85 | // Public subs 86 | void flipdot_init(); 87 | 88 | class Hanover_Flipdot : public Adafruit_GFX 89 | { 90 | public: 91 | Hanover_Flipdot(void); 92 | void begin(); 93 | void drawPixel(int16_t x, int16_t y, uint16_t color); 94 | void writeDisplay(void); 95 | void clear(void); 96 | void backup(void); 97 | void restore(void); 98 | void fillScreen(uint16_t color); 99 | uint8_t getWidth(void); 100 | uint8_t getHeight(void); 101 | db_t db_buffer; 102 | 103 | private: 104 | void enable(uint8_t db); 105 | void disable(uint8_t db); 106 | void flipDot(bool state, uint16_t pulse_time); 107 | void advanceRow(void); 108 | void advanceCol(void); 109 | db_t db_backup; 110 | db_t db_displayed; 111 | }; 112 | 113 | #endif // Hanover_Flipdot_h -------------------------------------------------------------------------------- /include/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project header files. 3 | 4 | A header file is a file containing C declarations and macro definitions 5 | to be shared between several project source files. You request the use of a 6 | header file in your project source file (C, C++, etc) located in `src` folder 7 | by including it, with the C preprocessing directive `#include'. 8 | 9 | ```src/main.c 10 | 11 | #include "header.h" 12 | 13 | int main (void) 14 | { 15 | ... 16 | } 17 | ``` 18 | 19 | Including a header file produces the same results as copying the header file 20 | into each source file that needs it. Such copying would be time-consuming 21 | and error-prone. With a header file, the related declarations appear 22 | in only one place. If they need to be changed, they can be changed in one 23 | place, and programs that include the header file will automatically use the 24 | new version when next recompiled. The header file eliminates the labor of 25 | finding and changing all the copies as well as the risk that a failure to 26 | find one copy will result in inconsistencies within a program. 27 | 28 | In C, the usual convention is to give header files names that end with `.h'. 29 | It is most portable to use only letters, digits, dashes, and underscores in 30 | header file names, and at most one dot. 31 | 32 | Read more about using header files in official GCC documentation: 33 | 34 | * Include Syntax 35 | * Include Operation 36 | * Once-Only Headers 37 | * Computed Includes 38 | 39 | https://gcc.gnu.org/onlinedocs/cpp/Header-Files.html 40 | -------------------------------------------------------------------------------- /include/main.h: -------------------------------------------------------------------------------- 1 | #define UDP_PORT 8080 -------------------------------------------------------------------------------- /lib/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for project specific (private) libraries. 3 | PlatformIO will compile them to static libraries and link into executable file. 4 | 5 | The source code of each library should be placed in a an own separate directory 6 | ("lib/your_library_name/[here are source files]"). 7 | 8 | For example, see a structure of the following two libraries `Foo` and `Bar`: 9 | 10 | |--lib 11 | | | 12 | | |--Bar 13 | | | |--docs 14 | | | |--examples 15 | | | |--src 16 | | | |- Bar.c 17 | | | |- Bar.h 18 | | | |- library.json (optional, custom build options, etc) https://docs.platformio.org/page/librarymanager/config.html 19 | | | 20 | | |--Foo 21 | | | |- Foo.c 22 | | | |- Foo.h 23 | | | 24 | | |- README --> THIS FILE 25 | | 26 | |- platformio.ini 27 | |--src 28 | |- main.c 29 | 30 | and a contents of `src/main.c`: 31 | ``` 32 | #include 33 | #include 34 | 35 | int main (void) 36 | { 37 | ... 38 | } 39 | 40 | ``` 41 | 42 | PlatformIO Library Dependency Finder will find automatically dependent 43 | libraries scanning project source files. 44 | 45 | More information about PlatformIO Library Dependency Finder 46 | - https://docs.platformio.org/page/librarymanager/ldf.html 47 | -------------------------------------------------------------------------------- /platformio.ini: -------------------------------------------------------------------------------- 1 | ; PlatformIO Project Configuration File 2 | ; 3 | ; Build options: build flags, source filter 4 | ; Upload options: custom upload port, speed and extra flags 5 | ; Library options: dependencies, extra library storages 6 | ; Advanced options: extra scripting 7 | ; 8 | ; Please visit documentation for the other options and examples 9 | ; https://docs.platformio.org/page/projectconf.html 10 | 11 | [env:pico32] 12 | platform = espressif32 13 | board = pico32 14 | framework = arduino 15 | upload_protocol = espota 16 | upload_port = Hanover-Flipdot.local 17 | lib_deps = 18 | https://github.com/tzapu/WiFiManager#development 19 | Adafruit GFX Library 20 | -------------------------------------------------------------------------------- /src/Hanover_FlipDot.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | static const uint8_t pin_enable[] = {PIN_ENABLE1, PIN_ENABLE2, PIN_ENABLE3, PIN_ENABLE4}; 9 | 10 | Hanover_Flipdot::Hanover_Flipdot(void) : Adafruit_GFX(DB_COLS * DB_BOARDS, DB_ROWS) 11 | { 12 | } 13 | 14 | void Hanover_Flipdot::begin(void) 15 | { 16 | 17 | // Set GPIO pins of dot board to output 18 | pinMode(PIN_ENABLE1, OUTPUT); 19 | pinMode(PIN_ENABLE2, OUTPUT); 20 | pinMode(PIN_ENABLE3, OUTPUT); 21 | pinMode(PIN_ENABLE4, OUTPUT); 22 | pinMode(PIN_ROW_ADVANCE, OUTPUT); 23 | pinMode(PIN_COL_ADVANCE_ROW_RESET, OUTPUT); 24 | pinMode(PIN_COL_RESET, OUTPUT); 25 | pinMode(PIN_SET_UNSET, OUTPUT); 26 | pinMode(PIN_COIL_DRIVE, OUTPUT); 27 | 28 | // Set GPIO pins of status LEDs to output 29 | pinMode(PIN_LED_COIL, OUTPUT); 30 | pinMode(PIN_LED_ROW, OUTPUT); 31 | pinMode(PIN_LED_COL, OUTPUT); 32 | 33 | // Set GPIO pins of dot board to low (to avoid potentially overheating coils) 34 | digitalWrite(PIN_ENABLE1, LOW); 35 | digitalWrite(PIN_ENABLE2, LOW); 36 | digitalWrite(PIN_ENABLE3, LOW); 37 | digitalWrite(PIN_ENABLE4, LOW); 38 | digitalWrite(PIN_ROW_ADVANCE, LOW); 39 | digitalWrite(PIN_COL_ADVANCE_ROW_RESET, LOW); 40 | digitalWrite(PIN_COL_RESET, LOW); 41 | digitalWrite(PIN_SET_UNSET, LOW); 42 | digitalWrite(PIN_COIL_DRIVE, LOW); 43 | 44 | if (rtc_get_reset_reason(0) == 1 || rtc_get_reset_reason(0) == 15) 45 | { // POWERON_RESET || RTCWDT_BROWN_OUT_RESET 46 | // Lamp test while waiting for power to stabilize (if cold boot) 47 | digitalWrite(PIN_LED_COIL, HIGH); 48 | digitalWrite(PIN_LED_ROW, HIGH); 49 | digitalWrite(PIN_LED_COL, HIGH); 50 | sleep(1); 51 | } 52 | 53 | // Set GPIO pins of status LEDs 54 | digitalWrite(PIN_LED_COIL, LOW); 55 | digitalWrite(PIN_LED_ROW, LOW); 56 | digitalWrite(PIN_LED_COL, LOW); 57 | 58 | // Lamp test / force clear 59 | fillScreen(1); 60 | writeDisplay(); 61 | clear(); 62 | writeDisplay(); 63 | } 64 | 65 | uint8_t Hanover_Flipdot::getWidth(void) 66 | { 67 | return DB_COLS * DB_BOARDS; 68 | } 69 | uint8_t Hanover_Flipdot::getHeight(void) 70 | { 71 | return DB_ROWS; 72 | } 73 | 74 | void Hanover_Flipdot::enable(uint8_t db) 75 | { 76 | // Unlatch dot board 77 | digitalWrite(pin_enable[db], HIGH); 78 | 79 | // Clear row and column resets 80 | digitalWrite(PIN_COL_ADVANCE_ROW_RESET, LOW); 81 | digitalWrite(PIN_COL_RESET, LOW); // Clear column reset after row, as row reset advances column 82 | } 83 | 84 | void Hanover_Flipdot::disable(uint8_t db) 85 | { 86 | // Hold row and column counters in reset 87 | digitalWrite(PIN_COL_ADVANCE_ROW_RESET, HIGH); 88 | digitalWrite(PIN_COL_RESET, HIGH); // Set column reset after row, as row reset advances column 89 | 90 | // Latch dot board 91 | digitalWrite(pin_enable[db], LOW); 92 | } 93 | 94 | void Hanover_Flipdot::advanceRow(void) 95 | { 96 | digitalWrite(PIN_ROW_ADVANCE, HIGH); 97 | if (LED_DEBUG) 98 | digitalWrite(PIN_LED_ROW, HIGH); 99 | delayMicroseconds(1); 100 | digitalWrite(PIN_ROW_ADVANCE, LOW); 101 | if (LED_DEBUG) 102 | digitalWrite(PIN_LED_ROW, LOW); 103 | delayMicroseconds(1); 104 | } 105 | 106 | void Hanover_Flipdot::advanceCol(void) 107 | { 108 | digitalWrite(PIN_COL_ADVANCE_ROW_RESET, HIGH); 109 | if (LED_DEBUG) 110 | digitalWrite(PIN_LED_COL, HIGH); 111 | delayMicroseconds(1); 112 | digitalWrite(PIN_COL_ADVANCE_ROW_RESET, LOW); 113 | if (LED_DEBUG) 114 | digitalWrite(PIN_LED_COL, LOW); 115 | delayMicroseconds(1); 116 | } 117 | 118 | void Hanover_Flipdot::flipDot(bool state, uint16_t pulse_time) 119 | { 120 | digitalWrite(PIN_SET_UNSET, state ^ DB_INVERT ? HIGH : LOW); 121 | digitalWrite(PIN_COIL_DRIVE, HIGH); 122 | if (LED_DEBUG) 123 | digitalWrite(PIN_LED_COIL, HIGH); 124 | 125 | delayMicroseconds(pulse_time); 126 | 127 | digitalWrite(PIN_COIL_DRIVE, LOW); 128 | if (LED_DEBUG) 129 | digitalWrite(PIN_LED_COIL, LOW); 130 | 131 | delayMicroseconds(DB_THROTTLE); 132 | } 133 | void Hanover_Flipdot::writeDisplay(void) 134 | { 135 | // Iterate dot boards 136 | for (uint8_t db = 0; db < DB_BOARDS; db++) 137 | { 138 | bool db_enabled = false; 139 | 140 | uint8_t col_start = db * DB_COLS; 141 | uint8_t col_end = col_start + DB_COLS; 142 | 143 | // Iterate columns 144 | uint8_t col_backlog = 0; 145 | for (uint8_t col = col_start; col < col_end; col++) 146 | { 147 | // Iterate rows 148 | uint8_t row_backlog = 0; 149 | for (uint8_t row = 0; row < DB_ROWS; row++) 150 | { 151 | bool db_buffer_bit = db_buffer[col] & (1 << row); 152 | bool db_displayed_bit = db_displayed[col] & (1 << row); 153 | if (db_displayed_bit != db_buffer_bit) 154 | { 155 | // Enable dot-board if first time writing 156 | if (!db_enabled) 157 | { 158 | enable(db); 159 | db_enabled = true; 160 | } 161 | 162 | // Increment to match 163 | for (uint8_t cb = 0; cb < col_backlog; cb++) 164 | advanceCol(); 165 | for (uint8_t rb = 0; rb < row_backlog; rb++) 166 | advanceRow(); 167 | row_backlog = 0; 168 | col_backlog = 0; 169 | 170 | uint16_t pulse_time = pow(db, 2) * DB_SEQ_THROTTLE_COIL + (db_buffer_bit ? PULSE_COIL_ON : PULSE_COIL_OFF); 171 | flipDot(db_buffer_bit, pulse_time); 172 | } 173 | row_backlog++; 174 | } // Iterate rows 175 | db_displayed[col] = db_buffer[col]; 176 | Serial.println(); 177 | col_backlog++; 178 | } // Iterate columns 179 | if (db_enabled) 180 | disable(db); 181 | } // Iterate dot boards 182 | } 183 | 184 | // Drawing helper functions 185 | 186 | void Hanover_Flipdot::clear(void) 187 | { 188 | for (uint8_t i = 0; i < DB_COLS * DB_BOARDS; i++) 189 | db_buffer[i] = 0; 190 | } 191 | 192 | void Hanover_Flipdot::backup(void) 193 | { 194 | memcpy(db_backup, db_buffer, sizeof(db_buffer)); 195 | } 196 | 197 | void Hanover_Flipdot::restore(void) 198 | { 199 | memcpy(db_buffer, db_backup, sizeof(db_backup)); 200 | } 201 | 202 | // Drawing functions (Adafruit GFX Primitives) 203 | 204 | void Hanover_Flipdot::drawPixel(int16_t x, int16_t y, uint16_t color) 205 | { 206 | if ((y < 0) || (y >= DB_ROWS)) 207 | return; 208 | if ((x < 0) || (x >= DB_COLS * DB_BOARDS)) 209 | return; 210 | 211 | if (color) 212 | { 213 | db_buffer[x] |= 1 << y; 214 | } 215 | else 216 | { 217 | db_buffer[x] &= ~(1 << y); 218 | } 219 | } 220 | 221 | void Hanover_Flipdot::fillScreen(uint16_t color) 222 | { 223 | for (uint8_t i = 0; i < DB_COLS * DB_BOARDS; i++) 224 | db_buffer[i] = pow(2, DB_ROWS) - 1; 225 | } -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | extern "C" 11 | { 12 | #include "freertos/FreeRTOS.h" 13 | } 14 | 15 | #include 16 | #include 17 | #include 18 | 19 | #include "AsyncUDP.h" 20 | 21 | Hanover_Flipdot flipdot = Hanover_Flipdot(); 22 | AsyncUDP udp; 23 | IPAddress multicastGroup = IPAddress(239, 1, 2, 3); 24 | 25 | bool panelOverride = false; 26 | 27 | void debugText(String text, uint8_t seconds = 0) 28 | { 29 | if (seconds) 30 | flipdot.backup(); 31 | flipdot.clear(); 32 | flipdot.setCursor(0, 5); 33 | flipdot.print(text); 34 | flipdot.writeDisplay(); 35 | if (seconds) 36 | { 37 | sleep(seconds); 38 | flipdot.restore(); 39 | flipdot.writeDisplay(); 40 | } 41 | } 42 | 43 | void configModeCallback(WiFiManager *myWiFiManager) 44 | { 45 | debugText("Setup: " + myWiFiManager->getConfigPortalSSID()); 46 | } 47 | 48 | void onUdpPacket(AsyncUDPPacket packet) 49 | { 50 | if (panelOverride) 51 | return; 52 | 53 | uint16_t len = (uint16_t)flipdot.getWidth() * flipdot.getHeight() / 8; 54 | if (packet.length() != len) 55 | return; 56 | 57 | memcpy(flipdot.db_buffer, packet.data(), len); 58 | flipdot.writeDisplay(); 59 | } 60 | 61 | void connectToUdp() 62 | { 63 | if (udp.listenMulticast(multicastGroup, UDP_PORT)) 64 | { 65 | // Power optimizations 66 | setCpuFrequencyMhz(80); 67 | 68 | // Uncomment for better receive performance, e.g. for videos 69 | // ESP32 may get hot! 70 | WiFi.setSleep(false); 71 | 72 | // UDP handler function 73 | udp.onPacket(onUdpPacket); 74 | } 75 | } 76 | 77 | void WiFiEvent(WiFiEvent_t event) 78 | { 79 | Serial.printf("[WiFi-event] event: %d\n", event); 80 | String ip_msg = "IP: "; 81 | ip_msg += multicastGroup.toString() + "\nPort: " + UDP_PORT; 82 | switch (event) 83 | { 84 | case SYSTEM_EVENT_STA_START: 85 | debugText("Wi-Fi connecting", 3); 86 | break; 87 | case SYSTEM_EVENT_STA_GOT_IP: 88 | connectToUdp(); 89 | debugText(ip_msg); 90 | break; 91 | case SYSTEM_EVENT_STA_DISCONNECTED: 92 | debugText("Wi-Fi lost", 3); 93 | break; 94 | case SYSTEM_EVENT_STA_LOST_IP: 95 | debugText("Wi-Fi lost IP", 3); 96 | break; 97 | } 98 | } 99 | void setup() 100 | { 101 | 102 | Serial.begin(115200); 103 | 104 | flipdot.begin(); 105 | flipdot.setFont(&TomThumb); 106 | flipdot.setTextSize(1); 107 | 108 | WiFi.onEvent(WiFiEvent); 109 | 110 | WiFiManager wifiManager; 111 | wifiManager.setAPCallback(configModeCallback); 112 | wifiManager.setHostname("Hanover-Flipdot"); 113 | wifiManager.autoConnect(); 114 | 115 | ArduinoOTA.onStart([]() { 116 | // Stash flipdot contents and lock display to prevent slowdown of OTA by synchronous display updates from UDP 117 | flipdot.backup(); 118 | panelOverride = true; 119 | String type; 120 | if (ArduinoOTA.getCommand() == U_FLASH) 121 | { 122 | type = "FW"; 123 | } 124 | else 125 | { // U_FS 126 | type = "FS"; 127 | } 128 | debugText("OTA " + type); 129 | }) 130 | .onProgress([](unsigned int progress, unsigned int total) { 131 | String ota_msg = "OTA "; 132 | ota_msg += String(progress / (total / 100)) + "%"; 133 | debugText(ota_msg); 134 | }) 135 | .onEnd([]() { 136 | debugText("OTA success!"); 137 | }) 138 | .onError([](ota_error_t error) { 139 | String ota_msg = "OTA error " + String(error) + ":\n"; 140 | switch (error) 141 | { 142 | case OTA_AUTH_ERROR: 143 | ota_msg += "Auth Failed"; 144 | break; 145 | case OTA_BEGIN_ERROR: 146 | ota_msg += "Begin Failed"; 147 | break; 148 | case OTA_CONNECT_ERROR: 149 | ota_msg += "Connect Failed"; 150 | break; 151 | case OTA_RECEIVE_ERROR: 152 | ota_msg += "Receive Failed"; 153 | break; 154 | case OTA_END_ERROR: 155 | ota_msg += "End Failed"; 156 | break; 157 | } 158 | debugText(ota_msg); 159 | sleep(3); 160 | panelOverride = false; 161 | flipdot.restore(); 162 | flipdot.writeDisplay(); 163 | }); 164 | 165 | ArduinoOTA.begin(); 166 | } 167 | 168 | void loop() 169 | { 170 | ArduinoOTA.handle(); 171 | } 172 | -------------------------------------------------------------------------------- /test/README: -------------------------------------------------------------------------------- 1 | 2 | This directory is intended for PIO Unit Testing and project tests. 3 | 4 | Unit Testing is a software testing method by which individual units of 5 | source code, sets of one or more MCU program modules together with associated 6 | control data, usage procedures, and operating procedures, are tested to 7 | determine whether they are fit for use. Unit testing finds problems early 8 | in the development cycle. 9 | 10 | More information about PIO Unit Testing: 11 | - https://docs.platformio.org/page/plus/unit-testing.html 12 | --------------------------------------------------------------------------------