In this project I build several OLED displays that can be controlled by mobile phones. We have a guest Wi-Fi system in our office that handles a large number of daily visitors. I noticed leaflets with the Wi-Fi password scattered throughout the office. This just felt like bad security, so I decided to come up with a better approach. The goal is to have an electronic display in every conference room that can clearly display the Wi-Fi SSID, password and a Wi-Fi QR code. The display will also allow employees to send custom notifications. In previous videos I’ve written phone apps or used existing apps to control my projects, but in this case, we’ll keep it simple by using SMS text messaging for control and feedback.
Table of Contents
- Adafruit IO
- MQTT Manager
- OLED Display
- Disconnection Layer
- Clock Layer
- Message Layer & I2S Amp
- Password Layer
- Main Program
- Environment Variables
- The Build
Communication flows from a mobile phone via SMS text message to a Twilio API function which then fires a webhook on the Adafruit IO cloud service. A CircuitPython program running on an ESP32-S2 Wi-Fi enabled board monitors the Adafruit IO feed using MQTT and then performs commands or displays any published messages on an OLED display.
Adafruit IO is a paid service, but they do offer a free plan which I’m currently using. Leveraging Adafruit IO saves me the trouble of spinning up and maintaining a web server and an MQTT broker. The setup is unbelievably easy. You just navigate to the Feeds tab and then click New Feed. Then give it a name and click create.
Next click the gear icon next to Webhooks and click Create. This generates a webhook URL which you’ll need for Twilio. Copy the URL to the clipboard.
Twilio is a paid service, but they offer a free trial that gives you a small credit. We are currently paying $1.15 US per month for a local phone number and around a penny per text (send or receive) which includes a carrier fee and taxes. Still YMMV depending on your country and carrier. The trial account does not require a credit card. Twilio is a very robust system, but configuration was relatively easy because I’m only using a tiny subset of its functionality. After creating an account, you will need to click Get a Twilio phone number.
Next you need to verify all phone numbers that will be allowed to receive texts from your new number. It’s a constraint set by the low-cost plan but it’s alright because we only have a few users.
In theory, you could just add the Adafruit IO webhook address to your new Twilio phone number and the Adafruit IO feed would start receiving text messages. Unfortunately, this currently doesn’t work properly because Adafruit IO only supports JSON and Twilio expects XML. Hopefully, this will change in the future. However, it’s not too difficult to remedy by implementing a Twilio services with a single Node.js function to process the webhook. Locate the Functions and Assets tab. Please note you may need to click Explore Products and search under Developer tools. You can always click the 3 dots next to a tab to pin it to the side bar. Click Create Service and give it a name.
Click Add to create a new function and call it process_sms. Also please make a note of the address just above the Deploy All button. This is the function address which is combined with the function name. For example, the address below would be http://smsdisplay-2673.twil.io/process_sms.
Save the code above and then click Deploy All. Navigate to Phone Numbers – Active numbers and click your Twilio phone number. Scroll down to the section titled “A MESSAGE COMES IN”. Type in the address to the process_sms function above and click Save
Now test the function by sending a text to the Twilio phone number. You should get a “Message received” reply. If you don’t make sure you verified the caller ID of the mobile phone you’re using. You can also check the Twilio logs for errors. The prepended “Sent from your Twilio trial account” will be removed after your first payment.
If everything worked, then your message should show up on the Adafruit IO feed.
An Adafruit QT PY ESP32-S2 will be used to monitor the Adafruit feed over the web using MQTT. The QT PY is a low-cost, Wi-Fi enabled board with a single-core 240 MHz Tensilica chip. It has a real time clock and supports I2S audio, ADC, PWM, SPI, I2C and much more all in an extremely compact form factor.
The lower level MQTT is handled by the adafruit_minimqtt library, but I wrote a class called mqtt_manager to manage all the MQTT communication between the QT PY and Adafruit IO. The constructor for the MQTTManager class takes an mqtt_client and a callback function onmessage that will be triggered when an MQTT message is received. The Adafruit IO feed address is retrieved from the environment variable AIO_FEED. An instance variable status tracks the connection status and will initially be set to disconnected. Next 4 callback methods are wired to the MQTT client: on_connect, on_disconnect, on_message and on_subscribe.
The connected callback is fired when the MQTT client makes a successful connection to Adafruit IO. The status variable is changed to connected and then the subscribe method is called to subscribe to the specified Adafruit IO feed with QOS=1. MQTT provides 3 QOS (quality of service) levels. Level zero is basically no guarantee of delivery (sort of like the post office). Level one ensures that your message will be delivered at least once. Level two ensure that your message is delivered exactly once. Level two is currently not supported by Adafruit IO. The disconnected callback fires when the MQTT client is disconnected. The status is set to disconnected. The message callback is fired when a message is received. It relays the message to the onmessage callback that was initially passed into the class. The subscribed callback is fired when the MQTT client successfully subscribes to a feed. Please note that there is currently a bug in the granted_qos level. If you set the MQTT client to QOS=2 then the granted_qos will also show 2 even though it’s not supported and not actually running.
A method called connect is used to connect the MQTT client to Adafruit IO. It will only run if the status is disconnected to ensure multiple connections are not attempted. The status is changed to connecting. The actual connect command is wrapped in a try statement because Wi-Fi and Internet connections are not always reliable. If the command is successful, then the connected callback above should fire. Otherwise, if an exception occurs then the status is set back to disconnected.
The loop method checks the MQTT broker for messages. If the status is still connecting, then the method exits and returns false. If the status is disconnected, then the status is set to connecting and a reconnect will be initiated. If an error occurs, then the status is set back to disconnected. Either way the method exits and returns false. Finally, mqtt_client.is_connected is called to double check the connection, and if true then the loop method is called which checks for messages. Please note that the method is_connected does not return false if you’re not connected. Instead, it throws an error. I ran into a lot MQTT and Adafruit IO reliability issues and that is the main reason I wrote this class in an attempt to make it more bulletproof. There are several possible errors that can manifest as either an MMQTTException or an OSError. For the MMQTTException the method just returns false because it is probably just a networking issue that will fix itself. However, the OSError usually indicates a more serious condition that requires a disconnection and reconnection to fix.
A large 5.5 inch (14 cm) green 256×64 OLED module will be used for the display. I bought it from a company called BuyDisplay and it is a model ER-OLEDM055. It’s an expensive display at $70 US, but you can get smaller 3.2 inch (8.1 cm) versions for a fraction of the price. The OLED display is very bright even in sunlit rooms and has a wide viewing angle. It can display 16 shades of green and is controlled by an SSD1322 IC. It supports 8-bit parallel and 3 or 4 wire SPI communication. It’s 3.3 V so it easy to hook up to the QT PY.
The OLED CS (chip select) line is connected to the QT PY’s A0. RST (reset) is connected to A1. DC (data/command) is connected to A2. It doesn’t really matter what pins are specified for CS, RST and DC because they will be specified in code. SCLK (serial clock) is connected the SCK. SDIN (serial data in) is connected to MOSI (master out slave in). Vin is connected to the QT PY’s 3.3 V line and the grounds are connected. The OLED can draw a lot of current which could brown out the QT PY. Therefore, a 100 μF capacitor is placed across the 3.3 V rail and ground.
In order to simplify the CircuitPython coding, I have broken the OLED display code into 4 separate classes:
- Clock Layer
- Disconnection Layer
- Message Layer
- Password Layer
The clock layer displays the time and date on the screen and also handle NTP synchronization with the ESP32’s real-time clock. The disconnection layer displays a warning icon if communication is lost. The message layer displays custom text messages. It also handles the I2S audio to play a notification chime. The password layer displays the Wi-Fi SSID and password. It also generates and displays a Wi-Fi QR code to facilitate guest access.
The simplest code is the Disconnection Layer. The class takes a single parameter which is the primary Display IO group. The group can hold TileGrids which are used to display graphics, text and images. Adding or removing items from the group either displays them or hides them. A small 24×18 bitmap formatted to 4 bits-per-pixel is loaded from the flash storage using the OnDiskBitmap method. A new TileGrid is instantiated to hold the bitmap and is positioned in the lower right corner of the display. There is a single property called visible which defaults to false. The getter returns if the disconnected bitmap is currently visible, and the setter toggles the visibility by appending or removing the TileGrid from the Display IO group.
The clock layer is more complicated. There are several imports:
- adafruit_datetime handles the manipulation of dates and times
- adafruit_display_text.label is used to draw text
- adafruit_ntp is used to synchronize the real-time clock over the Internet with an NTP server
- display IO groups are used to hold the text labels
- getenv to retrieve TimeZone environment variable
- socketpool is used by the NTP library for networking
- rtc controls the ESP32’s built-in real-time clock
- terminalio font is used for the clock text
Two constants are declared. The color white will be used for text although on a green display that is just the brightest green. A tuple of months holds the abbreviated names for the months.
The clock class constructor takes the font, group, socket pool and the display width & height and stores them in instance variables. The visibility is set to false. An instance variable offset will slightly shift the clock position to avoid OLED burn-in. Scale will control the size of the clock. The real-time clock is instantiated. An instance variable last time will track the last displayed time to prevent unnecessary updates. A boolean _ntp_up_to_date will track if the NTP synchronization is current. A method called sync_time is called to perform the initial NTP synchronization. A display IO label is created to hold the time and date text. I’m using a label class even though it uses more memory than the bitmap_label class because I found the bitmap_label to have a very noticeable flicker which won’t work for a clock that updates every minute. A method called parse_time is called to retrieve, format and display the clock. A method called _place_clock is used to set the position of the clock.
The clock layer has 3 properties. Visible returns and sets the visibility of the clock. Offset returns and sets the horizontal offset of the clock (used to reduce OLED burn-in). Scale returns and sets the size of the clock. Currently only scale 1 and 2 are supported.
A method called _place_clock sets the size and position of the clock. The X, Y label position is calculated based on the scale and offset. The smaller scale 1 clock is positioned in the top right corner when the Wi-Fi information is taking up most of the screen. The larger scale 2 clock is displayed in the center by itself.
The is_dst method checks if it is daylight savings which varies by where you live. In most of North America it starts on the 2nd Sunday of March and ends on the first Sunday of November which is relatively easy to calculate using the formulas below. You would need to modify this code if your city is different. Ideally the Adafruit library will be upgraded to automatically handle DST or even better all of the world will stop using DST.
The sync_time method ensures the real-time clock is accurate using NTP (network time protocol). An NTP server is instantiated using the passed socket pool and the time zone offset. The Adafruit NTP library currently does not automatically handle time zones, so you have to specify your offset in hours from Greenwich Mean Time in the environment variables. Unfortunately, the library doesn’t account for daylight saving time yet either, so the current year, month and day are passed to the is_dst method. If it is daylight saving then the NTP server is reinstantiated but the time zone offset is modified by 1 hour to allow for DST. It has to be instantiated again because currently the tz_offset property is not exposed by the Adafruit NTP library. Next the real-time clock is set to the NTP date & time and the _ntp_up_to_date instance variable is set true. Errors can always occur over a network, so any exception is printed and _ntp_up_to_date is set false.
The last method in the clock layer is called parse_time. First it checks that NTP is up to date. Otherwise, it calls sync_time. The datetime is retrieved from the real-time clock and stored in a variable t. A variable h stores the hour. I’ll be using standard time as opposed to the returned military time so if the hour >= 12, PM is selected otherwise AM. Then 12 is subtracted from the hour if it is greater than 12. If the NTP server is not up to date, then the am/pm is displayed in lowercase as a subtle cue that something may be wrong. A variable new_time holds the formatted date and time. The OLED display is only updated if the new time is different from the last time. The hour (in military) is returned which can be used by the main program to handle scheduled events.
In addition to displaying messages, the message layer will also play a notification chime which will require audio output. I wanted to use the Adafruit STEMMA speaker with built-in amp because it is inexpensive and only requires a single GPIO but unfortunately the QT PY ESP32-S2 does not currently support the audioio library or the audiopwmio library. Therefore, I decided to use the same Adafruit I2S amp (Max 98357A) that I used in my BLE Restroom Key Tracker project. Wiring the amp requires a lot more connections. LRC (Left/Right Frame Clock) is connected to the QT PY’s RX pin. BCLK (Bit Clock Input) is connected to TX. DIN (Data Input) is connected to SCL. The Gain pin is used to adjust the amp gain from 3 to 15 dB (see Table 8 in the datasheet). A 100k Ω resistor to ground sets the gain at 15 dB. SD (Shut Down) is connected to SDA. By the way, you can use any pins for LRC, BCLK, DIN and SD because they are specified in code. The grounds are connected, and Vin is connected to 5V to maximize the possible output volume. The positive and negative speaker terminals are connected to a 4 Ω 3 watt speaker. I’m using a PUI Audio AS04004PO-2-LW152-WR-R.
The message layer class will use a BDF font which affords lots of possibilities. A bitmap_label is used because the message is only displayed once. Since there is no updating of the label, flicker won’t be an issue and it will use less memory. Although I found both labels to be inefficient. For example, every minute update to the clock uses over 6K of memory. It will eventually be recovered by garbage collection but when you have a big program that uses a lot of memory it’s not ideal. The displayed message can span multiple lines so wrap_text_to_lines is also imported. The audiobusio class is for I2S. The audiocore class is used to play a Wave audio file which stores a pleasant chime sound as opposed to the shrill beeps of a piezo buzzer. DigitalInOut and Direction will be used to control the amp’s shut down pin. A display IO group will hold the message label. Microcontroller Pin is only imported for the typing of the passed parameters. Sleep is used for the message duration.
The message class is passed GPIO pins for the I2S bit_clock (BCLK), word_select (LRC), data (DIN) and amp_enable (SD). A BDF font is passed along with the Display IO group. The audiobus I2SOut is instantiated. Audiocore is used to load a Wave file of the chime. The Shut Down pin is configured to the output amp_enable. This will let the main program power down the amp to save electricity when not in use. The bitmap_label is instantiated with the font, single line spacing and the coordinates are zeroed.
The method show_message will display the passed message on the OLED for the specified delay time. Only one message is allowed at a time, so the group is checked to make sure it doesn’t already contain the message label. The message is formatted. And is changed to & to save space. I might add more abbreviations later. Next wrap_text_to_lines is used to format the text, so it spans multiple lines if necessary. A list called prior_state will hold all existing items from the Display IO group so the display can be returned to its previous state after the message. The group is also cleared while the prior_state is populated. Next the _message_label is appended to the group which displays it on the OLED. The I2S amp is enabled, and the notification chime is played through the speaker. Afterwards the amp is disabled. The sleep method leaves the message on the screen for the specified delay time. Then the message is cleared by removing the message label from the group which is then restored to its previous state.
A deinit method is added so the main program can clean up the I2S audio on exit.
The password layer has several imports. BDF font will be used for the password text displayed on the OLED. Bitmap labels will hold the Wi-Fi password text and the SSID text. L and QRCode are imported from adafruit_miniQR to generate the Wi-Fi QR code. Bitmap, Group, Palette and TileGrid are imported from display IO to handle the above text and graphics. Getenv is imported from os to get access to the Wi-Fi SSID and password. The terminalio font is used for the SSID label. Color constants white and black are defined and the QR code border-pixels is set to 2.
The password constructor is passed the fonts, the display IO group and the width & height of the display. An instance variable _visible will handle the layer’s visibility and like the clock layer an offset will be used to prevent OLED burn-in. A QR code is instantiated. The add_data method is passed the necessary data for the Wi-Fi connection. The make method generates the QR code in memory. Qr_bitmap will be a graphical version of the QR matrix to display on the OLED. The bitmap width is stored. Scale fits the QR bitmap to the display. Pos_y vertically centers the QR bitmap. A 2 color QR palette is defined and passed the constants for black and white. A display IO tile grid is instantiated to hold the QR bitmap. A display IO label _wifi_label holds the SSID in the smaller font. A display IO label _password_label holds the password text in the larger font.
A static method _bitmap_qr generates the QR code bitmap. It is passed the QR matrix. It creates a new bitmap using the matrix dimensions along with the border_pixels. Next the method loops through the complete X, Y range of the matrix to build the bitmap pixel by pixel. The bitmap is returned.
Like the other display layers, there is a visible property. The setter will add the QR code, the SSID label and the password label to the display IO group if visible is true. Otherwise, it will remove all 3 if visible is false.
The offset property allows the program to horizontally shift the QR code, the SSID label and the password text to avoid OLED burn-in.
The main program has a lot of imports. The adafruit_bitmap_font library is imported for BDF fonts. The adafruit_minimqtt library is imported for MQTT. The adafruit_ssd1322 library is imported for the SSD1322 OLED display. The required GPIO pins and SPI bus are imported from board. The displayio library is imported for the display text and graphics. The garbage collection (gc) is imported to address some memory issues (no pun intended). The microcontroller library is imported. From os, getenv is imported to access environment variables. SocketPool is for network sockets. The ssl module with afford secure Internet access. The supervisor module is imported. Terminalio is used for its font. From time, sleep is imported. The wifi module provides Wi-Fi networking. The 4 display layer classes are imported for Clock, Disconnection, Message and Password. Finally, my mqtt_manager is imported to manage MQTT.
A client_id that is unique to each QT PY board is created by taking the least significant 19 bits of the microcontroller.cpu.uid. I ran into a strange bug with respect to running multiple QT PY boards on the same network. By default, all the QT PY ESP32 boards default to the network hostname “espressif”. For an unknown reason this was intermittently causing the boards to disconnect and core crash. I don’t know if the problem is specific to my Asus router. However, after assigning unique hostnames, the crashing hasn’t reoccurred. Also, unique names help to identify stuff when you’re using networking tools.
The display IO release_displays method is called to ensure any existing displays are released. The SPI bus for the display is instantiated and passed the necessary board parameters. The baud rate is set at what I would consider a conservative speed to favor reliability of speed. The display is instantiated and passed the bus, the width, the height and I found it necessary to modify the default colstart value to 112 for my particular OLED display. This is supposed to be the index of the first visible display column. The display IO group is defined. This is the group that all the 4 display layers will use to display text and graphics. A small font is defined using terminalio font and a large font is defined using the bitmap_font load_font method which is passed the path to the font file profont22.bdf.
The function connect_to_wifi establishes a Wi-Fi connection. I’m using a settings.toml file to store my SSID and password so CircuitPython should automatically connect to Wi-Fi. However, despite what the docs say CircuitPython currently doesn’t automatically reconnect if the connection is dropped. Hopefully this will change in the future. Therefore, the ipv4_address is checked and if there isn’t one then that means there is no Wi-Fi connection. The function will loop until an address obtained. The wifi.radio connect method is passed the SSID and password from the environment variables stored in the settings.toml file. As always with networking operations, error catching is used. The function will repeat every 10 seconds until a Wi-Fi connection is established.
At this point the program checks for a Wi-Fi connection and calls the above connect_to_wifi function if not. A SocketPool is defined which will be used by the NTP and MQTT protocols. The clock_layer is instantiated to handle the display of the clock. The disconnection_layer is instantiated to handle the display of the disconnection error icon. The message_layer is instantiated to handle display of SMS text messages. The password_layer is instantiated to handle display of the SSID, Wi-Fi password and QR code.
A callback function called message will fire when an SMS text message is received. The text is cleaned up for more flexibility with the commands. Any non-letters are removed and the case is set to upper. This allows a user to issue the WIFI command by also typing Wifi, Wi-Fi, wifi, etc. If clean_text = “WIFI” then clock scale is set to 1 to make room for the password_layer which is made visible. This shows the SSID, the password and the QR code on the display. If clean_text = “OFF” then the password_layer is turned off and the clock scale is made large to fill the display. The reset command will restart the QT PY board. I did run into some bugs with the display IO library where after a few days, artifacts started appearing on the screen. if it reoccurs then anyone in the office can reset all the displays by texting reset. I may add more commands later on. The final else indicates the text message is not a command so the message_layer is enabled and passed the full text message along with the delay time of 10 seconds.
An mqtt_client is instantiated. If you don’t pass a client_id then one will automatically be generated. However, I found the connection to be more reliable by always specifying the same unique client ID per board. Therefore, I’m using the same one generated for the hostname from the CPU ID. The broker address is retrieved from the settings.toml file along with the port number, the username and the security key. The socket pool is passed and ssl.create_default_context is used to secure the Internet communication. The mqtt_manager is instantiated and passed the mqtt_client and the callback function to be fired when a message is received. The connect method is called to establish the MQTT connection with Adafruit IO. A variable last_hour is set to -1. It is used to keep track of when the hour of day changes. The clock_layer is enabled and display.show is called to actually display the main group.
The main program loop is an infinite while wrapped in a try to catch errors. The clock_layer parse_time method is called to update the time and date if necessary and returns the current hour. If the hour does not equal the last hour, then hourly routines are performed. In order to mitigate bugs and improve reliability, a soft restart is performed using supervisor.reload. It occurs once daily at midnight when the hour equals zero and the last hour was 23. Otherwise, the last_hour is updated to the current hour. The available memory is printed to the REPL to aid in debugging. Every hour the password_layer offset is cycled through 1 of 4 positions to reduce OLED burn-in. The clock_layer offset is also adjusted plus or minus up to 12 pixels from noon. Our business hours are typically 5 AM to 5 PM so the password_layer is automatically hidden outside these times in case someone forgets to turn it off. Then the OLED is given a rest between 9 PM and 5 AM. Next the Wi-Fi connect is checked using the ipv4_address. If the Wi-Fi has dropped, the mqtt_manager status is set to disconnected, the disconnection_layer is made visible to show the error icon and the connect_to_wifi function is called. Afterwards the loop is restarted. Otherwise, the mqtt_manager loop method is called to check for text messages. If true is returned, then everything worked and the disconnection_layer can be hidden if necessary. I did find it necessary to use gc.collect to force the python garbage collection each loop because I ran into memory issues especially with display IO. Hopefully, these will get better as CircuitPython continues to mature. The loop sleeps for 1 second and repeats. An except statement is added to catch Ctrl-C and gracefully exit by putting the display to sleep, releasing display IO and deinitializing the mqtt_client, the message_layer and the SPI bus.
Here are the environment variables used in this tutorial. CircuitPython 8.0.0 introduces support for environment variables. CircuitPython uses a file called settings.toml at the drive root (no folder) as the environment. Environment variables are commonly used to store “secrets” such as Wi-Fi passwords and API keys. This method does not make them secure. It only separates them from the code so be careful not to post them to a public website such as GitHub. Don’t worry, I’m going to change my Wi-Fi password, regenerate my Adafruit IO key and change my Twilio phone number.
The Adafruit IO security key above can be obtained by clicking the key icon on the Adafruit IO website.
The following CircuitPython libraries were used in this project:
I need to build at least 4 displays and I wanted something that would look good in our conference rooms. I found an inexpensive aluminum case at my favorite electronics surplus store All Electronics.
I designed 3D printed covers for the sides of the case in Sketch Up.
I needed 4 new holes on the front of the case to mount the OLED display and a rounded rectangular cut out to pass the wires. The aluminum case has a concave back which is hard to mount so I cut a pocket in a 2×4 with my Romaxx HS1 to hold it securely and then made the required cuts using CNC precision.
I also used the CNC to cut green plexiglass covers for the 4 cases.
In order to keep the wiring tidy, I designed a PCB in Eagle to hold the QT PY, the I2S amp and the necessary connectors.
Since I needed 4 PCB’s, I ordered them online from one of the popular PCB makers.
I used 7 position female header sockets to mount the QT PY and I2S amp because quick removal greatly facilitates testing and debugging. For the same reason, I swapped out the 3.5 mm terminal block on the I2S amp for a pluggable version. Please note the through holes on the I2S amp are small so the pin diameter of your terminal block should be less than 0.8 mm. I didn’t screw down the black standoffs. The two 3D printed standoffs were created using a great Customizable Standoff Generator that I found on Thingiverse.
The PCB is mounted inside the enclosure using 2 of the screws that retain the OLED display. The PCB is isolated from the case by 3 mm PVC spacers. JST-XH 3 & 4 pin connectors are used to connect the OLED. I used two separate plugs because 7 pin connectors are less common. I omitted the gain resistor because the default 9 dB was loud enough. The STEMMA connector on the QT PY is located near the case opening so it can be used as a debugging console output.
I bought a 4-wire panel mount USB Type C connector on AliExpress because it was the only one I could find that would fit in the 1/2 inch (12.7 mm) opening in the back of the enclosure. In retrospect, it was a bad choice because it is not fully compatible. It doesn’t work if I plug it into a Type C jack on a computer. However, it does work if I plug it into a Type A jack using a USB-C to USB-A cable. I did try all the different resistor combinations on the male plug, but I couldn’t find a compatible combination. In future projects I would just buy a very short USB C extension cable and either 3D print or CNC a panel mount for it. Alternatively, I could have just run a USB cable out the back with a strain relief grommet.
The display is mounted to the case with green anodized aluminum M3 socket cap screws. The speaker is mounted to the left cover using the same screws in black. Eight M3 x 3 mm anodized aluminum spacers are used on each side of the OLED display board to separate it from the case and the plexiglass.
When I first tried the OLED display it didn’t work. I tracked the problem down to the configuration resistors labelled BS0 and BS1. I had ordered the 4-wire SPI display but mine was set up for 8080 parallel. I had to move the zero ohm resistor labelled R18 to the R19 position as shown below. R21 was already in the correct position. The configuration options are detailed in the interfacing instructions.
The displays shows a text message sent from a mobile phone and the phone gets a “Message received” confirmation text from Twilio. Of course, the phone doesn’t need to be next to the display. The text message could be sent from anywhere in the world with cell service. Furthermore, I ended up making 4 displays and they could be spread out across the world in different locations and as long as they have Wi-Fi they’ll work.
When the message “Wifi” is texted, the display shows the Wi-Fi information. Notice the clock is reduced in scale and moved to the top right corner.
The QR code can be scanned by a mobile device, and it will automatically be connected to the Wi-Fi network.
When the MQTT connection is dropped, the disconnection error icon appears on the bottom right side of the screen. The QT PY ESP32-S2 is available in 2 models. One has a built-in Wi-Fi antenna on the PCB. The other has a uFL antenna port. This particular display was place a good distance from an access point so I opted for an external 2.4 GHz antenna which if you look carefully is mounted just to the left of the OLED display.
- CircuitPython Code (12-31-2022)
- Twilio Code (01-03-2023)
- Eagle Schematics & Board (12-13-2022)
- Sketchup & STL Files (12-13-2022)
- VCarve CNC Files (12-17-2022)