-
1Keypad Connection Points
Connection points on keypad LCD pcb:
Connection Points on button Matrix (there are also corresponding connection points on the 100 pin micro controller on opposite side of pcb but requires very fine soldering). The solder points were chosen to cause least interference with the numpad and most used function buttons to allow continued use of physical keypad. Keep solder low and flat and wires running off to the side and away from center of button contacts to maintain physical keypad use:
-
2Wiring Schematic
Optocouplers = PC817
Resistors = 210 Ohm
Note the opposite direction of X vs Y connections to the optocouplers due to the opposite direction in current flow.
-
3ESP32S3-DevkitC1 Code (UPDATED NOV. 19, 2024)
AP-mode has been removed due to too many problems. Edit the code below to manually enter your WIFI/MQTT credentials.
On board RGB LED indicator status:
Blue = WIFI and MQTT connected
Flashing Purple = AP Setup Mode
Flashing White = Attempting to connect to WIFI
Flashing Yellow = Attempting to connect to MQTT
Green = Rebooting deviceWill Attempt to connect to WIFI and MQTT for 30 seconds each and then reboot itself.
Can confirm the LCD decoder is working using serial monitor.
//ESP32 DSC NEO KEYPAD DECODER & BUTTON PUSHER BY VALDEZ - NOV/2023 //CODE IS DESIGNED FOR USE WITH ESP32-S3-WROOM-2-DEVKITC-1-N32R8V AS PER SCHEMATIC //LIBRARIES MARKED #INCLUDE <*.h> BELOW SUCH AS PUBSUBCLIENT MAY NEED TO BE INSTALLED THROUGH YOUR LIBRARY MANAGER OF YOUR IDE //DECODER CODE MODIFIED FROM LEONARDO MARTINS 19/01/2019 HD44780 DECODER CODE //OTA UPDATING ENABLED AFTER FIRST FLASH //THANKS TO USER KUSHKI7 FOR ADDITIONAL HELP WITH CODE OPTIMIZATIONS & DEBUGGING #include <PubSubClient.h> #include <Wire.h> #include <WiFi.h> #include <ESPmDNS.h> #include <NetworkUdp.h> #include <ArduinoOTA.h> //DEFINE HD44780 DECODER INPUT PINS (RS, RW, EN, DB4-7) # define RS_PIN 2 // RS (Register Select) pin # define RW_PIN 42 // RW (Read/Write) pin # define EN_PIN 1 // EN (Enable) pin # define DB4 4 # define DB5 5 # define DB6 6 # define DB7 7 //Define Pushbutton Matrix Pins # define X1 10 # define X2 11 # define X3 12 # define X4 13 # define X5 14 # define Y1 18 # define Y2 8 # define Y3 3 # define Y4 46 # define Y5 9 # define LED_BUILTIN 38 //define onboard LED pin //declare functions void IRAM_ATTR readData(void); //the ISR is to run from RAM not flash void connect_wifi(void); void connect_mqtt(void); void callback(char* topic, byte* message, unsigned int length); void buttonPush(uint8_t X, uint8_t Y, int time); void dataSpill(void); //******************************************************************** //******* ENTER YOUR WIFI AND MQTT CREDENTIALS MANUALLY HERE********** //WIFI SSID/Password credentials const char* ssid = "ENTER WIFI SSID HERE"; const char* pass = "ENTER WIFI PASSWORD HERE"; //MQTT SERVER CREDENTIALS const char* mqtt_server = "homeassistant.local"; const int mqtt_port = 1883; const char* mqtt_username = "ENTER MQTT USERNAME HERE"; const char* mqtt_password = "ENTER MQTT PASSWORD HERE"; //************************************************************************ //************************************************************************ //DECLARE GLOBAL VARIABLES FOR HD44780 DECODER volatile byte data = 0; // Storage for data volatile bool highBits = true; // flag to indicate high or low 4 bits being read volatile bool dataReady = false; // Flag to indicate when data is ready to read volatile bool highBitsInt = true; volatile char dataBuffer[32] = {0}; //databuffer to store the 32 char message sent to LCD volatile byte charCount = 0; // character counter volatile byte internalReg = 0; //internal register instruction storage //WIRELESS SETUP WiFiClient wifiClient; //create wifi instance wifiClinet PubSubClient mqttClient(wifiClient); //create mqtt instance mqttClient int countx = 0; //counter for reconnection attempts //MAIN PROGRAM SETUP void setup() { Serial.begin(115200); // Start serial monitor communication neopixelWrite(LED_BUILTIN,0,0,0); // initialize onboard led to off //initializes keypad matrix output pins pinMode(X1, OUTPUT); pinMode(X2, OUTPUT); pinMode(X3, OUTPUT); pinMode(X4, OUTPUT); pinMode(X5, OUTPUT); pinMode(Y1, OUTPUT); pinMode(Y2, OUTPUT); pinMode(Y3, OUTPUT); pinMode(Y4, OUTPUT); pinMode(Y5, OUTPUT); digitalWrite(X1, LOW); digitalWrite(X2, LOW); digitalWrite(X3, LOW); digitalWrite(X4, LOW); digitalWrite(X5, LOW); digitalWrite(Y1, LOW); digitalWrite(Y2, LOW); digitalWrite(Y3, LOW); digitalWrite(Y4, LOW); digitalWrite(Y5, LOW); //INITIALIZE INPUT PINS pinMode(RS_PIN, INPUT); pinMode(RW_PIN, INPUT); pinMode(EN_PIN, INPUT); pinMode(DB4, INPUT); pinMode(DB5, INPUT); pinMode(DB6, INPUT); pinMode(DB7, INPUT); // Setup the interrupt //note the HD44780 reads the data on the falling edge but the data is setup at the time of a rising edge which allows extra time to be captured attachInterrupt(digitalPinToInterrupt(EN_PIN), readData, RISING); connect_wifi(); //connect wifi connect_mqtt(); //connect mqtt ArduinoOTA.begin(); //initialze OTA firmware updating // OTA UPLOADING DEFAULT INFORMATION (DEFAULT PASSWORD IS "admin" unless changed) // UNCOMMENT BELOW IF WANT TO CHANGE OTA AUTHENTICATION SETTINGS FROM DEFAULT // ArduinoOTA.setPort(3232); // Port defaults to 3232 // ArduinoOTA.setHostname("myesp32"); // Hostname defaults to esp3232-[MAC] // ArduinoOTA.setPassword("admin"); // No authentication by default } //MAIN PROGRAM LOOP void loop() { ArduinoOTA.handle(); //prioritize OTA updater //reconnect wifi if disconnected if (!WiFi.isConnected()) { connect_wifi(); } //reconnect mqtt if disconnected if (!mqttClient.connected()) { connect_mqtt(); } mqttClient.loop(); //mqtt message check //send LCD message to MQTT if ready if (dataReady){ dataSpill(); } } //WIFI SETUP & CONNECT FUNCTION void connect_wifi() { neopixelWrite(LED_BUILTIN,0,0,0); delay(1000); Serial.println(); Serial.print("Connecting to "); Serial.println(ssid); WiFi.begin(ssid, pass); while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); } Serial.println(""); Serial.println("WiFi connected"); Serial.println("IP address: "); Serial.println(WiFi.localIP()); neopixelWrite(LED_BUILTIN,32,0,32); }; //MQTT SETUP & CONNECT FUNCTION void connect_mqtt() { mqttClient.setServer(mqtt_server, mqtt_port); mqttClient.setCallback(callback); mqttClient.connect("esp32-DSC-Client", mqtt_username, mqtt_password); countx++; //increases counter each time it attempts a connection //attempt 15 connection attempts (30 seconds) then restart if (mqttClient.connected()) { Serial.println("connected to MQTT"); mqttClient.subscribe("esp32/output"); //topic subscribed to for button pushing messages neopixelWrite(LED_BUILTIN,0,0,32); //if MQTT connects change LED to blue countx = 0; } else { if (countx >= 15) { ESP.restart(); } Serial.print("MQTT connection failed, try again in 2 seconds"); // Wait 2 seconds while blinking orange LED delay(500); neopixelWrite(LED_BUILTIN,0,0,0); delay(500); neopixelWrite(LED_BUILTIN,32,16,0); delay(500); neopixelWrite(LED_BUILTIN,0,0,0); delay(500); } } //CALLBACK FUNCTION TO RESPOND TO MQTT MESSAGES RECEIVED - PUSHES BUTTONS DEPENDING ON PAYLOAD RECEIVED void callback(char* topic, byte* message, unsigned int length) { Serial.print("Message arrived on topic: "); Serial.print(topic); Serial.print(". Message: "); String messageTemp; //PRINT OUT THE RECEIVED MSG TO SERIAL MONITOR for (int i = 0; i < length; i++) { Serial.print((char)message[i]); messageTemp += (char)message[i]; } Serial.println(""); //CODE TO CALL BUTTONPUSH CONDITIONAL ON THE PAYLOAD RECEIVED if (messageTemp == "1"){ buttonPush(X1, Y1, 100); } else if (messageTemp == "2"){ buttonPush(X2,Y1,100); } else if (messageTemp == "3"){ buttonPush(X3,Y1,100); } else if (messageTemp == "4"){ buttonPush(X1,Y2,100); } else if (messageTemp == "5"){ buttonPush(X2,Y2,100); } else if (messageTemp == "6"){ buttonPush(X3,Y2,100); } else if (messageTemp == "7"){ buttonPush(X1,Y3,100); } else if (messageTemp == "8"){ buttonPush(X2,Y3,100); } else if (messageTemp == "9"){ buttonPush(X3,Y3,100); } else if (messageTemp == "*"){ buttonPush(X1,Y4,100); } else if (messageTemp == "0"){ buttonPush(X2,Y4,100); } else if (messageTemp == "#"){ buttonPush(X3,Y4,100); } else if (messageTemp == "STAY"){ buttonPush(X4,Y1,2000); } else if (messageTemp == "AWAY"){ buttonPush(X4,Y2,2000); } else if (messageTemp == "CHIME"){ buttonPush(X4,Y3,2000); } else if (messageTemp == "F4"){ buttonPush(X4,Y4,2000); } else if (messageTemp == "<"){ buttonPush(X5,Y1,100); } else if (messageTemp == ">"){ buttonPush(X5,Y2,100); } else if (messageTemp == "NIGHT"){ buttonPush(X5,Y3, 2000); } else if (messageTemp == "reset"){ ESP.restart(); // ability to restart ESP32 from MQTT with "reset" payload } } //BUTTON PUSHING FUNCTION void buttonPush(uint8_t X, uint8_t Y, int time){ neopixelWrite(LED_BUILTIN,32,0,32); // blink purple each time a button is pushed digitalWrite(X, HIGH); digitalWrite(Y, HIGH); delay(time); //length of button push digitalWrite(X, LOW); digitalWrite(Y, LOW); neopixelWrite(LED_BUILTIN,0,0,32); delay(100); //IF PUSHING MULTIPLE BUTTONS QUICKLY BY AUTOMATION NEED TO HAVE A DELAY IN BETWEEN } //ISR FUNCTION TO READ THE 4 bit DATA WHEN THE ENABLE PIN TRIGGERS THE INTERRUPT void IRAM_ATTR readData() { // Check that data is being written (RS == 1) // store the 4-bit parallel data into the LSB (0000xxxx) of variable part if (digitalRead(RS_PIN) == HIGH) { byte part = 0; part |= digitalRead(DB4); part |= digitalRead(DB5) << 1; part |= digitalRead(DB6) << 2; part |= digitalRead(DB7) << 3; if (highBits) { // This is the high bits (MSB) (xxxx0000) part of the 8-bit data data = part << 4; //assign and shift the 4 bit part to the most sig bit positions of the 8 bit byte } else { // This is the low bits part (LSB) (0000xxxx) data |= part; //copy the 4 bit part to the least sig bit end of the 8 bit byte using bitwise OR dataBuffer[charCount++ & 31] = data; //store the full 8 bit character data byte in the buffer array and increment the counter if (!(charCount & 31)) dataReady = true, highBitsInt = true; // once counter reaches 32 set dataready flag, reset internal register highbits flag to keep in sync. //above statements uses bitwise & (X & (n-1)) similar to modulo (X % n) to repeatedly loop through the array with minimal processing; This only works when n is a power of 2. } // Flip the highBits flag to alternate inputting the 4 bits from the high bits to low bits highBits = !highBits; //if controller is not writing to the LCD (RS == LOW), check if it is writing internal register instructions for the new message instruction (0x80) to keep messages in sync //maintain a separate highbits flag for internal register instructions so each type of write instruction can re-sync the other } else if (digitalRead(RW_PIN) == LOW) { byte partInternal = 0; partInternal |= digitalRead(DB4); partInternal |= digitalRead(DB5) << 1; partInternal |= digitalRead(DB6) << 2; partInternal |= digitalRead(DB7) << 3; if (highBitsInt) { internalReg = partInternal << 4; //shift the high bits into the internal register instruction storage } else { internalReg |= partInternal; if (internalReg == 0x80) { // if new message instruction detected resync the highBits flag and charCount to keep messages in sync. charCount = 0; highBits = true; } } highBitsInt = !highBitsInt; } } //FUNCTION TO WORK WITH THE STORED DATABUFFER AFTER INTERRUPT COMPLETES //SEND THE MESSAGE TO MQTT WHEN THE DATA READY FLAG IS TRUE void dataSpill(){ noInterrupts(); //no interrupts must be set while manipulting volatile data that can also be changed by the ISR dataReady = false; //set flag to false so message is not re-sent until next message arrives interrupts(); char *outputx = (char *) dataBuffer; // setup pointer to the databuffer char array to act like a string mqttClient.publish("esp32/alarm", outputx); //send the message to mqtt for(int i = 0; i < 32; i++){ Serial.print(outputx[i]); // prints the lcd message to the serial monitor } Serial.printf("\n"); /* for(int i = 0; i < 32; i++){ //for debugging purposes, prints the hex codes of each char to serial monitor. Serial.printf("%02x ", outputx[i] ); //for debugging purposes, prints the hex codes of each char to serial monitor. } //for debugging purposes, prints the hex codes of each char to serial monitor. Serial.printf("\n"); //for debugging purposes, prints the hex codes of each char to serial monitor. */ }
-
4Home Assistant Code
Once MQTT is set up in Home Assistant, set up a sensor to listen for the LCD changes:
mqtt: sensor: - name: "Alarm Status" state_topic: "esp32/alarm"
Home Assistant Dashboard Card Code for Virtual Keypad. The zones must be added and set up unique to each system with zone sensors as per original project description
type: vertical-stack cards: - type: markdown content: |- <font size="4">{{ states.sensor.alarm_status.state}} </font> - square: false type: grid cards: - show_name: false show_icon: true type: button tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '1' icon: mdi:numeric-1-box-outline show_state: false - show_name: false show_icon: true type: button icon: mdi:numeric-2-box-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '2' - show_name: false show_icon: true type: button icon: mdi:numeric-3-box-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '3' - show_name: false show_icon: true type: button icon: mdi:numeric-4-box-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '4' - show_name: false show_icon: true type: button tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '5' icon: mdi:numeric-5-box-outline show_state: false - show_name: false show_icon: true type: button icon: mdi:numeric-6-box-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '6' - show_name: false show_icon: true type: button icon: mdi:numeric-7-box-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '7' - show_name: false show_icon: true type: button tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '8' icon: mdi:numeric-8-box-outline show_state: false - show_name: false show_icon: true type: button icon: mdi:numeric-9-box-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '9' - show_name: false show_icon: true type: button icon: mdi:asterisk-circle-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '*' - show_name: false show_icon: true type: button tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '0' icon: mdi:numeric-0-box-outline show_state: false - show_name: false show_icon: true type: button icon: mdi:pound-box-outline show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '#' - show_name: false show_icon: true type: button icon: mdi:door-closed-lock show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: STAY - show_name: false show_icon: true type: button icon: mdi:door-open show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: AWAY - show_name: false show_icon: true type: button icon: mdi:sleep show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: NIGHT - show_name: false show_icon: true type: button icon: mdi:arrow-left-bold show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: < - show_name: false show_icon: true type: button icon: mdi:bell show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: CHIME - show_name: false show_icon: true type: button icon: mdi:arrow-right-bold show_state: false tap_action: action: call-service service: mqtt.publish target: {} data: qos: 0 retain: false topic: esp32/output payload: '>' columns: 3 - type: entities entities: - entity: binary_sensor.front_door - entity: binary_sensor.zone2 - entity: binary_sensor.alarm_triggered icon: mdi:alarm-bell name: Alarm Triggered title: Zones state_color: true show_header_toggle: false
-
5Zone Setup
This section highly depends on how each zone label is programmed on the individual keypad.
It also requires the auto scroll function for faulted zones to be enabled in the keypad settings.
To integrate the zones, create a binary sensor for each zone that responds to the keypad LCD displaying that particular zone is faulted as the faulted zones are scrolled.
For example, for a door on zone 1:
- binary_sensor: - name: Zone01 state: > {% if is_state('sensor.alarm_status', ('Front Door 01 <> '),) %} on {% else %} off {% endif %}
('Front Door 01 <> ') may display differently depending on your keypad depending zone labeling. It is best to listen in on the MQTT service to topic esp32/alarm, fault the zone, and copy and paste it exactly as it is received into your code.
However, this will turn the sensor on as "Front Door 01 <> " flashes on the keypad lcd screen but then it will turn the sensor off when the lcd displays something else even if the door is still open. Therefore the sensor blinks on/off while the door is open, which is not ideal.
We need a sensor that latched on while the door is open and then turn off when the door is closed. To latch the state add a corresponding Input Boolean and use automations to latch them on when the binary sensor is detected turning on, and off when either "System Is Ready to Arm" is displayed or when the binary sensor doesn't turn back on for at least 6 seconds.
The 6 second rule addresses when multiple zones are triggered, as Ready to Arm will not display with other zones faulted, but the zone at issue may have been restored while other zones are still faulted. This way if the automation doesn't re-see the zone within 6 seconds in the scrolling list of faulted zones it assumes its closed. You can change this time to any length but I found 6 seconds worked well for many zones being faulted. A more precise method would be to have the code check between the displaying of "Scroll to View <> Open Zones" to see if the zone re-appears in the scrolling list as that message signifies the start/end of the scrolling list.
You should also set up a bypass sensor that tells if the keypad has entered the bypass zone selection section of the menu or not. This way the virtual keypad doesn't show the zones are triggered as you are scrolling through the list of zones to bypass (as the same descriptions will appear on the lcd. The code would look like:
An example of the the input booleans for one zone and bypass flag as follows:
Configuration.yaml:
input_boolean: zone01: name: Front Door 01 icon: mdi:electric-switch zonebypassmode: name: Bypassmode Flag icon: mdi:electric-switch
Automations.yaml:
- id: '1234567890' alias: Zone 01 Front Door description: Front Door Zone 1 trigger: - platform: state entity_id: - binary_sensor.zone01 from: to: 'on' condition: - condition: state entity_id: input_boolean.zonebypassmode state: 'off' action: - service: input_boolean.turn_on data: {} target: entity_id: input_boolean.zone01 - wait_for_trigger: - platform: state entity_id: - binary_sensor.zone01 to: 'off' for: hours: 0 minutes: 0 seconds: 6 - platform: state entity_id: - sensor.alarm_status to: 'System is Ready to Arm ' - service: input_boolean.turn_off data: {} target: entity_id: input_boolean.zone01 mode: single - id: '0987654321' alias: Zone Bypass On description: '' trigger: - platform: state entity_id: - sensor.alarm_status to: 'Zone Bypass <>(*) To Bypass ' from: 'Press (*) for <>Zone Bypass ' condition: - condition: state entity_id: input_boolean.zonebypassmode state: 'off' action: - service: input_boolean.turn_on data: {} target: entity_id: input_boolean.zonebypassmode - wait_for_trigger: - platform: state entity_id: - sensor.alarm_status to: 'System is Ready to Arm ' - service: input_boolean.turn_off data: {} target: entity_id: input_boolean.zonebypassmode mode: single
Lastly I added another binary sensor to the config that tracks the state of the latching input Boolean. I do this because I don't want the on/off toggle switches on my zones and I want to utilize the various device classes of the binary sensors to show if it is a door, window, motion, garage, etc. There is probably a more elegant way to code this all using variables, but it works.
- binary_sensor: - name: Front Door Zone 01 device_class: door state: "{{ states('input_boolean.zone01') }}"
When programming motion sensors you will want the latch to stay on for a set amount of time. I found especially the wired motion sensors open and close so fast if you don't set the latch to a longer period you wont see them being triggered in your list of zones - although your automations will still be triggered.
Discussions
Become a Hackaday.io Member
Create an account to leave a comment. Already have an account? Log In.