diff --git a/README.md b/README.md index de12309..440a61f 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # DSC Keybus Interface ![dscKeybusInterface](https://user-images.githubusercontent.com/12835671/105620980-5b356380-5dc8-11eb-93c2-e813751dda8a.png) -This library directly interfaces Arduino, esp8266, and esp32 microcontrollers to [DSC PowerSeries](http://www.dsc.com/dsc-security-products/g/PowerSeries/4) security systems for integration with home automation, notifications on alarm events, and direct control as a virtual keypad. This enables existing DSC security system installations to retain the features and reliability of a hardwired system while integrating with modern devices and software for under $5USD in components. +This library directly interfaces Arduino, esp8266, esp32, and esp32-s2 microcontrollers to [DSC PowerSeries](http://www.dsc.com/dsc-security-products/g/PowerSeries/4) and [Classic series](https://www.dsc.com/manual/29000203) security systems for integration with home automation, remote control as a virtual keypad, notifications on alarm events, unlocking installer codes, and emulating DSC panels to use DSC keypads as general purpose input devices. + +This enables existing DSC security system installations to retain the features and reliability of a hardwired system while integrating with modern devices and software for under $5USD in components. The built-in examples can be used as-is or as a base to adapt to other uses: * Home automation integration: [Home Assistant](https://www.home-assistant.io), [Apple HomeKit & Siri](https://www.apple.com/ios/home/), [Google Home](https://assistant.google.com), [OpenHAB](https://www.openhab.org), [Athom Homey](https://www.athom.com/en/) -* Notifications: [Telegram](https://www.telegram.org) bot, [PushBullet](https://www.pushbullet.com), [Twilio SMS](https://www.twilio.com), E-mail -* Virtual keypad: Web interface, [Blynk](https://www.blynk.cc) mobile app -* Installer code unlocking: automatic code search to unlock panels with unknown installer codes - -See the [dscKeybusInterface-RTOS](https://github.com/taligentx/dscKeybusInterface-RTOS) repository for a port of this library to [esp-open-rtos](https://github.com/SuperHouse/esp-open-rtos) - this enables a standalone esp8266 HomeKit accessory using [esp-homekit](https://github.com/maximkulkin/esp-homekit). +* Remote control: Web interface, [Blynk](https://www.blynk.cc) mobile app, [Telegram](https://www.telegram.org) bot (with remote arming/disarming via chat) +* Notifications: [Pushover](https://www.pushover.net), [PushBullet](https://www.pushbullet.com), [Pushsafer](https://www.pushsafer.com), [Twilio SMS](https://www.twilio.com), [TinyGSM SMS](https://github.com/vshymanskyy/TinyGSM), E-mail +* Keypad interface: Emulates a DSC panel to connect DSC keypads as physical input devices for any general purpose, without a DSC panel. +* Installer code unlocking: Automatic code search to unlock panels with unknown installer codes Example integrations: * [Apple Home & Siri](https://www.apple.com/ios/home/): @@ -40,15 +41,15 @@ Example integrations: **I Had**: _A DSC security system not being monitored by a third-party service._ **I Wanted**: _Notification if the alarm triggered._ -I was interested in finding a solution that directly accessed the pair of data lines that DSC uses for their proprietary Keybus protocol to send data between the panel, keypads, and other modules. Tapping into the data lines is an ideal task for a microcontroller and also presented an opportunity to work with the [Arduino](https://www.arduino.cc) and [FreeRTOS](https://www.freertos.org) (via [esp-open-rtos](https://github.com/SuperHouse/esp-open-rtos)) platforms. +I was interested in finding a solution that directly accessed the pair of data lines that DSC uses for their proprietary Keybus protocol to send data between the panel, keypads, and other modules (instead of using the DSC IT-100 serial module). Tapping into the data lines is an ideal task for a microcontroller and also presented an opportunity to work with the [Arduino](https://www.arduino.cc) and [FreeRTOS](https://www.freertos.org) (via [esp-open-rtos](https://github.com/SuperHouse/esp-open-rtos)) platforms. -While there has been excellent [discussion about the DSC Keybus protocol](https://www.avrfreaks.net/forum/dsc-keybus-protocol) and a several existing projects, there were a few issues that remained unsolved: +While there has been excellent [discussion about the DSC Keybus protocol](https://www.avrfreaks.net/forum/dsc-keybus-protocol) and several existing projects, there were a few issues that remained unsolved: * Error-prone Keybus data capture. * Limited data decoding - there was good progress for armed/disarmed states and partial zone status for a single partition, but otherwise most of the data was undecoded (notably missing the alarm triggered state). * Read-only - unable to control the Keybus to act as a virtual keypad. * No implementations to do useful work with the data. -Poking around with a logic analyzer and oscilloscope revealed that the errors capturing the Keybus data were timing issues - after resolving the data errors, it was possible to reverse engineer the protocol by capturing the Keybus binary data as the security system handled various events. +This library uses a combination of hardware and timer interrupts to accurately capture Keybus data, and has lead to reverse engineering much of the Keybus protocol. ## Features * Monitor the status of all partitions: @@ -60,22 +61,26 @@ Poking around with a logic analyzer and oscilloscope revealed that the errors ca * Monitor PGM outputs 1-14 status * Virtual keypad: - Write keys to the panel for all partitions -* Panel time - retrieve current panel date/time and set a new date/time + - Trigger panel command outputs +* Keypad interface: + - Emulates a DSC panel to use DSC PowerSeries and Classic series keypads as physical input devices for any general purpose, without needing a DSC panel. +* Panel time - retrieve current panel date/time and set a new date/time (including an example with NTP sync) * Panel installer code unlocking - determine the 4-digit panel installer code +* Virtual zone expander - the [expander branch](https://github.com/taligentx/dscKeybusInterface/tree/expander) can emulate a DSC zone expander module to add zones to the security system that are handled by the microcontroller - thanks to [Dilbert66](https://github.com/Dilbert66) for this contribution! * Direct Keybus interface: - Does not require the [DSC IT-100 serial interface](https://www.dsc.com/alarm-security-products/IT-100%20-%20PowerSeries%20Integration%20Module/22). * Designed for reliable data decoding and performance: - - Pin change and timer interrupts for accurate data capture timing + - Hardware GPIO pin interrupts and timer interrupts for accurate data capture timing - Data buffering: helps prevent lost Keybus data if the sketch is busy - Extensive data decoding: the majority of Keybus data as seen in the [DSC IT-100 Data Interface developer's guide](https://cms.dsc.com/download.php?t=1&id=16238) has been reverse engineered and documented in [`src/dscKeybusPrintData.cpp`](https://github.com/taligentx/dscKeybusInterface/blob/master/src/dscKeybusPrintData.cpp). - Non-blocking code: Allows sketches to run as quickly as possible without using `delay` or `delayMicroseconds` * Supported security systems: - - [DSC PowerSeries](https://www.dsc.com/?n=enduser&o=identify) - - Verified panels: PC585, PC1555MX, PC1565, PC5005, PC5010, PC5015, PC5020, PC1616, PC1808, PC1832, PC1864. - - All PowerSeries series are supported, please [post an issue](https://github.com/taligentx/dscKeybusInterface/issues) if you have a different panel and have tested the interface to update this list. + - [DSC PowerSeries](https://www.dsc.com/?n=enduser&o=identify) - all panels are supported, tested with: PC585, PC1555MX, PC1565, PC1565-2P, PC5005, PC5010, PC5015, PC5020, PC1616, PC1808, PC1832, PC1864 + - [DSC Classic series](https://www.dsc.com/?n=enduser&o=identify): PC1500, PC1550, PC2550 + * Requires configuring the panel through *8 programming to enable PC16-OUT: section 19, option 4. + * PC2500 and PC3000 are untested, [post an issue](https://github.com/taligentx/dscKeybusInterface/issues) if you're able to test these panels. - Rebranded DSC PowerSeries (such as some ADT systems) should also work with this interface. * Unsupported security systems: - - DSC Classic series ([PC1500, PC1550, etc](https://www.dsc.com/?n=enduser&o=identify)) use a different data protocol, though support is possible. - DSC Alexor (PC9155) is all wireless and does not have an accessible Keybus interface. - DSC Neo series use a higher speed encrypted data protocol (Corbus) that is not currently possible to support. - Other brands (that are not rebranded DSC systems) use different protocols and are not supported. @@ -90,20 +95,36 @@ Poking around with a logic analyzer and oscilloscope revealed that the errors ca * Includes [Arduino framework support](https://github.com/esp8266/Arduino) and WiFi for ~$3USD shipped. - esp32: * Development boards: NodeMCU ESP-32S, Doit ESP32 Devkit v1, Wemos Lolin D32, etc. - * Includes [Arduino framework support](https://github.com/espressif/arduino-esp32) (v1.0.5-rc6 or newer required), dual cores, WiFi, and Bluetooth for ~$5USD shipped. + * Includes [Arduino framework support](https://github.com/espressif/arduino-esp32) (v2.0.2 or newer required), dual cores, WiFi, and Bluetooth for ~$5USD shipped. + - esp32-s2: + * Includes [Arduino framework support](https://github.com/espressif/arduino-esp32) (v2.0.2 or newer required) and WiFi. * Possible features (PRs welcome!): - [DSC IT-100](https://cms.dsc.com/download.php?t=1&id=16238) emulation - Unlock 6-digit installer codes - - DSC Classic series support: This protocol is [already decoded](https://github.com/dougkpowers/pc1550-interface), use with this library would require major changes. ## Release notes +* 3.0 + - New: DSC Classic series panel support: PC1500, PC1550, PC2550 + - New: `KeypadInterface` and `KeypadInterface-MQTT` example sketches - emulate a DSC panel to connect DSC PowerSeries and Classic keypads as physical input devices for any general purpose, without needing a DSC panel. + - New: `HomeKit-HomeSpan` example sketch (esp32) - integrate directly with Apple HomeKit as a native standalone accessory using [HomeSpan](https://github.com/HomeSpan/HomeSpan) + - New: [Pushover](https://www.pushover.net) and [Pushsafer](https://www.pushsafer.com) push notification example sketches for esp8266/esp32 + - New: esp32-s2 microcontroller support + - New: Code restructured to support new features from sketches using #define flags - enables Classic series support and `KeypadInterface`. + - Updated: `Homebridge-MQTT` supports switching armed modes while armed + - Updated: Added TLS root certificate to `Twilio-SMS` + - Updated: removed deprecated `handlePanel()` + - Bugfix: `VirtualKeypad-Web` updated notes to switch to [this fork of ESPAsyncWebServer](https://github.com/arjenhiemstra/ESPAsyncWebServer) to resolve crashes with iOS and macOS clients. + - Bugfix: `Pushbullet` example sketch updated TLS security certificate fingerprint + - Bugfix: Workaround for [upstream esp32 TLS handshake issue](https://github.com/espressif/arduino-esp32/issues/6165) preventing making a TLS connection more than once. + - Bugfix: Fixed `Homebridge-MQTT` handling exit delay states while multiple partitions are arming + - Bugfix: Resolved access codes not updating on disarm, changed arming access codes to update before armed status changes * 2.0 - New: [Telegram](https://www.telegram.org) bot example sketch - New: [OpenHAB](https://www.openhab.org) integration example sketch using MQTT - New: `Unlocker` example sketch - determines the panel installer code - New: `TimeSyncNTP` example sketch - uses NTP to automatically set the panel time - New: [ESPHome](https://esphome.io) integration example (located in the `extras` directory) - thanks to [Dilbert66](https://github.com/Dilbert66) for this contribution! - - New: `TinyGMS-SMS` example sketch - sends status via SMS with a GSM modem - thanks to [jvitkauskas](https://github.com/jvitkauskas) for this contribution! + - New: `TinyGSM-SMS` example sketch - sends status via SMS with a GSM modem - thanks to [jvitkauskas](https://github.com/jvitkauskas) for this contribution! - New: `KeybusReaderIP` example sketch enables Keybus data access over IP, thanks to [aboulfad](https://github.com/aboulfad) for this contribution! - New: esp32 microcontroller support - requires [Arduino-esp32](https://github.com/espressif/arduino-esp32) v1.0.5-rc6 or newer - New: Features for sketches: @@ -192,8 +213,10 @@ The included examples demonstrate how to use the library and can be used as-is o * Panel trouble * Keybus connected +* **HomeKit-HomeSpan** (esp32): Integrates directly with Apple HomeKit as a native accessory (for the Home app and Siri) using [HomeSpan](https://github.com/HomeSpan/HomeSpan), without needing a separate service or device. Demonstrates arming/disarming partitions, zones status, fire alarms, PGM outputs status, and controlling panel command outputs. + - For esp8266, the [dscKeybusInterface-RTOS](https://github.com/taligentx/dscKeybusInterface-RTOS) library includes a native HomeKit implementation that runs directly on the esp8266, without requiring a separate device running MQTT or Homebridge. + * **Homebridge-MQTT**: Interfaces with [Homebridge](https://github.com/nfarina/homebridge) via MQTT to integrate with Apple HomeKit (including the iOS Home app and Siri) and [Google Home](https://github.com/oznu/homebridge-gsh). Demonstrates arming/disarming partitions and for HomeKit, viewing the status of zones, PGM outputs, and fire alarms. - - The [dscKeybusInterface-RTOS](https://github.com/taligentx/dscKeybusInterface-RTOS) library includes a native HomeKit implementation that runs directly on esp8266, without requiring a separate device running MQTT or Homebridge. * **HomeAssistant-MQTT**: Interfaces with [Home Assistant](https://www.home-assistant.io) via MQTT. Demonstrates arming/disarming partitions and viewing the status of zones, PGM outputs, fire alarms, and trouble. For esp8266/esp32, the partition status is available as a text message for display. @@ -203,32 +226,37 @@ The included examples demonstrate how to use the library and can be used as-is o * **Homey**: Integrates with [Athom Homey](https://www.athom.com/en/) and the [Homeyduino](https://github.com/athombv/homey-arduino-library/) library, including armed, alarm, and fire states (currently limited to one partition), and zone states. Thanks to [MagnusPer](https://github.com/MagnusPer) for contributing this example! -* **Telegram** (esp8266/esp32): Demonstrates sending status updates and arming/disarming the security system via a [Telegram](https://www.telegram.org) bot. +* **Telegram** (esp8266/esp32): Demonstrates sending status updates as push notifications and arming/disarming the security system via a [Telegram](https://www.telegram.org) bot. Supports iOS, Android, and macOS/Windows/Linux desktop notifications (free). + +* **Pushover** (esp8266/esp32): Demonstrates sending status updates as push notifications via [Pushover](https://www.pushover.net). Supports iOS, Android, macOS native desktop notifications, and Chrome/Firefox/Safari browser popups ($4.99USD one-time purchase per client platform). -* **Pushbullet** (esp8266/esp32): Demonstrates sending status updates as a push notification via [Pushbullet](https://www.pushbullet.com). +* **Pushbullet** (esp8266/esp32): Demonstrates sending status updates as push notifications via [Pushbullet](https://www.pushbullet.com). Supports Android, Windows desktop notifications, and Chrome/Firefox browser popups (free). Note that iOS is no longer supported. -* **Twilio-SMS** (esp8266/esp32): Demonstrates sending status updates as an SMS text message via [Twilio](https://www.twilio.com) - thanks to [ColingNG](https://github.com/ColinNg) for contributing this example! +* **Pushsafer** (esp8266/esp32): Demonstrates sending status updates as push notifications via [Pushsafer](https://www.pushsafer.com). Supports iOS, Android, Windows desktop notifications, and Chrome/Firefox/Edge/Opera/Yandex browser popups (€0.99EUR or less per 1000 notifications). -* **Email** (esp8266/esp32): Demonstrates sending status updates as an email. Email is sent using SMTPS (port 465) with SSL for encryption - this is necessary on the esp8266/esp32 until STARTTLS can be supported. For example, this will work with Gmail after changing the account settings to [allow less secure apps](https://support.google.com/accounts/answer/6010255). +* **Twilio-SMS** (esp8266/esp32): Demonstrates sending status updates as SMS text messages via [Twilio](https://www.twilio.com) - thanks to [ColingNG](https://github.com/ColinNg) for contributing this example! + +* **Email** (esp8266/esp32): Demonstrates sending status updates as email. Email is sent using SMTPS (port 465) with SSL for encryption - this is necessary on the esp8266/esp32 until STARTTLS can be supported. For example, this will work with Gmail after changing the account settings to [allow less secure apps](https://support.google.com/accounts/answer/6010255). This can be used to send SMS text messages if the number's service provider has an [email to SMS gateway](https://en.wikipedia.org/wiki/SMS_gateway#Email_clients) - examples for the US: * T-mobile: 5558675309@tmomail.net * Verizon: 5558675309@vtext.com - * Sprint: 5558675309@messaging.sprintpcs.com * AT&T: 5558675309@txt.att.net -* **VirtualKeypad-Blynk** (esp8266/esp32): Provides a virtual keypad interface for the free [Blynk](https://www.blynk.cc) app on iOS and Android, including viewing alarm memory, programming zone lights, and the event buffer. Scan one of the following QR codes from within the Blynk app for an example keypad layout: +* **VirtualKeypad-Blynk** (esp8266/esp32): Provides a virtual keypad interface for the free [Blynk legacy](https://www.blynk.cc) app on iOS and Android, including viewing alarm memory, programming zone lights, and the event buffer. The newer generation Blynk app is not currently supported. Scan one of the following QR codes from within the Blynk app for an example keypad layout: - [Virtual keypad with 16 zones](https://user-images.githubusercontent.com/12835671/103719316-5f6f1d80-4f8e-11eb-8a7c-4bd7bfe3cd8a.png) - [Virtual keypad with 32 zones](https://user-images.githubusercontent.com/12835671/103719459-af4de480-4f8e-11eb-8e4a-7172961e2d29.png) - [Virtual keypad with 8 zones and event log](https://user-images.githubusercontent.com/12835671/103719518-cc82b300-4f8e-11eb-8b2a-97299e7be3a2.png) - Note: Installing [Blynk as a local server](https://github.com/blynkkk/blynk-server) is recommended to keep control of the security system internal to your network. This also lets you use as many widgets as needed for free - local servers can setup users with any amount of Blynk Energy. Using the default Blynk cloud service with the above example layouts requires more of Blynk's Energy units than available on the free usage tier. + Note: Installing [Blynk as a local server](https://github.com/blynkkk/blynk-server) is recommended to keep control of the security system internal to your network. * **VirtualKeypad-Web** (esp8266/esp32): Provides a virtual keypad web interface, using the esp8266/esp32 itself as a standalone web server, including viewing alarm memory, programming zone lights, and the event buffer. Thanks to [Elektrik1](https://github.com/Elektrik1) for contributing this example! -* **TimeSyncNTP**: Synchronizes and maintains the panel time via an NTP server, including DST adjustments. +* **TimeSyncNTP**: Synchronizes and maintains time on PowerSeries panels via an NTP server, including DST adjustments. + +* **Unlocker**: Finds the 4-digit installer code for PowerSeries panels by checking all possible codes, including handling keypad lockout if enabled. The valid code is output to serial as well as repeatedly flashed with the built-in LED. Arduino checks each code sequentially but esp8266/esp32 may find the code more quickly as they check in order of the [most commonly used general 4-digit codes](https://www.datagenetics.com/blog/september32012). -* **Unlocker**: Checks all possible 4-digit installer codes until a valid code is found, including handling keypad lockout if enabled. The valid code is output to serial as well as repeatedly flashed with the built-in LED. +* **KeypadInterface**: Interfaces directly to DSC PowerSeries and Classic series (tested with PC1500RK) keypads (without a DSC panel) to enable using these as physical inputs for any general purpose. Examples included for interfacing via serial and MQTT. Note that this uses a different wiring setup from the standard Keybus interface, refer to the wiring diagram in the example sketch. * **KeybusReader**: Decodes and prints data from the Keybus to a serial interface, including reading from serial for the virtual keypad. This can be used to help decode the Keybus protocol and is also handy as a troubleshooting tool to verify that data is displayed without errors. For esp8266/esp32, `KeybusReaderIP` enables connectivity over WiFi. @@ -249,34 +277,45 @@ DSC Aux(+) ---+--- Arduino Vin pin DSC Aux(-) --- Arduino/esp8266/esp32 Ground - Arduino +--- dscClockPin (Arduino Uno: 2,3) + Arduino +--- dscClockPin (Arduino Uno: 3) DSC Yellow ---+--- 15k ohm resistor ---| | +--- 10k ohm resistor --- Ground | - | esp8266/esp32 +--- dscClockPin (esp8266: D1,D2,D8 / esp32: 4,13,16-39) + | esp8266/esp32 +--- dscClockPin (esp8266: D1, GPIO 5 / esp32: 18) +--- 33k ohm resistor ---| +--- 10k ohm resistor --- Ground - Arduino +--- dscReadPin (Arduino Uno: 2-12) + Arduino +--- dscReadPin (Arduino Uno: 5) DSC Green ----+--- 15k ohm resistor ---| | +--- 10k ohm resistor --- Ground | - | esp8266/esp32 +--- dscReadPin (esp8266: D1,D2,D8 / esp32: 4,13,16-39) + | esp8266/esp32 +--- dscReadPin (esp8266: D2, GPIO 4 / esp32: 19) + +--- 33k ohm resistor ---| + +--- 10k ohm resistor --- Ground + +Classic series only, PGM configured for PC-16 output: +DSC PGM ------+--- 1k ohm resistor --- DSC Aux(+) + | + | Arduino +--- dscPC16Pin (Arduino Uno: 4) + +--- 15k ohm resistor ---| + | +--- 10k ohm resistor --- Ground + | + | esp8266/esp32 +--- dscPC16Pin (esp8266: D7, GPIO 13 / esp32: 17) +--- 33k ohm resistor ---| +--- 10k ohm resistor --- Ground Virtual keypad (optional): DSC Green ---- NPN collector --\ - |-- NPN base --- 1k ohm resistor --- dscWritePin (Arduino Uno: 2-12 / esp8266: D1,D2,D8 / esp32: 4,13,16-33) + |-- NPN base --- 1k ohm resistor --- dscWritePin (Arduino Uno: 6 / esp8266: D8, GPIO 15 / esp32: 21) Ground --- NPN emitter --/ ``` -* The DSC Keybus operates at ~12.6v, a pair of resistors per data line will bring this down to an appropriate voltage for each microcontroller. +* The DSC PowerSeries Keybus operates at ~12.6v and the Classic series operates at ~13.75v, a pair of resistors per data line will bring this down to an appropriate voltage for each microcontroller. * Arduino: * The DSC yellow (clock) line connects to a [hardware interrupt pin](https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/) - for the Uno, these are pins 2 or 3. The example sketches use dscClockPin: 3. * The DSC green (data) line can be connected to any of the remaining digital pins 2-12. The examples sketches use dscReadPin: 5 and dscWritePin: 6. - * esp8266: connect the DSC lines to GPIO pins that are normally low to avoid putting spurious data on the Keybus: D1 (GPIO5), D2 (GPIO4) and D8 (GPIO15). The example sketches use dscClockPin: D1, dscReadPin: D2, dscWritePin: D8. - * esp32: connect the DSC lines to GPIO pins that do not send signals at boot: 4, 13, 16-39. For virtual keypad, use pins 4, 13, 16-33 - pins 34-39 are input only and cannot be used. The example sketches use dscClockPin: 18, dscReadPin: 19, dscWritePin: 21. + * esp8266: connect the DSC lines to GPIO pins that are normally low to avoid putting spurious data on the Keybus: D1 (GPIO5), D2 (GPIO4) and D8 (GPIO15). The example sketches use dscClockPin: D1, dscReadPin: D2, dscWritePin: D8. For the Classic series, dscPC16Pin: D7 (GPIO 13). + * esp32: connect the DSC lines to GPIO pins that do not send signals at boot: 4, 13, 16-39. For virtual keypad, use pins 4, 13, 16-33 - pins 34-39 are input only and cannot be used. The example sketches use dscClockPin: 18, dscReadPin: 19, dscWritePin: 21. For the Classic series, dscPC16Pin: 17. * Virtual keypad uses an NPN transistor and a resistor to write to the Keybus. Most small signal NPN transistors should be suitable, for example: * 2N3904 * BC547, BC548, BC549 @@ -321,6 +360,11 @@ Panel options affecting this interface, configured by `*8 + installer code` - se - AC power failure reporting delay: The default delay is 30 minutes and can be set to `000` to immediately report a power failure. +* PC1500/PC1550 Classic series - the following configuration is required to get the security system status: + - Communicator: Enable in section `12`, option `1` to support PC16-OUT mode + - PC16-OUT: Enable section `13`, option `4` to set the PGM output to PC16-OUT mode to send required panel status data on the Keybus. + - PGM output: Enable section `24`, option `08` to set the PGM output to trigger while the system alarm is tripped (works together with PC16-OUT mode). + ## Notes * For OTA updates on esp8266 and esp32, you may need to stop the interface using `dsc.stop();`: ``` @@ -330,13 +374,18 @@ Panel options affecting this interface, configured by `*8 + installer code` - se dsc.stop(); ... ``` -* Memory usage can be adjusted based on the number of partitions, zones, and data buffer size specified in [`src/dscKeybusInterface.h`](https://github.com/taligentx/dscKeybusInterface/blob/master/src/dscKeybusInterface.h). Default settings: +* Memory usage can be adjusted based on the number of partitions, zones, and data buffer size specified in [`src/dscKeybus.h`](https://github.com/taligentx/dscKeybusInterface/blob/master/src/dscKeybus.h) or [`src/dscClassic.h`](https://github.com/taligentx/dscKeybusInterface/blob/master/src/dscClassic.h). Default settings: * Arduino: up to 4 partitions, 32 zones, 10 buffered commands * esp8266/esp32: up to 8 partitions, 64 zones, 50 buffered commands * PCB layouts are available in [`extras/PCB Layouts`](https://github.com/taligentx/dscKeybusInterface/tree/master/extras/PCB%20Layouts) - thanks to [sjlouw](https://github.com/sj-louw) for contributing these designs! -* Support for other platforms depends on adjusting the code to use their platform-specific timers. In addition to hardware interrupts to capture the DSC clock, this library uses platform-specific timer interrupts to capture the DSC data line in a non-blocking way 250μs after the clock changes (without using `delayMicroseconds()`). This is necessary because the clock and data are asynchronous - I've observed keypad data delayed up to 160μs after the clock falls. +* Support for other platforms depends on adjusting the code to use their platform-specific timers. In addition to hardware pin-change interrupts to capture the DSC clock, this library uses platform-specific timer interrupts to capture the DSC data line in a non-blocking way 250μs after the clock changes (without using `delayMicroseconds()`). This is necessary because the clock and data are asynchronous - I've observed keypad data delayed up to 160μs after the clock falls. + +* Resource utilization: + * Arduino: 1 hardware interrupt digital pin, 2 digital pins (+1 for Classic series), Timer1 interrupt + * esp8266: 3 digital pins (+1 for Classic series), timer1 interrupt + * esp32/esp32-s2: 3 digital pins (+1 for Classic series), timer0 interrupt ## Troubleshooting If you are running into issues: @@ -346,6 +395,8 @@ If you are running into issues: * If keys are not displayed in the output, verify the transistor pinout, base resistor, and wiring connections. 3. Run the `Status` example sketch and view the serial output to verify that the interface displays events from the security system correctly as partitions are armed, zones opened, etc. +For general discussions, feature requests, or how-to issues, you can [post in Discussions](https://github.com/taligentx/dscKeybusInterface/discussions), or [post an Issue](https://github.com/taligentx/dscKeybusInterface/issues) if it looks like an issue with the library code itself. + ## References [AVR Freaks - DSC Keybus Protocol](https://www.avrfreaks.net/forum/dsc-keybus-protocol): An excellent discussion on how data is sent on the Keybus. diff --git a/examples/Arduino/HomeAssistant-MQTT/HomeAssistant-MQTT.ino b/examples/Arduino/HomeAssistant-MQTT/HomeAssistant-MQTT.ino index f34a4b7..b7085bf 100644 --- a/examples/Arduino/HomeAssistant-MQTT/HomeAssistant-MQTT.ino +++ b/examples/Arduino/HomeAssistant-MQTT/HomeAssistant-MQTT.ino @@ -1,5 +1,5 @@ /* - * HomeAssistant-MQTT 1.4 (Arduino with Ethernet) + * HomeAssistant-MQTT 1.5 (Arduino with Ethernet) * * Processes the security system status and allows for control using Home Assistant via MQTT. * @@ -156,6 +156,7 @@ entity: alarm_control_panel.security_partition_1 * Closed: "0" * * Release notes + * 1.5 - Added DSC Classic series support * 1.4 - Added PGM outputs 1-14 status * 1.2 - Added night arm (arming with no entry delay) * Added status update on initial MQTT connection and reconnection @@ -177,7 +178,14 @@ entity: alarm_control_panel.security_partition_1 * DSC Green ---- 15k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin (Arduino Uno: 2-12) + * +-- 15k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ * |-- NPN base --- 1k ohm resistor --- dscWritePin (Arduino Uno: 2-12) * Ground --- NPN emitter --/ @@ -193,6 +201,9 @@ entity: alarm_control_panel.security_partition_1 * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -200,7 +211,7 @@ entity: alarm_control_panel.security_partition_1 // Settings byte mac[] = { 0xAA, 0x61, 0x0A, 0x00, 0x00, 0x01 }; // Set a MAC address unique to the local network -const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm based on panel configuration. +const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm or enable command outputs based on panel configuration. const char* mqttServer = ""; // MQTT server domain name or IP address const int mqttPort = 1883; // MQTT server port const char* mqttUsername = ""; // Optional, leave blank if not required @@ -208,9 +219,9 @@ const char* mqttPassword = ""; // Optional, leave blank if not required // MQTT topics - match to Home Assistant's configuration.yaml const char* mqttClientName = "dscKeybusInterface"; -const char* mqttPartitionTopic = "dsc/Get/Partition"; // Sends armed and alarm status per partition: dsc/Get/Partition1 ... dsc/Get/Partition8 -const char* mqttZoneTopic = "dsc/Get/Zone"; // Sends zone status per zone: dsc/Get/Zone1 ... dsc/Get/Zone64 -const char* mqttFireTopic = "dsc/Get/Fire"; // Sends fire status per partition: dsc/Get/Fire1 ... dsc/Get/Fire8 +const char* mqttPartitionTopic = "dsc/Get/Partition"; // Sends armed and alarm status per partition: dsc/Get/Partition1 ... dsc/Get/Partition4 +const char* mqttZoneTopic = "dsc/Get/Zone"; // Sends zone status per zone: dsc/Get/Zone1 ... dsc/Get/Zone32 +const char* mqttFireTopic = "dsc/Get/Fire"; // Sends fire status per partition: dsc/Get/Fire1 ... dsc/Get/Fire4 const char* mqttPgmTopic = "dsc/Get/PGM"; // Sends PGM status per PGM: dsc/Get/PGM1 ... dsc/Get/PGM14 const char* mqttTroubleTopic = "dsc/Get/Trouble"; // Sends trouble status const char* mqttStatusTopic = "dsc/Status"; // Sends online/offline status @@ -221,11 +232,16 @@ const char* mqttSubscribeTopic = "dsc/Set"; // Receives messages to w // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. #define dscClockPin 3 // Arduino Uno hardware interrupt pin: 2,3 +#define dscPC16Pin 4 // DSC Classic Series only, Arduino Uno: 2-12 #define dscReadPin 5 // Arduino Uno: 2-12 #define dscWritePin 6 // Arduino Uno: 2-12 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif EthernetClient ipClient; PubSubClient mqtt(mqttServer, mqttPort, ipClient); unsigned long mqttPreviousTime; @@ -266,7 +282,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -279,7 +295,7 @@ void loop() { else mqtt.publish(mqttStatusTopic, mqttLwtMessage, true); } - // Sends the access code when needed by the panel for arming + // Sends the access code when needed by the panel for arming or command outputs if (dsc.accessCodePrompt) { dsc.accessCodePrompt = false; dsc.write(accessCode); diff --git a/examples/Arduino/Homebridge-MQTT/Homebridge-MQTT.ino b/examples/Arduino/Homebridge-MQTT/Homebridge-MQTT.ino index 3c2d69e..91c30eb 100644 --- a/examples/Arduino/Homebridge-MQTT/Homebridge-MQTT.ino +++ b/examples/Arduino/Homebridge-MQTT/Homebridge-MQTT.ino @@ -1,5 +1,5 @@ /* - * Homebridge-MQTT 1.4 (Arduino with Ethernet) + * Homebridge-MQTT 1.7 (Arduino with Ethernet) * * Processes the security system status and allows for control using Apple HomeKit, including the iOS Home app, * Siri, and Google Home. This uses MQTT to interface with Homebridge and the homebridge-mqttthing plugin for @@ -146,6 +146,9 @@ * Closed: "0" * * Release notes: + * 1.7 - Fixed exit delay states while multiple partitions are arming + * 1.6 - Added DSC Classic series support + * 1.5 - Support switching armed modes while armed * 1.4 - Added PGM outputs 1-14 status * Added notes on Google Home integration * 1.2 - Resolved handling HomeKit target states @@ -168,7 +171,14 @@ * DSC Green ---- 15k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin (Arduino Uno: 2-12) + * +-- 15k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ * |-- NPN base --- 1k ohm resistor --- dscWritePin (Arduino Uno: 2-12) * Ground --- NPN emitter --/ @@ -184,6 +194,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -191,7 +204,7 @@ // Settings byte mac[] = { 0xAA, 0x61, 0x0A, 0x00, 0x00, 0x01 }; // Set a MAC address unique to the local network -const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm based on panel configuration. +const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm or enable command outputs based on panel configuration. const char* mqttServer = ""; // MQTT server domain name or IP address const int mqttPort = 1883; // MQTT server port const char* mqttUsername = ""; // Optional, leave blank if not required @@ -199,24 +212,29 @@ const char* mqttPassword = ""; // Optional, leave blank if not required // MQTT topics - match to Homebridge's config.json const char* mqttClientName = "dscKeybusInterface"; -const char* mqttPartitionTopic = "dsc/Get/Partition"; // Sends armed and alarm status per partition: dsc/Get/Partition1 ... dsc/Get/Partition8 -const char* mqttZoneTopic = "dsc/Get/Zone"; // Sends zone status per zone: dsc/Get/Zone1 ... dsc/Get/Zone64 -const char* mqttFireTopic = "dsc/Get/Fire"; // Sends fire status per partition: dsc/Get/Fire1 ... dsc/Get/Fire8 +const char* mqttPartitionTopic = "dsc/Get/Partition"; // Sends armed and alarm status per partition: dsc/Get/Partition1 ... dsc/Get/Partition4 +const char* mqttZoneTopic = "dsc/Get/Zone"; // Sends zone status per zone: dsc/Get/Zone1 ... dsc/Get/Zone32 +const char* mqttFireTopic = "dsc/Get/Fire"; // Sends fire status per partition: dsc/Get/Fire1 ... dsc/Get/Fire4 const char* mqttPgmTopic = "dsc/Get/PGM"; // Sends PGM status per PGM: dsc/Get/PGM1 ... dsc/Get/PGM14 const char* mqttSubscribeTopic = "dsc/Set"; // Receives messages to write to the panel // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. #define dscClockPin 3 // Arduino Uno hardware interrupt pin: 2,3 +#define dscPC16Pin 4 // DSC Classic Series only, Arduino Uno: 2-12 #define dscReadPin 5 // Arduino Uno: 2-12 #define dscWritePin 6 // Arduino Uno: 2-12 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif EthernetClient ipClient; PubSubClient mqtt(mqttServer, mqttPort, ipClient); unsigned long mqttPreviousTime; -char exitState; +char exitState[4]; void setup() { @@ -254,13 +272,13 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; } - // Sends the access code when needed by the panel for arming + // Sends the access code when needed by the panel for arming or command outputs if (dsc.accessCodePrompt) { dsc.accessCodePrompt = false; dsc.write(accessCode); @@ -275,7 +293,7 @@ void loop() { // Publishes armed/disarmed status if (dsc.armedChanged[partition]) { if (dsc.armed[partition]) { - exitState = 0; + exitState[partition] = 0; // Night armed away if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) { @@ -310,21 +328,21 @@ void loop() { if (dsc.exitDelay[partition]) { // Sets the arming target state if the panel is armed externally - if (exitState == 0 || dsc.exitStateChanged[partition]) { + if (exitState[partition] == 0 || dsc.exitStateChanged[partition]) { dsc.exitStateChanged[partition] = 0; switch (dsc.exitState[partition]) { case DSC_EXIT_STAY: { - exitState = 'S'; + exitState[partition] = 'S'; publishState(mqttPartitionTopic, partition, "S", 0); break; } case DSC_EXIT_AWAY: { - exitState = 'A'; + exitState[partition] = 'A'; publishState(mqttPartitionTopic, partition, "A", 0); break; } case DSC_EXIT_NO_ENTRY_DELAY: { - exitState = 'N'; + exitState[partition] = 'N'; publishState(mqttPartitionTopic, partition, "N", 0); break; } @@ -334,7 +352,7 @@ void loop() { // Disarmed during exit delay else if (!dsc.armed[partition]) { - exitState = 0; + exitState[partition] = 0; publishState(mqttPartitionTopic, partition, "D", "D"); } } @@ -439,7 +457,52 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { payloadIndex = 1; } - // Resets the HomeKit target state if attempting to change the armed mode while armed or not ready + // Sets night arm (no entry delay) while armed + if (payload[payloadIndex] == 'N' && dsc.armed[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write('n'); // Keypad no entry delay + publishState(mqttPartitionTopic, partition, "N", 0); + exitState[partition] = 'N'; + return; + } + + // Disables night arm while armed stay + if (payload[payloadIndex] == 'S' && dsc.armedStay[partition] && dsc.noEntryDelay[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write('n'); // Keypad no entry delay + publishState(mqttPartitionTopic, partition, "S", 0); + exitState[partition] = 'S'; + return; + } + + // Disables night arm while armed away + if (payload[payloadIndex] == 'A' && dsc.armedAway[partition] && dsc.noEntryDelay[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write('n'); // Keypad no entry delay + publishState(mqttPartitionTopic, partition, "A", 0); + exitState[partition] = 'A'; + return; + } + + // Changes from arm away to arm stay after the exit delay + if (payload[payloadIndex] == 'S' && dsc.armedAway[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write("s"); + publishState(mqttPartitionTopic, partition, "S", 0); + exitState[partition] = 'S'; + return; + } + + // Changes from arm stay to arm away after the exit delay + if (payload[payloadIndex] == 'A' && dsc.armedStay[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write("w"); + publishState(mqttPartitionTopic, partition, "A", 0); + exitState[partition] = 'A'; + return; + } + + // Resets the HomeKit target state if attempting to change the armed mode while not ready if (payload[payloadIndex] != 'D' && !dsc.ready[partition]) { dsc.armedChanged[partition] = true; dsc.statusChanged = true; @@ -447,10 +510,10 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { } // Resets the HomeKit target state if attempting to change the arming mode during the exit delay - if (payload[payloadIndex] != 'D' && dsc.exitDelay[partition] && exitState != 0) { - if (exitState == 'S') publishState(mqttPartitionTopic, partition, "S", 0); - else if (exitState == 'A') publishState(mqttPartitionTopic, partition, "A", 0); - else if (exitState == 'N') publishState(mqttPartitionTopic, partition, "N", 0); + if (payload[payloadIndex] != 'D' && dsc.exitDelay[partition] && exitState[partition] != 0) { + if (exitState[partition] == 'S') publishState(mqttPartitionTopic, partition, "S", 0); + else if (exitState[partition] == 'A') publishState(mqttPartitionTopic, partition, "A", 0); + else if (exitState[partition] == 'N') publishState(mqttPartitionTopic, partition, "N", 0); } @@ -459,7 +522,7 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { dsc.writePartition = partition + 1; // Sets writes to the partition number dsc.write('s'); // Keypad stay arm publishState(mqttPartitionTopic, partition, "S", 0); - exitState = 'S'; + exitState[partition] = 'S'; return; } @@ -468,7 +531,7 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { dsc.writePartition = partition + 1; // Sets writes to the partition number dsc.write('w'); // Keypad away arm publishState(mqttPartitionTopic, partition, "A", 0); - exitState = 'A'; + exitState[partition] = 'A'; return; } @@ -477,7 +540,7 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { dsc.writePartition = partition + 1; // Sets writes to the partition number dsc.write('n'); // Keypad arm with no entry delay publishState(mqttPartitionTopic, partition, "N", 0); - exitState = 'N'; + exitState[partition] = 'N'; return; } diff --git a/examples/Arduino/KeybusReader/KeybusReader.ino b/examples/Arduino/KeybusReader/KeybusReader.ino index 3a4ccc2..99c48f1 100644 --- a/examples/Arduino/KeybusReader/KeybusReader.ino +++ b/examples/Arduino/KeybusReader/KeybusReader.ino @@ -1,11 +1,12 @@ /* - * DSC Keybus Reader 1.2 (Arduino) + * DSC Keybus Reader 1.3 (Arduino) * * Decodes and prints data from the Keybus to a serial interface, including reading from serial for the virtual * keypad. This is primarily to help decode the Keybus protocol - see the Status example to put the interface * to productive use. * * Release notes: + * 1.3 - Added DSC Classic series support * 1.2 - Handle spurious data while keybus is disconnected * Removed redundant data processing * 1.0 - Initial release @@ -23,7 +24,14 @@ * DSC Green ---- 15k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin (Arduino Uno: 2-12) + * +-- 15k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ * |-- NPN base --- 1k ohm resistor --- dscWritePin (Arduino Uno: 2-12) * Ground --- NPN emitter --/ @@ -39,16 +47,24 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. #define dscClockPin 3 // Arduino Uno hardware interrupt pin: 2,3 +#define dscPC16Pin 4 // DSC Classic Series only, Arduino Uno: 2-12 #define dscReadPin 5 // Arduino Uno: 2-12 #define dscWritePin 6 // Arduino Uno: 2-12 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin); +#endif void setup() { @@ -88,7 +104,7 @@ void loop() { } // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; diff --git a/examples/Arduino/KeypadInterface-MQTT/KeypadInterface-MQTT.ino b/examples/Arduino/KeypadInterface-MQTT/KeypadInterface-MQTT.ino new file mode 100644 index 0000000..922d8dd --- /dev/null +++ b/examples/Arduino/KeypadInterface-MQTT/KeypadInterface-MQTT.ino @@ -0,0 +1,364 @@ +/* + * DSC Keypad Interface-MQTT 1.2 (Arduino with Ethernet) + * + * Emulates a DSC panel to directly interface DSC PowerSeries or Classic series keypads as physical + * input devices for any general purpose, without needing a DSC panel. This sketch uses MQTT to + * send pressed keypad keys and receive commands to control keypad lights and tones. + * + * PowerSeries keypad features: + * - Read keypad key button presses, including fire/aux/panic alarm keys: dsc.key + * - Set keypad lights: Ready, Armed, Trouble, Memory, Bypass, Fire, Program, Backlight, Zones 1-8: dsc.lightReady, dsc.lightZone1, etc + * - Set keypad beeps, 1-128: dsc.beep(3) + * - Set keypad buzzer in seconds, 1-255: dsc.tone(5) + * - Set keypad tone pattern with a number of beeps, an optional constant tone, and the interval in seconds between beeps: + * 2 beeps, no constant tone, 4 second interval: dsc.tone(2, false, 4) + * 3 beeps, constant tone, 2 second interval: dsc.tone(3, true, 2) + * Disable the tone: dsc.tone() or dsc.tone(0, false, 0) + * + * Classic keypad features: + * - Read keypad key button presses, including fire/aux/panic alarm keys: dsc.key + * - Set keypad lights: Ready, Armed, Trouble, Memory, Bypass, Fire, Program, Zones 1-8: dsc.lightReady, dsc.lightZone1, etc + * + * This interface uses a different wiring setup from the standard Keybus interface, adding + * an NPN transistor on dscClockPin. The DSC keypads require a 12v DC power source, though + * lower voltages down to 7v may work for key presses (the LEDs will be dim). + * + * Release notes: + * 1.3 - Add Classic keypad support - PC2550RK + * 1.2 - Add Classic keypad support - PC1500RK + * 1.1 - Add keypad beep, buzzer, constant tone + * 1.0 - Initial release + * + * Wiring: + * DSC Keypad R --- 12v DC + * + * DSC Keypad B --- Arduino ground + * + * DSC Keypad Y ---+--- 1k ohm resistor --- 12v DC + * | + * +--- NPN collector --\ + * |-- NPN base --- 1k ohm resistor --- dscClockPin // Arduino Uno: 3 + * Ground --- NPN emitter --/ + * + * DSC Keypad G ---+--- 1k ohm resistor --- 12v DC + * | + * +--- 15k ohm resistor ---+--- dscReadPin // Arduino Uno: 5 + * | | + * | +--- 10k ohm resistor --- Ground + * | + * +--- NPN collector --\ + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Arduino Uno: 6 + * Ground --- NPN emitter --/ + * + * The keypad interface uses NPN transistors to pull the clock and data lines low - most small + * signal NPN transistors should be suitable, for example: + * - 2N3904 + * - BC547, BC548, BC549 + * + * Issues and (especially) pull requests are welcome: + * https://github.com/taligentx/dscKeybusInterface + * + * This example code is in the public domain. + */ + +// Set the keypad type +#define dscKeypad +//#define dscClassicKeypad + +#include +#include +#include +#include + +// Settings +byte mac[] = { 0xAA, 0x61, 0x0A, 0x00, 0x00, 0x01 }; // Set a MAC address unique to the local network +const char* mqttServer = ""; // MQTT server domain name or IP address +const int mqttPort = 1883; // MQTT server port +const char* mqttUsername = ""; // Optional, leave blank if not required +const char* mqttPassword = ""; // Optional, leave blank if not required + +// MQTT topics +const char* mqttClientName = "dscKeypadInterface"; +const char* mqttKeyTopic = "dsc/Key"; // Sends keypad keys +const char* mqttSubscribeTopic = "dsc/Set"; // Receives messages to send to the keypad + +// Configures the Keybus interface with the specified pins +#define dscClockPin 3 // Arduino Uno hardware interrupt pin: 2,3 +#define dscReadPin 5 // Arduino Uno: 2-12 +#define dscWritePin 6 // Arduino Uno: 2-12 + +// Initialize components +#ifdef dscKeypad +dscKeypadInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicKeypadInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#endif +bool lightOff, lightBlink, inputReceived; +const byte inputLimit = 50; +char input[inputLimit]; +byte beepLength, buzzerLength, toneLength; +EthernetClient ipClient; +PubSubClient mqtt(mqttServer, mqttPort, ipClient); +unsigned long mqttPreviousTime; + + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println(); + Serial.println(); + + // Initializes ethernet with DHCP + Serial.print(F("Ethernet....")); + while(!Ethernet.begin(mac)) { + Serial.println(F("DHCP failed. Retrying...")); + delay(5000); + } + Serial.print(F("connected: ")); + Serial.println(Ethernet.localIP()); + + mqtt.setCallback(mqttCallback); + if (mqttConnect()) mqttPreviousTime = millis(); + else mqttPreviousTime = 0; + + Serial.print(F("Keybus....")); + dsc.begin(); + Serial.println(F("connected.")); + Serial.println(F("DSC Keypad Interface is online.")); +} + +void loop() { + mqttHandle(); + + /* + * Sets keypad status via serial with the listed keys. Light status uses custom + * values for control: off, on, blink (example: dsc.lightReady = blink;) + * + * Light on: Send the keys listed below. Turning on the armed light: "a" + * Light off: Send "-" before a light key to turn it off. Turning off the zone 4 light: "-4" + * Light blink: Send "!" before a light key to blink. Blinking the ready light: "!r" + * Beep: Send "b" followed by the number of beeps, 1-128. Setting 2 beeps: "b2" + * Buzzer: Send "z" followed by the buzzer length in seconds, 1-255. Setting the buzzer to 5 seconds: "z5" + * Tone pattern: Send "n" followed by the number of beeps 1-7, constant tone true "t" or false "f", interval between beeps 1-15s + * Setting a tone pattern with 2 beeps, no constant tone, 4 second interval: "n2f4" + * Setting a tone pattern with 3 beeps, constant tone, 2 second interval: "n3t2" + * Disabling the tone pattern: "n" + */ + if (inputReceived) { + inputReceived = false; + + for (byte i = 0; i < strlen(input); i++) { + switch (input[i]) { + case 'r': case 'R': dsc.lightReady = setLight(); break; + case 'a': case 'A': dsc.lightArmed = setLight(); break; + case 'm': case 'M': dsc.lightMemory = setLight(); break; + case 'y': case 'Y': dsc.lightBypass = setLight(); break; + case 't': case 'T': dsc.lightTrouble = setLight(); break; + case 'p': case 'P': dsc.lightProgram = setLight(); break; + case 'f': case 'F': dsc.lightFire = setLight(); break; + case 'l': case 'L': dsc.lightBacklight = setLight(); break; + case '1': dsc.lightZone1 = setLight(); break; + case '2': dsc.lightZone2 = setLight(); break; + case '3': dsc.lightZone3 = setLight(); break; + case '4': dsc.lightZone4 = setLight(); break; + case '5': dsc.lightZone5 = setLight(); break; + case '6': dsc.lightZone6 = setLight(); break; + case '7': dsc.lightZone7 = setLight(); break; + case '8': dsc.lightZone8 = setLight(); break; + case 'b': case 'B': sendBeeps(i); i += beepLength; break; + case 'n': case 'N': sendTone(i); i+= toneLength; break; + case 'z': case 'Z': sendBuzzer(i); i+= buzzerLength; break; + case '-': lightOff = true; break; + case '!': lightBlink = true; break; + default: break; + } + } + } + + dsc.loop(); + + // Checks for a keypad key press + if (dsc.keyAvailable) { + dsc.keyAvailable = false; + switch (dsc.key) { + case 0x00: mqtt.publish(mqttKeyTopic, "0", false); break; + case 0x05: mqtt.publish(mqttKeyTopic, "1", false); break; + case 0x0A: mqtt.publish(mqttKeyTopic, "2", false); break; + case 0x0F: mqtt.publish(mqttKeyTopic, "3", false); break; + case 0x11: mqtt.publish(mqttKeyTopic, "4", false); break; + case 0x16: mqtt.publish(mqttKeyTopic, "5", false); break; + case 0x1B: mqtt.publish(mqttKeyTopic, "6", false); break; + case 0x1C: mqtt.publish(mqttKeyTopic, "7", false); break; + case 0x22: mqtt.publish(mqttKeyTopic, "8", false); break; + case 0x27: mqtt.publish(mqttKeyTopic, "9", false); break; + case 0x28: mqtt.publish(mqttKeyTopic, "*", false); break; + case 0x2D: mqtt.publish(mqttKeyTopic, "#", false); break; + case 0x82: mqtt.publish(mqttKeyTopic, "Enter", false); break; + case 0xAF: mqtt.publish(mqttKeyTopic, "Arm: Stay", false); break; + case 0xB1: mqtt.publish(mqttKeyTopic, "Arm: Away", false); break; + case 0xBB: mqtt.publish(mqttKeyTopic, "Door chime", false); break; + case 0xDA: mqtt.publish(mqttKeyTopic, "Reset", false); break; + case 0xE1: mqtt.publish(mqttKeyTopic, "Quick exit", false); break; + case 0xF7: mqtt.publish(mqttKeyTopic, "Menu navigation", false); break; + case 0x0B: mqtt.publish(mqttKeyTopic, "Fire alarm", false); break; + case 0x0D: mqtt.publish(mqttKeyTopic, "Aux alarm", false); break; + case 0x0E: mqtt.publish(mqttKeyTopic, "Panic alarm", false); break; + } + mqtt.subscribe(mqttSubscribeTopic); + } +} + + +// Parse the number of beeps from the input +void sendBeeps(byte position) { + char inputNumber[4]; + byte beeps = 0; + beepLength = 0; + + for (byte i = position + 1; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + inputNumber[beepLength] = input[i]; + beepLength++; + if (beepLength >= 3) break; + } + else break; + } + + inputNumber[beepLength] = '\0'; + beeps = atoi(inputNumber); + if (beeps > 128) beeps = 128; + + dsc.beep(beeps); +} + + +// Parse the buzzer length in seconds from the input +void sendBuzzer(byte position) { + char inputNumber[4]; + byte buzzerSeconds = 0; + buzzerLength = 0; + + for (byte i = position + 1; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + inputNumber[buzzerLength] = input[i]; + buzzerLength++; + if (buzzerLength >= 3) break; + } + else break; + } + + inputNumber[buzzerLength] = '\0'; + buzzerSeconds = atoi(inputNumber); + dsc.buzzer(buzzerSeconds); +} + + +// Parse the tone pattern number of beeps, constant tone state, and interval in seconds from the input +void sendTone(byte position) { + byte beeps = 0, interval = 0, intervalLength = 0; + char beepNumber[2]; + bool toneState; + char intervalNumber[3]; + toneLength = 0; + + if (strlen(input) < 4) { + dsc.tone(0, false, 0); + return; + } + + // Parse beeps 0-7 + if (input[position + 1] >= '0' && input[position + 1] <= '9') { + beepNumber[0] = input[position + 1]; + beeps = atoi(beepNumber); + if (beeps > 7) beeps = 7; + toneLength++; + } + else return; + + // Parse constant tone value + switch (input[position + 2]) { + case 't': + case 'T': toneState = true; toneLength++; break; + case 'f': + case 'F': toneState = false; toneLength++; break; + default: toneLength--; return; + } + + // Parse interval + for (byte i = position + 3; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + intervalNumber[intervalLength] = input[i]; + intervalLength++; + toneLength++; + if (intervalLength >= 2) break; + } + else break; + } + intervalNumber[intervalLength] = '\0'; + interval = atoi(intervalNumber); + if (interval > 15) interval = 15; + + dsc.tone(beeps, toneState, interval); +} + + +// Sets keypad lights state - lights use custom values for control: off, on, blink (example: dsc.lightReady = blink;) +Light setLight() { + if (lightOff) { + lightOff = false; + return off; + } + else if (lightBlink) { + lightBlink = false; + return blink; + } + else return on; +} + + +// Handles messages received in the mqttSubscribeTopic +void mqttCallback(char* topic, byte* payload, unsigned int length) { + + // Handles unused parameters + (void)topic; + + for (unsigned int i = 0; i < length; i++) { + input[i] = payload[i]; + } + + input[length] = '\0'; + if (input[0] == '\0') inputReceived = false; + else inputReceived = true; +} + + +void mqttHandle() { + if (!mqtt.connected()) { + unsigned long mqttCurrentTime = millis(); + if (mqttCurrentTime - mqttPreviousTime > 5000) { + mqttPreviousTime = mqttCurrentTime; + if (mqttConnect()) { + Serial.println(F("MQTT disconnected, successfully reconnected.")); + mqttPreviousTime = 0; + mqtt.subscribe(mqttSubscribeTopic); + } + else Serial.println(F("MQTT disconnected, failed to reconnect.")); + } + } + else mqtt.loop(); +} + + +bool mqttConnect() { + Serial.print(F("MQTT....")); + if (mqtt.connect(mqttClientName, mqttUsername, mqttPassword)) { + Serial.print(F("connected: ")); + Serial.println(mqttServer); + mqtt.subscribe(mqttSubscribeTopic); + } + else { + Serial.print(F("connection error: ")); + Serial.println(mqttServer); + } + return mqtt.connected(); +} diff --git a/examples/Arduino/KeypadInterface/KeypadInterface.ino b/examples/Arduino/KeypadInterface/KeypadInterface.ino new file mode 100644 index 0000000..b7f2657 --- /dev/null +++ b/examples/Arduino/KeypadInterface/KeypadInterface.ino @@ -0,0 +1,342 @@ +/* + * DSC Keypad Interface 1.4 (Arduino) + * + * Emulates a DSC panel to directly interface DSC PowerSeries or Classic series keypads as physical + * input devices for any general purpose, without needing a DSC panel. + * + * PowerSeries keypad features: + * - Read keypad key button presses, including fire/aux/panic alarm keys: dsc.key + * - Set keypad lights: Ready, Armed, Trouble, Memory, Bypass, Fire, Program, Backlight, Zones 1-8: dsc.lightReady, dsc.lightZone1, etc + * - Set keypad beeps, 1-128: dsc.beep(3) + * - Set keypad buzzer in seconds, 1-255: dsc.tone(5) + * - Set keypad tone pattern with a number of beeps, an optional constant tone, and the interval in seconds between beeps: + * 2 beeps, no constant tone, 4 second interval: dsc.tone(2, false, 4) + * 3 beeps, constant tone, 2 second interval: dsc.tone(3, true, 2) + * Disable the tone: dsc.tone() or dsc.tone(0, false, 0) + * - Set LCD keypad messages (on cmd 0x05/byte3) with entering HEX input into serial console: + * According to printPanelMessages in dscKeybusPrintData.cpp, through it doesn't seem to fully match + * Change Function keys 1-5 with entering: 0x70 - 0x74 + * Change LCD keypad time by entering: 0x2A (slight delay before LCD will show to input time data) + * Change LCD Brightness/contrast/buzzer level by entering: 0x29 and scrolling to desired setting then pressing (*) + * + * Classic keypad features: + * - Read keypad key button presses, including fire/aux/panic alarm keys: dsc.key + * - Set keypad lights: Ready, Armed, Trouble, Memory, Bypass, Fire, Program, Zones 1-8: dsc.lightReady, dsc.lightZone1, etc + * + * This interface uses a different wiring setup from the standard Keybus interface, adding + * an NPN transistor on dscClockPin. The DSC keypads require a 12v DC power source, though + * lower voltages down to 7v may work for key presses (the LEDs will be dim). + * + * Release notes: + * 1.4 - Added ability to change LCD keypad messages + * 1.3 - Add Classic keypad support - PC2550RK + * 1.2 - Add Classic keypad support - PC1500RK + * 1.1 - Add keypad beep, buzzer, constant tone + * 1.0 - Initial release + * + * Wiring: + * DSC Keypad R --- 12v DC + * + * DSC Keypad B --- Arduino ground + * + * DSC Keypad Y ---+--- 1k ohm resistor --- 12v DC + * | + * +--- NPN collector --\ + * |-- NPN base --- 1k ohm resistor --- dscClockPin // Arduino Uno: 3 + * Ground --- NPN emitter --/ + * + * DSC Keypad G ---+--- 1k ohm resistor --- 12v DC + * | + * +--- 15k ohm resistor ---+--- dscReadPin // Arduino Uno: 5 + * | | + * | +--- 10k ohm resistor --- Ground + * | + * +--- NPN collector --\ + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Arduino Uno: 6 + * Ground --- NPN emitter --/ + * + * The keypad interface uses NPN transistors to pull the clock and data lines low - most small + * signal NPN transistors should be suitable, for example: + * - 2N3904 + * - BC547, BC548, BC549 + * + * Issues and (especially) pull requests are welcome: + * https://github.com/taligentx/dscKeybusInterface + * + * This example code is in the public domain. + */ + +// Set the keypad type +#define dscKeypad +//#define dscClassicKeypad + +#include + +// Configures the Keybus interface with the specified pins +#define dscClockPin 3 // Arduino Uno hardware interrupt pin: 2,3 +#define dscReadPin 5 // Arduino Uno: 2-12 +#define dscWritePin 6 // Arduino Uno: 2-12 + +// Initialize components +#ifdef dscKeypad +dscKeypadInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicKeypadInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#endif +bool lightOff, lightBlink, inputReceived; +const byte inputLimit = 50; +char input[inputLimit]; +byte beepLength, buzzerLength, toneLength; + + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println(); + Serial.println(); + + Serial.print(F("Keybus....")); + dsc.begin(); + Serial.println(F("connected.")); + Serial.println(F("DSC Keypad Interface is online.")); +} + +void loop() { + + inputSerial(); // Stores Serial data in input[], requires a newline character (NL, CR, or both) + + /* + * Sets keypad status via serial with the listed keys. Light status uses custom + * values for control: off, on, blink (example: dsc.lightReady = blink;) + * + * Light on: Send the keys listed below. Turning on the armed light: "a" + * Light off: Send "-" before a light key to turn it off. Turning off the zone 4 light: "-4" + * Light blink: Send "!" before a light key to blink. Blinking the ready light: "!r" + * Beep: Send "b" followed by the number of beeps, 1-128. Setting 2 beeps: "b2" + * Buzzer: Send "z" followed by the buzzer length in seconds, 1-255. Setting the buzzer to 5 seconds: "z5" + * Tone pattern: Send "n" followed by the number of beeps 1-7, constant tone true "t" or false "f", interval between beeps 1-15s + * Setting a tone pattern with 2 beeps, no constant tone, 4 second interval: "n2f4" + * Setting a tone pattern with 3 beeps, constant tone, 2 second interval: "n3t2" + * Disabling the tone pattern: "n" + */ + if (inputReceived) { + inputReceived = false; + + #if defined(dscKeypad) + if (String(input).startsWith("0x")) dsc.panelCommand05[2] = strtoul(input, NULL, 16); + else { + for (byte i = 0; i < strlen(input); i++) { + switch (input[i]) { + case 'r': case 'R': dsc.lightReady = setLight(); break; + case 'a': case 'A': dsc.lightArmed = setLight(); break; + case 'm': case 'M': dsc.lightMemory = setLight(); break; + case 'y': case 'Y': dsc.lightBypass = setLight(); break; + case 't': case 'T': dsc.lightTrouble = setLight(); break; + case 'p': case 'P': dsc.lightProgram = setLight(); break; + case 'f': case 'F': dsc.lightFire = setLight(); break; + case 'l': case 'L': dsc.lightBacklight = setLight(); break; + case '1': dsc.lightZone1 = setLight(); break; + case '2': dsc.lightZone2 = setLight(); break; + case '3': dsc.lightZone3 = setLight(); break; + case '4': dsc.lightZone4 = setLight(); break; + case '5': dsc.lightZone5 = setLight(); break; + case '6': dsc.lightZone6 = setLight(); break; + case '7': dsc.lightZone7 = setLight(); break; + case '8': dsc.lightZone8 = setLight(); break; + case 'b': case 'B': sendBeeps(i); i += beepLength; break; + case 'n': case 'N': sendTone(i); i+= toneLength; break; + case 'z': case 'Z': sendBuzzer(i); i+= buzzerLength; break; + case '-': lightOff = true; break; + case '!': lightBlink = true; break; + default: break; + } + } + } + #else + for (byte i = 0; i < strlen(input); i++) { + switch (input[i]) { + case 'r': case 'R': dsc.lightReady = setLight(); break; + case 'a': case 'A': dsc.lightArmed = setLight(); break; + case 'm': case 'M': dsc.lightMemory = setLight(); break; + case 'y': case 'Y': dsc.lightBypass = setLight(); break; + case 't': case 'T': dsc.lightTrouble = setLight(); break; + case 'p': case 'P': dsc.lightProgram = setLight(); break; + case 'f': case 'F': dsc.lightFire = setLight(); break; + case 'l': case 'L': dsc.lightBacklight = setLight(); break; + case '1': dsc.lightZone1 = setLight(); break; + case '2': dsc.lightZone2 = setLight(); break; + case '3': dsc.lightZone3 = setLight(); break; + case '4': dsc.lightZone4 = setLight(); break; + case '5': dsc.lightZone5 = setLight(); break; + case '6': dsc.lightZone6 = setLight(); break; + case '7': dsc.lightZone7 = setLight(); break; + case '8': dsc.lightZone8 = setLight(); break; + case 'b': case 'B': sendBeeps(i); i += beepLength; break; + case 'n': case 'N': sendTone(i); i+= toneLength; break; + case 'z': case 'Z': sendBuzzer(i); i+= buzzerLength; break; + case '-': lightOff = true; break; + case '!': lightBlink = true; break; + default: break; + } + } + #endif + } + + dsc.loop(); + + // Checks for a keypad key press + if (dsc.keyAvailable) { + dsc.keyAvailable = false; + switch (dsc.key) { + case 0x00: Serial.println("0"); break; + case 0x05: Serial.println("1"); break; + case 0x0A: Serial.println("2"); break; + case 0x0F: Serial.println("3"); break; + case 0x11: Serial.println("4"); break; + case 0x16: Serial.println("5"); break; + case 0x1B: Serial.println("6"); break; + case 0x1C: Serial.println("7"); break; + case 0x22: Serial.println("8"); break; + case 0x27: Serial.println("9"); break; + case 0x28: Serial.println("*"); break; + case 0x2D: Serial.println("#"); break; + case 0x82: Serial.println(F("Enter")); break; + case 0xAF: Serial.println(F("Arm: Stay")); break; + case 0xB1: Serial.println(F("Arm: Away")); break; + case 0xBB: Serial.println(F("Door chime")); break; + case 0xDA: Serial.println(F("Reset")); break; + case 0xE1: Serial.println(F("Quick exit")); break; + case 0xF7: Serial.println(F("Menu navigation")); break; + case 0x0B: Serial.println(F("Fire alarm")); break; + case 0x0D: Serial.println(F("Aux alarm")); break; + case 0x0E: Serial.println(F("Panic alarm")); break; + default: break; + } + } +} + + +// Parse the number of beeps from the input +void sendBeeps(byte position) { + char inputNumber[4]; + byte beeps = 0; + beepLength = 0; + + for (byte i = position + 1; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + inputNumber[beepLength] = input[i]; + beepLength++; + if (beepLength >= 3) break; + } + else break; + } + + inputNumber[beepLength] = '\0'; + beeps = atoi(inputNumber); + if (beeps > 128) beeps = 128; + + dsc.beep(beeps); +} + + +// Parse the buzzer length in seconds from the input +void sendBuzzer(byte position) { + char inputNumber[4]; + byte buzzerSeconds = 0; + buzzerLength = 0; + + for (byte i = position + 1; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + inputNumber[buzzerLength] = input[i]; + buzzerLength++; + if (buzzerLength >= 3) break; + } + else break; + } + + inputNumber[buzzerLength] = '\0'; + buzzerSeconds = atoi(inputNumber); + dsc.buzzer(buzzerSeconds); +} + + +// Parse the tone pattern number of beeps, constant tone state, and interval in seconds from the input +void sendTone(byte position) { + byte beeps = 0, interval = 0, intervalLength = 0; + char beepNumber[2]; + bool toneState; + char intervalNumber[3]; + toneLength = 0; + + if (strlen(input) < 4) { + dsc.tone(0, false, 0); + return; + } + + // Parse beeps 0-7 + if (input[position + 1] >= '0' && input[position + 1] <= '9') { + beepNumber[0] = input[position + 1]; + beeps = atoi(beepNumber); + if (beeps > 7) beeps = 7; + toneLength++; + } + else return; + + // Parse constant tone value + switch (input[position + 2]) { + case 't': + case 'T': toneState = true; toneLength++; break; + case 'f': + case 'F': toneState = false; toneLength++; break; + default: toneLength--; return; + } + + // Parse interval + for (byte i = position + 3; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + intervalNumber[intervalLength] = input[i]; + intervalLength++; + toneLength++; + if (intervalLength >= 2) break; + } + else break; + } + intervalNumber[intervalLength] = '\0'; + interval = atoi(intervalNumber); + if (interval > 15) interval = 15; + + dsc.tone(beeps, toneState, interval); +} + + +// Sets keypad lights state - lights use custom values for control: off, on, blink (example: dsc.lightReady = blink;) +Light setLight() { + if (lightOff) { + lightOff = false; + return off; + } + else if (lightBlink) { + lightBlink = false; + return blink; + } + else return on; +} + + +// Stores Serial data in input[], requires a newline character (NL, CR, or both) +void inputSerial() { + static byte inputCount = 0; + if (!inputReceived) { + while (Serial.available() > 0 && inputCount < inputLimit) { + input[inputCount] = Serial.read(); + if (input[inputCount] == '\n' || input[inputCount] == '\r') { + input[inputCount] = '\0'; + inputCount = 0; + inputReceived = true; + break; + } + else inputCount++; + } + if (input[0] == '\0') inputReceived = false; + } +} diff --git a/examples/Arduino/OpenHAB-MQTT/OpenHAB-MQTT.ino b/examples/Arduino/OpenHAB-MQTT/OpenHAB-MQTT.ino index dbf79b9..c691245 100644 --- a/examples/Arduino/OpenHAB-MQTT/OpenHAB-MQTT.ino +++ b/examples/Arduino/OpenHAB-MQTT/OpenHAB-MQTT.ino @@ -1,5 +1,5 @@ /* - * OpenHAB-MQTT 1.2 (Arduino with Ethernet) + * OpenHAB-MQTT 1.3 (Arduino with Ethernet) * * Processes the security system status and allows for control using OpenHAB. This uses MQTT to * interface with OpenHAB and the MQTT binding and demonstrates using panel and partition states @@ -81,6 +81,7 @@ Contact zone3 "Zone 3" {channel="mqtt:topic:mymqtt:dsc:zone3"} * Fire alarm restored: "0" * * Release notes: + * 1.3 - Added DSC Classic series support * 1.2 - Added PGM outputs 1-14 status * Removed partition exit delay MQTT message, not used in this OpenHAB example * 1.0 - Initial release @@ -98,9 +99,16 @@ Contact zone3 "Zone 3" {channel="mqtt:topic:mymqtt:dsc:zone3"} * DSC Green ---- 15k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin (Arduino Uno: 2-12) + * +-- 15k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) + * |-- NPN base --- 1k ohm resistor --- dscWritePin (Arduino Uno: 2-12) * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -114,6 +122,9 @@ Contact zone3 "Zone 3" {channel="mqtt:topic:mymqtt:dsc:zone3"} * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -121,7 +132,7 @@ Contact zone3 "Zone 3" {channel="mqtt:topic:mymqtt:dsc:zone3"} // Settings byte mac[] = { 0xAA, 0x61, 0x0A, 0x00, 0x00, 0x01 }; // Set a MAC address unique to the local network -const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm based on panel configuration. +const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm or enable command outputs based on panel configuration. const char* mqttServer = ""; // MQTT server domain name or IP address const int mqttPort = 1883; // MQTT server port const char* mqttUsername = ""; // Optional, leave blank if not required @@ -129,9 +140,9 @@ const char* mqttPassword = ""; // Optional, leave blank if not required // MQTT topics - match to the OpenHAB "things" configuration file const char* mqttClientName = "dscKeybusInterface"; -const char* mqttPartitionTopic = "dsc/Get/Partition"; // Sends armed and alarm status per partition: dsc/Get/Partition1 ... dsc/Get/Partition8 -const char* mqttZoneTopic = "dsc/Get/Zone"; // Sends zone status per zone: dsc/Get/Zone1 ... dsc/Get/Zone64 -const char* mqttFireTopic = "dsc/Get/Fire"; // Sends fire status per partition: dsc/Get/Fire1 ... dsc/Get/Fire8 +const char* mqttPartitionTopic = "dsc/Get/Partition"; // Sends armed and alarm status per partition: dsc/Get/Partition1 ... dsc/Get/Partition4 +const char* mqttZoneTopic = "dsc/Get/Zone"; // Sends zone status per zone: dsc/Get/Zone1 ... dsc/Get/Zone32 +const char* mqttFireTopic = "dsc/Get/Fire"; // Sends fire status per partition: dsc/Get/Fire1 ... dsc/Get/Fire4 const char* mqttPgmTopic = "dsc/Get/PGM"; // Sends PGM status per PGM: dsc/Get/PGM1 ... dsc/Get/PGM14 const char* mqttTroubleTopic = "dsc/Get/Trouble"; // Sends trouble status const char* mqttStatusTopic = "dsc/Status"; // Sends online/offline status @@ -142,11 +153,16 @@ const char* mqttSubscribeTopic = "dsc/Set"; // Receives messages to w // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. #define dscClockPin 3 // Arduino Uno hardware interrupt pin: 2,3 +#define dscPC16Pin 4 // DSC Classic Series only, Arduino Uno: 2-12 #define dscReadPin 5 // Arduino Uno: 2-12 #define dscWritePin 6 // Arduino Uno: 2-12 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif EthernetClient ipClient; PubSubClient mqtt(mqttServer, mqttPort, ipClient); unsigned long mqttPreviousTime; @@ -187,7 +203,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -200,7 +216,7 @@ void loop() { else mqtt.publish(mqttStatusTopic, mqttLwtMessage, true); } - // Sends the access code when needed by the panel for arming + // Sends the access code when needed by the panel for arming or command outputs if (dsc.accessCodePrompt) { dsc.accessCodePrompt = false; dsc.write(accessCode); diff --git a/examples/Arduino/Status/Status.ino b/examples/Arduino/Status/Status.ino index f71d143..4be4c37 100644 --- a/examples/Arduino/Status/Status.ino +++ b/examples/Arduino/Status/Status.ino @@ -1,11 +1,12 @@ /* - * DSC Status 1.3 (Arduino) + * DSC Status 1.4 (Arduino) * * Processes and prints the security system status to a serial interface, including reading from serial for the * virtual keypad. This demonstrates how to determine if the security system status has changed, what has * changed, and how to take action based on those changes. * * Release notes: + * 1.4 - Added DSC Classic series support * 1.3 - Added PGM outputs 1-14 status * 1.1 - Added partition ready, access code, and timestamp status * 1.0 - Initial release @@ -23,7 +24,14 @@ * DSC Green ---- 15k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin (Arduino Uno: 2-12) + * +-- 15k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ * |-- NPN base --- 1k ohm resistor --- dscWritePin (Arduino Uno: 2-12) * Ground --- NPN emitter --/ @@ -39,16 +47,24 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. #define dscClockPin 3 // Arduino Uno hardware interrupt pin: 2,3 +#define dscPC16Pin 4 // DSC Classic Series only, Arduino Uno: 2-12 #define dscReadPin 5 // Arduino Uno: 2-12 #define dscWritePin 6 // Arduino Uno: 2-12 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin); +#endif void setup() { @@ -75,7 +91,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -97,7 +113,7 @@ void loop() { if (dsc.disabled[partition]) { Serial.print(F("Partition ")); Serial.print(partition + 1); - Serial.println(F(" disabled")); + Serial.println(F(": Disabled")); } } if (dsc.disabled[partition]) continue; diff --git a/examples/Arduino/TimeSyncNTP/TimeSyncNTP.ino b/examples/Arduino/TimeSyncNTP/TimeSyncNTP.ino index e46039c..411adbf 100644 --- a/examples/Arduino/TimeSyncNTP/TimeSyncNTP.ino +++ b/examples/Arduino/TimeSyncNTP/TimeSyncNTP.ino @@ -25,7 +25,7 @@ * DSC Green ---- 15k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad: + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ * |-- NPN base --- 1k ohm resistor --- dscWritePin (Arduino Uno: 2-12) * Ground --- NPN emitter --/ @@ -60,12 +60,13 @@ byte mac[] = { 0xAA, 0x61, 0x0A, 0x00, 0x00, 0x01 }; // Set a MAC address uniqu // Configures the Keybus interface with the specified pins #define dscClockPin 3 // Arduino Uno hardware interrupt pin: 2,3 +#define dscPC16Pin 4 // DSC Classic Series only, Arduino Uno: 2-12 #define dscReadPin 5 // Arduino Uno: 2-12 #define dscWritePin 6 // Arduino Uno: 2-12 // Initialize components -EthernetUDP ipClient; dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +EthernetUDP ipClient; byte ntpBuffer[48]; unsigned int localPort = 8888; bool ntpSynced = true; @@ -79,7 +80,7 @@ void setup() { Serial.println(); // Initializes ethernet with DHCP - Serial.print(F("Ethernet...")); + Serial.print(F("Ethernet....")); while(!Ethernet.begin(mac)) { Serial.print("."); delay(1000); @@ -88,7 +89,7 @@ void setup() { Serial.println(Ethernet.localIP()); ipClient.begin(localPort); - Serial.print(F("NTP time...")); + Serial.print(F("NTP time....")); setSyncProvider(getDstCorrectedTime); // Initiates the NTP client, synced hourly setSyncInterval(3600); while (timeStatus() != timeSet) { @@ -146,7 +147,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; diff --git a/examples/Arduino/TinyGSM-SMS/TinyGSM-SMS.ino b/examples/Arduino/TinyGSM-SMS/TinyGSM-SMS.ino new file mode 100644 index 0000000..d412801 --- /dev/null +++ b/examples/Arduino/TinyGSM-SMS/TinyGSM-SMS.ino @@ -0,0 +1,297 @@ +/* + * TinyGSM SMS Notification 1.0 (Arduino) + * + * Processes the security system status and demonstrates how to send an SMS text message when the status has + * changed. This example sends SMS text messages via a TinyGSM-compatible SIM800L module which is connected + * onto Arduino board. Make sure that Micro-SIM card have PIN Code turned off (disable SIM Lock from phone). + * + * Usage: + * 1. Install the TinyGSM library, available in the Arduino IDE Library Manager and the Platform.io Library + * Registry: https://github.com/vshymanskyy/TinyGSM + * 2. Set the destination phone numbers in the sketch settings. + * + * Release notes: + * 1.0 - Just converted from ESP32 to Arduino. Tested with SIM800L module. + * + * Wiring: + * DSC Aux(+) --- Arduino Vin pin + * + * DSC Aux(-) --- Arduino Ground + * + * +--- dscClockPin (Arduino Uno: 2,3) + * DSC Yellow --- 15k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * +--- dscReadPin (Arduino Uno: 2-12) + * DSC Green ---- 15k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin (Arduino Uno: 2-12) + * +-- 15k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * + * Connecting SIM800L module with Arduino Uno: + * SIM800 RX -+-- 5.6k ohm resistor -- SIM800TxPin (Arduino Uno: 2-12) + * | + * | + * +-- 10k ohm resistor -- Arduino Uno Ground + * + * SIM800 TX ---- SIM800RxPin (Arduino Uno: 2-12) + * + * SIM800 GND --- Arduino Uno Ground + * + * SIM800 Vcc --- (+) 3.7 - 4.4V power supply which can supply burst current of 2A + * (you can use LM2596 Buck converter set to output 4.2V and connected to Bell+ and Aux-) + * + * Virtual keypad (optional): + * DSC Green ---- NPN collector --\ + * |-- NPN base --- 1k ohm resistor --- dscWritePin (Arduino Uno: 2-12) + * Ground --- NPN emitter --/ + * + * + * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should + * be suitable, for example: + * -- 2N3904 + * -- BC547, BC548, BC549 + * + * Issues and (especially) pull requests are welcome: + * https://github.com/taligentx/dscKeybusInterface + * + * Based on TinyGSM-SMS example for ESP32 by jvitkauskas: https://github.com/jvitkauskas + * + * This example code is in the public domain. + */ + +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + +// Configures GSM modem model. Must be done before including TinyGsmClient library. +#define TINY_GSM_MODEM_SIM800 +#include +#include +#include + +// Settings +const char* sendToPhoneNumbers[] = { + "+1234567890", + "+2345678901" +}; + +#define phone_number_count (sizeof (sendToPhoneNumbers) / sizeof (const char *)) + +// Configures the Keybus interface with the specified pins. +#define dscClockPin 3 // Arduino Uno hardware interrupt pin: 2,3 +#define dscPC16Pin 4 // DSC Classic Series only, Arduino Uno: 2-12 +#define dscReadPin 5 // Arduino Uno: 2-12 +#define SIM800RxPin 9 // Arduino Uno: 2-12 +#define SIM800TxPin 10 // Arduino Uno: 2-12 + +// Settings +// NOTE: I kept getting "Keybus buffer overflow" when sketch was sending multiple messages at once (example: AC power trouble and trouble status ON) +bool notifyOnPartitionAlarm = true; +bool notifyOnPowerTroubles = false; +bool notifyOnKeypadAlarm = false; +bool notifyOnDisArming = false; +bool notifyOnTrouble = true; + +// Initialize components +SoftwareSerial serialSIM800(SIM800RxPin,SIM800TxPin); +#ifndef dscClassicSeries +dscKeybusInterface dsc(dscClockPin, dscReadPin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin); +#endif +TinyGsm modem(serialSIM800); + +void setup() { + Serial.begin(9600); + delay(1000); + Serial.println(); + Serial.println(); + + serialSIM800.begin(9600); + + while (!modem.isNetworkConnected()) { + Serial.print(F("GSM...")); + while (!modem.restart()) { + Serial.print("."); + } + Serial.println(); + + Serial.print(F("Waiting for network...")); + if (modem.waitForNetwork(600000L) && modem.isNetworkConnected()) { + Serial.println(F("connected.")); + } + else { + Serial.println(F("connection error.")); + } + } + + // Starts the Keybus interface + dsc.begin(); + Serial.println(F("DSC Keybus Interface is online.")); +} + + +void loop() { + modem.maintain(); + + dsc.loop(); + + if (dsc.statusChanged) { // Checks if the security system status has changed + dsc.statusChanged = false; // Reset the status tracking flag + + // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call + // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + if (dsc.bufferOverflow) { + Serial.println(F("Keybus buffer overflow")); + dsc.bufferOverflow = false; + } + + // Checks status per partition + for (byte partition = 0; partition < dscPartitions; partition++) { + + // Skips processing if the partition is disabled or in installer programming + if (dsc.disabled[partition]) continue; + + // Checks alarm triggered status + if (notifyOnPartitionAlarm) { + if (dsc.alarmChanged[partition]) { + dsc.alarmChanged[partition] = false; // Resets the partition alarm status flag + + if (dsc.alarm[partition]) { + char messageContent[19] = "Alarm: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else { + char messageContent[34] = "Disarmed after alarm: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + + if (dsc.fireChanged[partition]) { + dsc.fireChanged[partition] = false; // Resets the fire status flag + + if (dsc.fire[partition]) { + char messageContent[24] = "Fire alarm: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else { + char messageContent[33] = "Fire alarm restored: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + } + + // Publishes armed/disarmed status + if (notifyOnDisArming) { + if (dsc.armedChanged[partition]) { + if (dsc.armed[partition]) { + // Night armed away + if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) { + char messageContent[33] = "Armed away - night: Partition: "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + + // Armed away + else if (dsc.armedAway[partition]) { + char messageContent[25] = "Armed away: Partition: "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + + // Night armed stay + else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) { + char messageContent[33] = "Armed stay - night: Partition: "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + + // Armed stay + else if (dsc.armedStay[partition]) { + char messageContent[25] = "Armed stay: Partition: "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + + // Disarmed + else { + char messageContent[23] = "Disarmed: Partition: "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + dsc.armedChanged[partition] = false; // Resets the partition armed status flag + } + } + } + + // Checks trouble status + if (notifyOnTrouble) { + if (dsc.troubleChanged) { + dsc.troubleChanged = false; // Resets the trouble status flag + if (dsc.trouble) sendMessage("Trouble status on"); + else sendMessage("Trouble status restored"); + } + } + // Checks for AC power status + if (notifyOnPowerTroubles) { + if (dsc.powerChanged) { + dsc.powerChanged = false; // Resets the battery trouble status flag + if (dsc.powerTrouble) sendMessage("AC power trouble"); + else sendMessage("AC power restored"); + } + } + // Checks panel battery status + if (dsc.batteryChanged) { + dsc.batteryChanged = false; // Resets the battery trouble status flag + if (dsc.batteryTrouble) sendMessage("Panel battery trouble"); + else sendMessage("Panel battery restored"); + } + + if (notifyOnKeypadAlarm) { + // Checks for keypad fire alarm status + if (dsc.keypadFireAlarm) { + dsc.keypadFireAlarm = false; // Resets the keypad fire alarm status flag + sendMessage("Keypad Fire alarm"); + } + + // Checks for keypad aux auxiliary alarm status + if (dsc.keypadAuxAlarm) { + dsc.keypadAuxAlarm = false; // Resets the keypad auxiliary alarm status flag + sendMessage("Keypad Aux alarm"); + } + + // Checks for keypad panic alarm status + if (dsc.keypadPanicAlarm) { + dsc.keypadPanicAlarm = false; // Resets the keypad panic alarm status flag + sendMessage("Keypad Panic alarm"); + } + } + } +} + +bool sendMessage(const char* messageContent) { + bool result = true; + + for (int i = 0; i < phone_number_count; i++) { + result &= modem.sendSMS(sendToPhoneNumbers[i], messageContent); + } + + return result; +} + +void appendPartition(byte sourceNumber, char* pushMessage) { + char partitionNumber[2]; + itoa(sourceNumber + 1, partitionNumber, 10); + strcat(pushMessage, partitionNumber); +} diff --git a/examples/Arduino/Unlocker/Unlocker.ino b/examples/Arduino/Unlocker/Unlocker.ino index 846804e..d705420 100644 --- a/examples/Arduino/Unlocker/Unlocker.ino +++ b/examples/Arduino/Unlocker/Unlocker.ino @@ -61,7 +61,7 @@ * DSC Green ---- 15k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad: + * Virtual keypad: * DSC Green ---- NPN collector --\ * |-- NPN base --- 1k ohm resistor --- dscWritePin (Arduino Uno: 2-12) * Ground --- NPN emitter --/ @@ -128,7 +128,7 @@ void setup() { // Starts the Keybus interface and optionally specifies how to print data. // begin() sets Serial by default and can accept a different stream: begin(Serial1), etc. dsc.begin(); - Serial.print(F("DSC Keybus Interface...")); + Serial.print(F("DSC Keybus Interface....")); // Loops until partition 1 is ready for key presses in status "Partition ready" (0x01), // "Stay/away zones open" (0x02), or "Zones open" (0x03) diff --git a/examples/esp32/Email/Email.ino b/examples/esp32/Email/Email.ino index d9c83a5..da44824 100644 --- a/examples/esp32/Email/Email.ino +++ b/examples/esp32/Email/Email.ino @@ -1,5 +1,5 @@ /* - * Email Notification 1.0 (esp32) + * Email Notification 1.1 (esp32) * * Processes the security system status and demonstrates how to send an email when the status has changed. Configure * the email SMTP server settings in sendEmail(). @@ -9,6 +9,7 @@ * apps: https://support.google.com/accounts/answer/6010255 * * Release notes: + * 1.1 - Added DSC Classic series support * 1.0 - Initial release * * Wiring: @@ -16,23 +17,20 @@ * * DSC Aux(-) --- esp32 Ground * - * +--- dscClockPin (esp32: 4,13,16-39) + * +--- dscClockPin // Default: 18 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp32: 4,13,16-39) + * +--- dscReadPin // Default: 19 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): - * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp32: 4,13,16-33) - * Ground --- NPN emitter --/ - * - * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should - * be suitable, for example: - * -- 2N3904 - * -- BC547, BC548, BC549 + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground * * Issues and (especially) pull requests are welcome: * https://github.com/taligentx/dscKeybusInterface @@ -40,6 +38,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include @@ -49,11 +50,16 @@ const char* wifiPassword = ""; const char* messagePrefix = "[Security system] "; // Set a prefix for all messages // Configures the Keybus interface with the specified pins. -#define dscClockPin 18 // esp32: 4,13,16-39 -#define dscReadPin 19 // esp32: 4,13,16-39 +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin); +#endif WiFiClientSecure ipClient; bool wifiConnected = true; @@ -64,7 +70,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi...")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -106,7 +112,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -125,12 +131,12 @@ void loop() { if (dsc.alarm[partition]) { char messageContent[19] = "Alarm: Partition "; appendPartition(partition, messageContent); // Appends the message with the partition number - sendMessage(messageContent, messageContent); + sendMessage(messageContent); } else { char messageContent[34] = "Disarmed after alarm: Partition "; appendPartition(partition, messageContent); // Appends the message with the partition number - sendMessage(messageContent, messageContent); + sendMessage(messageContent); } } @@ -140,12 +146,12 @@ void loop() { if (dsc.fire[partition]) { char messageContent[24] = "Fire alarm: Partition "; appendPartition(partition, messageContent); // Appends the message with the partition number - sendMessage(messageContent, messageContent); + sendMessage(messageContent); } else { char messageContent[33] = "Fire alarm restored: Partition "; appendPartition(partition, messageContent); // Appends the message with the partition number - sendMessage(messageContent, messageContent); + sendMessage(messageContent); } } } @@ -195,6 +201,7 @@ void loop() { // server - the login and password must be base64 encoded. For example, on the macOS/Linux terminal: // $ echo -n 'mylogin@example.com' | base64 -w 0 bool sendMessage(const char* messageContent) { + ipClient.setHandshakeTimeout(30); // Workaround for https://github.com/espressif/arduino-esp32/issues/6165 if (!ipClient.connect("smtp.example.com", 465)) return false; // Set the SMTP server address - for example: smtp.gmail.com if(!smtpValidResponse()) return false; ipClient.println(F("HELO ESP32")); @@ -237,6 +244,7 @@ bool smtpValidResponse() { while (!ipClient.available()) { dsc.loop(); // Processes Keybus data while waiting on the SMTP response if (millis() - previousMillis > 3000) { + Serial.println(); Serial.println(F("Connection timed out waiting for a response.")); ipClient.stop(); return false; @@ -255,6 +263,7 @@ bool smtpValidResponse() { // Unsuccessful, prints the response to serial to help debug else { + Serial.println(); Serial.println(F("Email send error, response:")); Serial.print(replyCode); while (ipClient.available()) Serial.print((char)ipClient.read()); diff --git a/examples/esp32/HomeAssistant-MQTT/HomeAssistant-MQTT.ino b/examples/esp32/HomeAssistant-MQTT/HomeAssistant-MQTT.ino index 169c05c..38a91e0 100644 --- a/examples/esp32/HomeAssistant-MQTT/HomeAssistant-MQTT.ino +++ b/examples/esp32/HomeAssistant-MQTT/HomeAssistant-MQTT.ino @@ -1,5 +1,5 @@ /* - * HomeAssistant-MQTT 1.4 (esp32) + * HomeAssistant-MQTT 1.5 (esp32) * * Processes the security system status and allows for control using Home Assistant via MQTT. * @@ -160,6 +160,7 @@ entity: alarm_control_panel.security_partition_1 * Closed: "0" * * Release notes: + * 1.5 - Added DSC Classic series support * 1.4 - Added PGM outputs 1-14 status * 1.0 - Initial release * @@ -168,17 +169,24 @@ entity: alarm_control_panel.security_partition_1 * * DSC Aux(-) --- esp32 Ground * - * +--- dscClockPin (esp32: 4,13,16-39) + * +--- dscClockPin // Default: 18 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp32: 4,13,16-39) + * +--- dscReadPin // Default: 19 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp32: 4,13,16-33) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: 21 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -192,6 +200,9 @@ entity: alarm_control_panel.security_partition_1 * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -199,7 +210,7 @@ entity: alarm_control_panel.security_partition_1 // Settings const char* wifiSSID = ""; const char* wifiPassword = ""; -const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm based on panel configuration. +const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm or enable command outputs based on panel configuration. const char* mqttServer = ""; // MQTT server domain name or IP address const int mqttPort = 1883; // MQTT server port const char* mqttUsername = ""; // Optional, leave blank if not required @@ -220,12 +231,17 @@ const char* mqttSubscribeTopic = "dsc/Set"; // Receives messages to w // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. -#define dscClockPin 18 // esp32: 4,13,16-39 -#define dscReadPin 19 // esp32: 4,13,16-39 -#define dscWritePin 21 // esp32: 4,13,16-33 +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 +#define dscWritePin 21 // 4,13,16-33 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif WiFiClient ipClient; PubSubClient mqtt(mqttServer, mqttPort, ipClient); unsigned long mqttPreviousTime; @@ -237,7 +253,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi...")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -267,7 +283,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -280,7 +296,7 @@ void loop() { else mqtt.publish(mqttStatusTopic, mqttLwtMessage, true); } - // Sends the access code when needed by the panel for arming + // Sends the access code when needed by the panel for arming or command outputs if (dsc.accessCodePrompt) { dsc.accessCodePrompt = false; dsc.write(accessCode); @@ -520,7 +536,7 @@ void publishMessage(const char* sourceTopic, byte partition) { case 0x03: mqtt.publish(publishTopic, "Zones open", true); break; case 0x04: mqtt.publish(publishTopic, "Armed: Stay", true); break; case 0x05: mqtt.publish(publishTopic, "Armed: Away", true); break; - case 0x06: mqtt.publish(publishTopic, "Armed: No entry delay", true); break; + case 0x06: mqtt.publish(publishTopic, "Armed: Stay with no entry delay", true); break; case 0x07: mqtt.publish(publishTopic, "Failed to arm", true); break; case 0x08: mqtt.publish(publishTopic, "Exit delay in progress", true); break; case 0x09: mqtt.publish(publishTopic, "Arming with no entry delay", true); break; @@ -533,7 +549,7 @@ void publishMessage(const char* sourceTopic, byte partition) { case 0x12: mqtt.publish(publishTopic, "Battery check in progress"); break; case 0x14: mqtt.publish(publishTopic, "Auto-arm in progress", true); break; case 0x15: mqtt.publish(publishTopic, "Arming with bypassed zones", true); break; - case 0x16: mqtt.publish(publishTopic, "Armed: No entry delay", true); break; + case 0x16: mqtt.publish(publishTopic, "Armed: Away with no entry delay", true); break; case 0x17: mqtt.publish(publishTopic, "Power saving: Keypad blanked", true); break; case 0x19: mqtt.publish(publishTopic, "Disarmed: Alarm memory"); break; case 0x22: mqtt.publish(publishTopic, "Disarmed: Recent closing", true); break; diff --git a/examples/esp32/HomeKit-HomeSpan/HomeKit-HomeSpan.ino b/examples/esp32/HomeKit-HomeSpan/HomeKit-HomeSpan.ino new file mode 100644 index 0000000..2a645bc --- /dev/null +++ b/examples/esp32/HomeKit-HomeSpan/HomeKit-HomeSpan.ino @@ -0,0 +1,217 @@ +/* + * HomeKit-HomeSpan 1.0 (esp32) + * + * Processes the security system status and allows for control using Apple HomeKit, including the Home app + * and Siri. This example uses HomeSpan to interface the esp32 directly with HomeKit without requiring + * a separate service or device. + * + * HomeSpan: https://github.com/HomeSpan/HomeSpan + * + * This sketch demonstrates using partition armed and alarm states as HomeKit Security System accessories, + * zone states as Contact Sensor accessories, fire alarm states as Smoke Sensor accessories, PGM output states + * as Contact Sensor accessories, and command outputs as Switch accessories. + * + * Usage: + * 1. Set the security system access code in the sketch settings to permit disarming and command outputs + * through HomeKit. + * 2. Define the security system components to add to HomeKit as accessories in the setup() section of + * the sketch, using the example accessories as a template. Each accessory requires its own + * SpanAccessory() definition (one per partition, zone, etc). + * 3. Upload the sketch. + * 4. Open the esp32 serial interface to configure WiFi using the HomeSpan interface as + * per https://github.com/HomeSpan/HomeSpan/blob/master/docs/CLI.md + * 5. Open the iOS Home app and add the new bridge accessory, which will include all accessories + * configured in setup(). By default, HomeSpan uses pairing code: 466-37-726 + * + * Release notes: + * 1.0 - Initial release + * + * Wiring: + * DSC Aux(+) --- 5v voltage regulator --- esp32 development board 5v pin + * + * DSC Aux(-) --- esp32 Ground + * + * +--- dscClockPin // Default: 18 + * DSC Yellow --- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * +--- dscReadPin // Default: 19 + * DSC Green ---- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * + * Virtual keypad (optional): + * DSC Green ---- NPN collector --\ + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: 21 + * Ground --- NPN emitter --/ + * + * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should + * be suitable, for example: + * -- 2N3904 + * -- BC547, BC548, BC549 + * + * Issues and (especially) pull requests are welcome: + * https://github.com/taligentx/dscKeybusInterface + * + * This example code is in the public domain. + */ + +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + +#include "HomeSpan.h" +#include + +// Settings +const char* accessCode = "1234"; // An access code is required to disarm/night arm and may be required to arm or enable command outputs based on panel configuration. + +// Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the +// virtual keypad. +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 +#define dscWritePin 21 // 4,13,16-33 + +// Initialize components +#ifndef dscClassicSeries +dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif +bool updatePartitions, updateZones, updatePGMs, updateSmokeSensors; + +#include "dscHomeSpanAccessories.h" // Processes security system components as HomeKit accessories + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println(); + Serial.println(); + + homeSpan.begin(Category::Bridges,"DSC Security System"); // Customizable name for the HomeKit accessory bridge + + // Accessory identification + new SpanAccessory(); + new homeSpanIdentify("DSC Security System","DSC","000000","PC1864","3.0"); // Customizable name, manufacturer, serial number, model, firmware revision + new Service::HAPProtocolInformation(); + new Characteristic::Version("1.1.0"); // HomeKit requires specifying HAP protocol version 1.1.0 + + /* + * HomeKit accessories - define partitions, zones, fire alarms, PGM outputs, and command outputs each as a + * separate SpanAccessory() using the definitions below as a template + */ + + // Partition 1: Security System accessory + new SpanAccessory(); + new homeSpanIdentify("Partition 1","DSC","000000","Alarm","3.0"); + new dscPartition(1); // Set the partition number + + // Partition 8: Security System accessory + new SpanAccessory(); + new homeSpanIdentify("Partition 8","DSC","000000","Alarm","3.0"); + new dscPartition(8); // Set the partition number + + // Zone 1: Contact Sensor accessory + new SpanAccessory(); + new homeSpanIdentify("Zone 1","DSC","000000","Sensor","3.0"); // Set the zone name + new dscZone(1); // Set the zone number + + // Zone 64: Contact Sensor accessory + new SpanAccessory(); + new homeSpanIdentify("Zone 64","DSC","000000","Sensor","3.0"); // Set the zone name + new dscZone(64); // Set the zone number + + // Fire alarm partition 1: Smoke Sensor accessory + new SpanAccessory(); + new homeSpanIdentify("Fire 1","DSC","000000","Sensor","3.0"); // Set the fire sensor name + new dscFire(1); // Set the partition number + + // Fire alarm partition 8: Smoke Sensor accessory + new SpanAccessory(); + new homeSpanIdentify("Fire 8","DSC","000000","Sensor","3.0"); // Set the fire sensor name + new dscFire(8); // Set the partition number + + // PGM output 1: Contact Sensor accessory + new SpanAccessory(); + new homeSpanIdentify("PGM 1","DSC","000000","Sensor","3.0"); // Set the PGM output name + new dscPGM(1); // Set the PGM output number + + // PGM output 14: Contact Sensor accessory + new SpanAccessory(); + new homeSpanIdentify("PGM 14","DSC","000000","Sensor","3.0"); // Set the PGM output name + new dscPGM(14); // Set the PGM output number + + // Command output 1: Switch accessory - this allows HomeKit to activate the PGM outputs assigned to the command output + new SpanAccessory(); + new homeSpanIdentify("Command 1","DSC","000000","Sensor","3.0"); // Set the command output name + new dscCommand(1, 1, 1); // Set the command output number (1-4), one of the PGM outputs (1-14) assigned to the command output, and the assigned partition: dscCommand(cmd, pgm, partition); + + // Command output 4: Switch accessory - this allows HomeKit to activate the PGM outputs assigned to the command output + new SpanAccessory(); + new homeSpanIdentify("Command 4","DSC","000000","Sensor","3.0"); // Set the command output name + new dscCommand(4, 4, 1); // Set the command output number (1-4), one of the PGM outputs (1-14) assigned to the command output, and the assigned partition: dscCommand(cmd, pgm, partition); + + // Starts the Keybus interface and optionally specifies how to print data. + // begin() sets Serial by default and can accept a different stream: begin(Serial1), etc. + dsc.begin(); +} + + +void loop() { + homeSpan.poll(); + + dsc.loop(); + + if (dsc.statusChanged) { // Checks if the security system status has changed + dsc.statusChanged = false; // Reset the status tracking flag + + // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call + // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + if (dsc.bufferOverflow) { + Serial.println(F("Keybus buffer overflow")); + dsc.bufferOverflow = false; + } + + // Sends the access code when needed by the panel for arming or command outputs + if (dsc.accessCodePrompt) { + dsc.accessCodePrompt = false; + dsc.write(accessCode); + } + + // Checks status per partition + for (byte partition = 0; partition < dscPartitions; partition++) { + + // Skips processing if the partition is disabled or in installer programming + if (dsc.disabled[partition]) continue; + + // Checks partition status and sets a flag to update security system partition accessories + if (dsc.armedChanged[partition] || dsc.exitDelayChanged[partition] || dsc.alarmChanged[partition]) { + updatePartitions = true; + } + + // Checks fire alarm status and sets a flag to update smoke sensor accessories + if (dsc.fireChanged[partition]) { + updateSmokeSensors = true; + } + } + + // Checks zone status and sets a flag to update zone accessories + if (dsc.openZonesStatusChanged) { + dsc.openZonesStatusChanged = false; // Resets the open zones status flag + updateZones = true; // Updates zone accessories + } + + // Checks PGM outputs status and sets a flag to update PGM accessories + if (dsc.pgmOutputsStatusChanged) { + dsc.pgmOutputsStatusChanged = false; // Resets the PGM outputs status flag + updatePGMs = true; // Updates PGM output and command output accessories + } + } +} diff --git a/examples/esp32/HomeKit-HomeSpan/dscHomeSpanAccessories.h b/examples/esp32/HomeKit-HomeSpan/dscHomeSpanAccessories.h new file mode 100644 index 0000000..69e1306 --- /dev/null +++ b/examples/esp32/HomeKit-HomeSpan/dscHomeSpanAccessories.h @@ -0,0 +1,449 @@ +/* + * HomeKit-HomeSpan 1.0 (esp32) + * + * This defines security system components as HomeKit accessories, processes security system status + * changes to update HomeKit, and handles HomeKit requests to change the security system state. + * + * All accessories are configured in HomeKit-HomeSpan.ino. + * + * Issues and (especially) pull requests are welcome: + * https://github.com/taligentx/dscKeybusInterface + * + * This example code is in the public domain. + */ + +// HomeKit security system states are defined as integers per HomeKit Accessory Protocol Specification R2 +#define HOMEKIT_STAY 0 +#define HOMEKIT_AWAY 1 +#define HOMEKIT_NIGHT 2 +#define HOMEKIT_DISARM 3 +#define HOMEKIT_ALARM 4 + +// Tracks which partitions, zones, and PGMs are configured in the sketch - only the configured accessories will be processed for status +bool configuredPartitions[8]; +byte configuredZones[8], configuredPGMs[2], configuredCommandPGMs[2], pendingPGMs[2]; + + +// Partitions are defined as separate Security System accessories +struct dscPartition : Service::SecuritySystem { + byte partition; + char exitState; + SpanCharacteristic *partitionCurrentState, *partitionTargetState; + dscPartition(byte setPartition) : Service::SecuritySystem() { + partition = setPartition - 1; + configuredPartitions[partition] = true; + partitionCurrentState = new Characteristic::SecuritySystemCurrentState(HOMEKIT_DISARM); // Sets initial state to disarmed + partitionTargetState = new Characteristic::SecuritySystemTargetState(HOMEKIT_DISARM); // Sets initial state to disarmed + } + + + // Handles requests received from HomeKit + boolean update() { + byte targetState = partitionTargetState->getNewVal(); + + // Sets night arm (no entry delay) while armed + if (targetState == HOMEKIT_NIGHT && dsc.armed[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write('n'); // Keypad no entry delay + exitState = 'N'; + return(true); + } + + // Disables night arm while armed stay + if (targetState == HOMEKIT_STAY && dsc.armedStay[partition] && dsc.noEntryDelay[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write('n'); // Keypad no entry delay + exitState = 'S'; + return(true); + } + + // Disables night arm while armed away + if (targetState == HOMEKIT_AWAY && dsc.armedAway[partition] && dsc.noEntryDelay[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write('n'); // Keypad no entry delay + exitState = 'A'; + return(true); + } + + // Changes from arm away to arm stay after the exit delay + if (targetState == HOMEKIT_STAY && dsc.armedAway[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write("s"); + exitState = 'S'; + return(true); + } + + // Changes from arm stay to arm away after the exit delay + if (targetState == HOMEKIT_AWAY && dsc.armedStay[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write("w"); + exitState = 'A'; + return(true); + } + + // Resets the HomeKit target state if attempting to change the armed mode while not ready + if (targetState != HOMEKIT_DISARM && !dsc.ready[partition]) { + dsc.armedChanged[partition] = true; + dsc.statusChanged = true; + return(true); + } + + // Resets the HomeKit target state if attempting to change the arming mode during the exit delay + if (targetState != HOMEKIT_DISARM && dsc.exitDelay[partition] && exitState != 0) { + if (exitState == 'S') partitionTargetState->setVal(HOMEKIT_STAY); + else if (exitState == 'A') partitionTargetState->setVal(HOMEKIT_AWAY); + else if (exitState == 'N') partitionTargetState->setVal(HOMEKIT_NIGHT); + } + + // Stay arm + if (targetState == HOMEKIT_STAY && !dsc.armed[partition] && !dsc.exitDelay[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write('s'); // Keypad stay arm + exitState = 'S'; + return(true); + } + + // Away arm + if (targetState == HOMEKIT_AWAY && !dsc.armed[partition] && !dsc.exitDelay[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write('w'); // Keypad away arm + exitState = 'A'; + return(true); + } + + // Night arm + if (targetState == HOMEKIT_NIGHT && !dsc.armed[partition] && !dsc.exitDelay[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write('n'); // Keypad arm with no entry delay + exitState = 'N'; + return(true); + } + + // Disarm + if (targetState == HOMEKIT_DISARM && (dsc.armed[partition] || dsc.exitDelay[partition] || dsc.alarm[partition])) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write(accessCode); + return(true); + } + + return(true); + } + + + // Checks for partition status changes to send to HomeKit + void loop() { + if (updatePartitions) { + updatePartitions = false; + + if (dsc.armedChanged[partition]) { + if (dsc.armed[partition]) { + exitState = 0; + if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) { // Night armed away + partitionTargetState->setVal(HOMEKIT_NIGHT); + partitionCurrentState->setVal(HOMEKIT_NIGHT); + } + else if (dsc.armedAway[partition]) { // Away armed + partitionTargetState->setVal(HOMEKIT_AWAY); + partitionCurrentState->setVal(HOMEKIT_AWAY); + } + else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) { // Night armed stay + partitionTargetState->setVal(HOMEKIT_NIGHT); + partitionCurrentState->setVal(HOMEKIT_NIGHT); + } + else if (dsc.armedStay[partition]) { // Stay armed + partitionTargetState->setVal(HOMEKIT_STAY); + partitionCurrentState->setVal(HOMEKIT_STAY); + } + } + else { // Disarmed + partitionTargetState->setVal(HOMEKIT_DISARM); + partitionCurrentState->setVal(HOMEKIT_DISARM); + } + } + + // Updates exit delay status + if (dsc.exitDelayChanged[partition]) { + dsc.exitDelayChanged[partition] = false; // Resets the exit delay status flag + + if (dsc.exitDelay[partition]) { + + // Sets the arming target state if the panel is armed externally + if (exitState == 0 || dsc.exitStateChanged[partition]) { + dsc.exitStateChanged[partition] = 0; + switch (dsc.exitState[partition]) { + case DSC_EXIT_STAY: { + exitState = 'S'; + partitionTargetState->setVal(HOMEKIT_STAY); + break; + } + case DSC_EXIT_AWAY: { + exitState = 'A'; + partitionTargetState->setVal(HOMEKIT_AWAY); + break; + } + case DSC_EXIT_NO_ENTRY_DELAY: { + exitState = 'N'; + partitionTargetState->setVal(HOMEKIT_NIGHT); + break; + } + } + } + } + + // Disarmed during exit delay + else if (!dsc.armed[partition]) { + exitState = 0; + partitionTargetState->setVal(HOMEKIT_DISARM); + partitionCurrentState->setVal(HOMEKIT_DISARM); + } + } + + // Publishes alarm triggered status + if (dsc.alarmChanged[partition]) { + dsc.alarmChanged[partition] = false; // Resets the partition alarm status flag + if (dsc.alarm[partition]) { + partitionCurrentState->setVal(HOMEKIT_ALARM); // Alarm triggered + } + else if (!dsc.armedChanged[partition]) { + partitionTargetState->setVal(HOMEKIT_DISARM); + partitionCurrentState->setVal(HOMEKIT_DISARM); + } + } + + if (dsc.armedChanged[partition]) dsc.armedChanged[partition] = false; // Resets the partition armed status flag + + // Checks for changed status in additional partitions + for (byte checkPartition = 0; checkPartition < dscPartitions; checkPartition++) { + + // Skips processing if the partition is disabled, in installer programming, or not configured in the sketch + if (dsc.disabled[checkPartition] || !configuredPartitions[checkPartition]) continue; + + // Checks for changed status in a partition + if (dsc.armedChanged[checkPartition] || dsc.exitDelayChanged[checkPartition] || dsc.alarmChanged[checkPartition]) { + updatePartitions = true; + } + } + } + } +}; + + +// Zones are defined as Contact Sensor accessories +struct dscZone : Service::ContactSensor { + byte zoneGroup, zoneBit; + SpanCharacteristic *zoneState; + dscZone(byte zone) : Service::ContactSensor() { + zoneGroup = (zone - 1) / 8; + zoneBit = (zone - 1) - (zoneGroup * 8); + bitWrite(configuredZones[zoneGroup], zoneBit, 1); // Sets a zone as being configured + zoneState = new Characteristic::ContactSensorState(0); + } + + // Checks for zone status changes to send to HomeKit + void loop() { + if (updateZones) { + updateZones = false; + + if (bitRead(dsc.openZonesChanged[zoneGroup], zoneBit)) { // Checks an individual open zone status flag + bitWrite(dsc.openZonesChanged[zoneGroup], zoneBit, 0); // Resets the individual open zone status flag + + if (bitRead(dsc.openZones[zoneGroup], zoneBit)) zoneState->setVal(1); // Set zone status open + else zoneState->setVal(0); // Set zone status closed + } + + // Checks if additional configured zones have changed + for (byte checkZoneGroup = 0; checkZoneGroup < dscZones; checkZoneGroup++) { + for (byte checkZoneBit = 0; checkZoneBit < 8; checkZoneBit++) { + if (bitRead(configuredZones[checkZoneGroup], checkZoneBit) && bitRead(dsc.openZonesChanged[checkZoneGroup], checkZoneBit)) { // Checks if additional zones have changed + updateZones = true; // Sets a flag to continue processing remaining changed zones that are configured + } + } + } + } + } +}; + + +// Fire alarms are defined as separate Smoke Sensor accessories +struct dscFire : Service::SmokeSensor { + byte partition; + SpanCharacteristic *fireState; + dscFire(byte setPartition) : Service::SmokeSensor() { + partition = setPartition - 1; + fireState = new Characteristic::SmokeDetected(0); + } + + // Checks for fire status changes to send to HomeKit + void loop() { + if (updateSmokeSensors) { + updateSmokeSensors = false; + + dsc.fireChanged[partition] = false; // Resets the fire status flag + + if (dsc.fire[partition]) fireState->setVal(1); // Fire alarm tripped + else fireState->setVal(0); // Fire alarm restored + + // Checks for changed status in additional partitions + for (byte checkPartition = 0; checkPartition < dscPartitions; checkPartition++) { + + // Skips processing if the partition is disabled, in installer programming, or not configured in the sketch + if (dsc.disabled[checkPartition] || !configuredPartitions[checkPartition]) continue; + + // Checks for changed fire status in a partition + if (dsc.fireChanged[checkPartition]) updateSmokeSensors = true; + } + } + } +}; + + +// PGM outputs are defined as Contact Sensor accessories +struct dscPGM : Service::ContactSensor { + byte pgmGroup, pgmBit; + SpanCharacteristic *pgmState; + dscPGM(byte pgm) : Service::ContactSensor() { + pgmGroup = (pgm - 1) / 8; + pgmBit = (pgm - 1) - (pgmGroup * 8); + bitWrite(configuredPGMs[pgmGroup], pgmBit, 1); // Sets a PGM output as being configured + pgmState = new Characteristic::ContactSensorState(0); + } + + // Checks for PGM status changes to send to HomeKit + void loop() { + if (updatePGMs) { + updatePGMs = false; + + if (bitRead(dsc.pgmOutputsChanged[pgmGroup], pgmBit)) { + + // Handles PGMs defined both as this contact sensor accessory and for a command switch output switch accessory + if (bitRead(configuredCommandPGMs[pgmGroup], pgmBit)) { + + // Sets processing status of the PGM depending on the order in which this accessory is handled + if (bitRead(pendingPGMs[pgmGroup], pgmBit)) { + bitWrite(pendingPGMs[pgmGroup], pgmBit, 0); + bitWrite(dsc.pgmOutputsChanged[pgmGroup], pgmBit, 0); + } + else bitWrite(pendingPGMs[pgmGroup], pgmBit, 1); + + if (bitRead(dsc.pgmOutputs[pgmGroup], pgmBit)) pgmState->setVal(1); // Set PGM output status on + else pgmState->setVal(0); // Set PGM output status off + } + + // Handles PGMs defined only for this contact sensor accessory + else { + bitWrite(dsc.pgmOutputsChanged[pgmGroup], pgmBit, 0); + if (bitRead(dsc.pgmOutputs[pgmGroup], pgmBit)) pgmState->setVal(1); // Set command output status on + else pgmState->setVal(0); // Set command output status off + } + } + + // Checks if additional configured PGM outputs have changed + for (byte checkPGMGroup = 0; checkPGMGroup < 2; checkPGMGroup++) { + for (byte checkPGMBit = 0; checkPGMBit < 8; checkPGMBit++) { + if (bitRead(dsc.pgmOutputsChanged[checkPGMGroup], checkPGMBit)) { + if (bitRead(configuredPGMs[checkPGMGroup], checkPGMBit) || bitRead(configuredCommandPGMs[checkPGMGroup], checkPGMBit)) { // Checks if additional PGM outputs have changed + updatePGMs = true; // Sets a flag to continue processing changed PGM outputs that are configured + } + } + } + } + } + } +}; + + +// Command outputs 1-4 are defined as Switch accessories - this allows HomeKit to view status and +// control the PGM outputs assigned to each command output +struct dscCommand : Service::Switch { + byte cmd, pgmGroup, pgmBit, partition; + SpanCharacteristic *cmdState; + dscCommand(byte setCMD, byte pgm, byte setPartition) : Service::Switch() { + cmd = setCMD; + partition = setPartition; + pgmGroup = (pgm - 1) / 8; + pgmBit = (pgm - 1) - (pgmGroup * 8); + bitWrite(configuredCommandPGMs[pgmGroup], pgmBit, 1); // Sets a PGM output as being configured + cmdState = new Characteristic::On(0); + } + + + // Handles requests received from HomeKit + boolean update() { + byte targetState = cmdState->getNewVal(); + + // HomeKit requests switch on - enables the command output if its assigned PGM output is inactive + if (targetState == 1 && !bitRead(dsc.pgmOutputs[pgmGroup], pgmBit)) { + dsc.writePartition = partition; + switch (cmd) { + case 1: dsc.write('['); break; + case 2: dsc.write(']'); break; + case 3: dsc.write('{'); break; + case 4: dsc.write('}'); break; + default: cmdState->setVal(0); + } + } + + // HomeKit requests switch off - resets the HomeKit state to On if the PGM output is still active + else if (targetState == 0 && bitRead(dsc.pgmOutputs[pgmGroup], pgmBit)) { + cmdState->setVal(1); + } + + return(true); + } + + + // Checks for PGM status changes to send to HomeKit + void loop() { + if (updatePGMs) { + updatePGMs = false; + + if (bitRead(dsc.pgmOutputsChanged[pgmGroup], pgmBit)) { + + // Handles PGMs defined both for this command switch output switch accessory and as a contact sensor accessory + if (bitRead(configuredPGMs[pgmGroup], pgmBit)) { + + // Sets processing status of the PGM depending on the order in which this accessory is handled + if (bitRead(pendingPGMs[pgmGroup], pgmBit)) { + bitWrite(pendingPGMs[pgmGroup], pgmBit, 0); + bitWrite(dsc.pgmOutputsChanged[pgmGroup], pgmBit, 0); + } + else bitWrite(pendingPGMs[pgmGroup], pgmBit, 1); + + if (bitRead(dsc.pgmOutputs[pgmGroup], pgmBit)) cmdState->setVal(1); // Set command output status on + else cmdState->setVal(0); // Set command output status off + } + + // Handles PGMs defined only for this command output switch accessory + else { + bitWrite(dsc.pgmOutputsChanged[pgmGroup], pgmBit, 0); + if (bitRead(dsc.pgmOutputs[pgmGroup], pgmBit)) cmdState->setVal(1); // Set command output status on + else cmdState->setVal(0); // Set command output status off + } + } + + // Checks if additional configured PGM outputs have changed + for (byte checkPGMGroup = 0; checkPGMGroup < 2; checkPGMGroup++) { + for (byte checkPGMBit = 0; checkPGMBit < 8; checkPGMBit++) { + if (bitRead(dsc.pgmOutputsChanged[checkPGMGroup], checkPGMBit)) { + if (bitRead(configuredPGMs[checkPGMGroup], checkPGMBit) || bitRead(configuredCommandPGMs[checkPGMGroup], checkPGMBit)) { // Checks if additional PGM outputs have changed + updatePGMs = true; // Sets a flag to continue processing changed PGM outputs that are configured + } + } + } + } + } + } +}; + + +// HomeSpan Identify +struct homeSpanIdentify : Service::AccessoryInformation { + homeSpanIdentify(const char *name, const char *manu, const char *sn, const char *model, const char *version) : Service::AccessoryInformation() { + new Characteristic::Name(name); + new Characteristic::Manufacturer(manu); + new Characteristic::SerialNumber(sn); + new Characteristic::Model(model); + new Characteristic::FirmwareRevision(version); + new Characteristic::Identify(); + } +}; diff --git a/examples/esp32/Homebridge-MQTT/Homebridge-MQTT.ino b/examples/esp32/Homebridge-MQTT/Homebridge-MQTT.ino index 1ca9f47..44e52e8 100644 --- a/examples/esp32/Homebridge-MQTT/Homebridge-MQTT.ino +++ b/examples/esp32/Homebridge-MQTT/Homebridge-MQTT.ino @@ -1,5 +1,5 @@ /* - * Homebridge-MQTT 1.4 (esp32) + * Homebridge-MQTT 1.7 (esp32) * * Processes the security system status and allows for control using Apple HomeKit, including the iOS Home app, * Siri, and Google Home. This uses MQTT to interface with Homebridge and the homebridge-mqttthing plugin for @@ -147,6 +147,9 @@ * Closed: "0" * * Release notes: + * 1.7 - Fixed exit delay states while multiple partitions are arming + * 1.6 - Added DSC Classic series support + * 1.5 - Support switching armed modes while armed * 1.4 - Added PGM outputs 1-14 status * Added notes on Google Home integration * 1.0 - Initial release @@ -156,17 +159,24 @@ * * DSC Aux(-) --- esp32 Ground * - * +--- dscClockPin (esp32: 4,13,16-39) + * +--- dscClockPin // Default: 18 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp32: 4,13,16-39) + * +--- dscReadPin // Default: 19 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp32: 4,13,16-33) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: 21 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -180,6 +190,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -187,7 +200,7 @@ // Settings const char* wifiSSID = ""; const char* wifiPassword = ""; -const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm based on panel configuration. +const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm or enable command outputs based on panel configuration. const char* mqttServer = ""; // MQTT server domain name or IP address const int mqttPort = 1883; // MQTT server port const char* mqttUsername = ""; // Optional, leave blank if not required @@ -203,16 +216,21 @@ const char* mqttSubscribeTopic = "dsc/Set"; // Receives messages to w // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. -#define dscClockPin 18 // esp32: 4,13,16-39 -#define dscReadPin 19 // esp32: 4,13,16-39 -#define dscWritePin 21 // esp32: 4,13,16-33 +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 +#define dscWritePin 21 // 4,13,16-33 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif WiFiClient ipClient; PubSubClient mqtt(mqttServer, mqttPort, ipClient); unsigned long mqttPreviousTime; -char exitState; +char exitState[8]; void setup() { @@ -221,7 +239,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi...")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -251,13 +269,13 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; } - // Sends the access code when needed by the panel for arming + // Sends the access code when needed by the panel for arming or command outputs if (dsc.accessCodePrompt) { dsc.accessCodePrompt = false; dsc.write(accessCode); @@ -272,7 +290,7 @@ void loop() { // Publishes armed/disarmed status if (dsc.armedChanged[partition]) { if (dsc.armed[partition]) { - exitState = 0; + exitState[partition] = 0; // Night armed away if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) { @@ -307,21 +325,21 @@ void loop() { if (dsc.exitDelay[partition]) { // Sets the arming target state if the panel is armed externally - if (exitState == 0 || dsc.exitStateChanged[partition]) { + if (exitState[partition] == 0 || dsc.exitStateChanged[partition]) { dsc.exitStateChanged[partition] = 0; switch (dsc.exitState[partition]) { case DSC_EXIT_STAY: { - exitState = 'S'; + exitState[partition] = 'S'; publishState(mqttPartitionTopic, partition, "S", 0); break; } case DSC_EXIT_AWAY: { - exitState = 'A'; + exitState[partition] = 'A'; publishState(mqttPartitionTopic, partition, "A", 0); break; } case DSC_EXIT_NO_ENTRY_DELAY: { - exitState = 'N'; + exitState[partition] = 'N'; publishState(mqttPartitionTopic, partition, "N", 0); break; } @@ -331,7 +349,7 @@ void loop() { // Disarmed during exit delay else if (!dsc.armed[partition]) { - exitState = 0; + exitState[partition] = 0; publishState(mqttPartitionTopic, partition, "D", "D"); } } @@ -436,7 +454,52 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { payloadIndex = 1; } - // Resets the HomeKit target state if attempting to change the armed mode while armed or not ready + // Sets night arm (no entry delay) while armed + if (payload[payloadIndex] == 'N' && dsc.armed[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write('n'); // Keypad no entry delay + publishState(mqttPartitionTopic, partition, "N", 0); + exitState[partition] = 'N'; + return; + } + + // Disables night arm while armed stay + if (payload[payloadIndex] == 'S' && dsc.armedStay[partition] && dsc.noEntryDelay[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write('n'); // Keypad no entry delay + publishState(mqttPartitionTopic, partition, "S", 0); + exitState[partition] = 'S'; + return; + } + + // Disables night arm while armed away + if (payload[payloadIndex] == 'A' && dsc.armedAway[partition] && dsc.noEntryDelay[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write('n'); // Keypad no entry delay + publishState(mqttPartitionTopic, partition, "A", 0); + exitState[partition] = 'A'; + return; + } + + // Changes from arm away to arm stay after the exit delay + if (payload[payloadIndex] == 'S' && dsc.armedAway[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write("s"); + publishState(mqttPartitionTopic, partition, "S", 0); + exitState[partition] = 'S'; + return; + } + + // Changes from arm stay to arm away after the exit delay + if (payload[payloadIndex] == 'A' && dsc.armedStay[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write("w"); + publishState(mqttPartitionTopic, partition, "A", 0); + exitState[partition] = 'A'; + return; + } + + // Resets the HomeKit target state if attempting to change the armed mode while not ready if (payload[payloadIndex] != 'D' && !dsc.ready[partition]) { dsc.armedChanged[partition] = true; dsc.statusChanged = true; @@ -444,10 +507,10 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { } // Resets the HomeKit target state if attempting to change the arming mode during the exit delay - if (payload[payloadIndex] != 'D' && dsc.exitDelay[partition] && exitState != 0) { - if (exitState == 'S') publishState(mqttPartitionTopic, partition, "S", 0); - else if (exitState == 'A') publishState(mqttPartitionTopic, partition, "A", 0); - else if (exitState == 'N') publishState(mqttPartitionTopic, partition, "N", 0); + if (payload[payloadIndex] != 'D' && dsc.exitDelay[partition] && exitState[partition] != 0) { + if (exitState[partition] == 'S') publishState(mqttPartitionTopic, partition, "S", 0); + else if (exitState[partition] == 'A') publishState(mqttPartitionTopic, partition, "A", 0); + else if (exitState[partition] == 'N') publishState(mqttPartitionTopic, partition, "N", 0); } @@ -456,7 +519,7 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { dsc.writePartition = partition + 1; // Sets writes to the partition number dsc.write('s'); // Keypad stay arm publishState(mqttPartitionTopic, partition, "S", 0); - exitState = 'S'; + exitState[partition] = 'S'; return; } @@ -465,7 +528,7 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { dsc.writePartition = partition + 1; // Sets writes to the partition number dsc.write('w'); // Keypad away arm publishState(mqttPartitionTopic, partition, "A", 0); - exitState = 'A'; + exitState[partition] = 'A'; return; } @@ -474,7 +537,7 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { dsc.writePartition = partition + 1; // Sets writes to the partition number dsc.write('n'); // Keypad arm with no entry delay publishState(mqttPartitionTopic, partition, "N", 0); - exitState = 'N'; + exitState[partition] = 'N'; return; } diff --git a/examples/esp32/Homey/Homey.ino b/examples/esp32/Homey/Homey.ino index dce044d..0501007 100755 --- a/examples/esp32/Homey/Homey.ino +++ b/examples/esp32/Homey/Homey.ino @@ -1,5 +1,5 @@ /* - * Homey 1.0 (esp32) + * Homey 1.1 (esp32) * * Processes the security system status for partition 1 and allows for control using Athom Homey. * @@ -12,6 +12,7 @@ * Zone states are published by Homey.trigger command including the zone number. * * Release notes: + * 1.1 - Added DSC Classic series support * 1.0 - Initial release * * Wiring: @@ -19,17 +20,24 @@ * * DSC Aux(-) --- esp32 Ground * - * +--- dscClockPin (esp32: 4,13,16-39) + * +--- dscClockPin // Default: 18 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp32: 4,13,16-39) + * +--- dscReadPin // Default: 19 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp32: 4,13,16-33) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: 21 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -45,6 +53,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -56,12 +67,17 @@ const char* accessCode = ""; // An access code is required to disarm/night arm // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. -#define dscClockPin 18 // esp32: 4,13,16-39 -#define dscReadPin 19 // esp32: 4,13,16-39 -#define dscWritePin 21 // esp32: 4,13,16-33 +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 +#define dscWritePin 21 // 4,13,16-33 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif bool wifiConnected = true; @@ -71,7 +87,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi...")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -118,14 +134,13 @@ void loop() { // Run the Homey loop Homey.loop(); - dsc.loop(); if (dsc.statusChanged) { // Checks if the security system status has changed dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; diff --git a/examples/esp32/KeybusReader/KeybusReader.ino b/examples/esp32/KeybusReader/KeybusReader.ino index 75411fa..093a358 100644 --- a/examples/esp32/KeybusReader/KeybusReader.ino +++ b/examples/esp32/KeybusReader/KeybusReader.ino @@ -1,11 +1,12 @@ /* - * DSC Keybus Reader 1.2 (esp32) + * DSC Keybus Reader 1.3 (esp32) * * Decodes and prints data from the Keybus to a serial interface, including reading from serial for the virtual * keypad. This is primarily to help decode the Keybus protocol - see the Status example to put the interface * to productive use. * * Release notes: + * 1.3 - Added DSC Classic series support * 1.2 - Handle spurious data while keybus is disconnected * Removed redundant data processing * 1.0 - Initial release @@ -15,17 +16,24 @@ * * DSC Aux(-) --- esp32 Ground * - * +--- dscClockPin (esp32: 4,13,16-39) + * +--- dscClockPin // Default: 18 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp32: 4,13,16-39) + * +--- dscReadPin // Default: 19 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp32: 4,13,16-33) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: 21 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -39,16 +47,24 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. -#define dscClockPin 18 // esp32: 4,13,16-39 -#define dscReadPin 19 // esp32: 4,13,16-39 -#define dscWritePin 21 // esp32: 4,13,16-33 +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 +#define dscWritePin 21 // 4,13,16-33 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin); +#endif void setup() { @@ -88,7 +104,7 @@ void loop() { } // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; diff --git a/examples/esp32/KeybusReaderIP/.DS_Store b/examples/esp32/KeybusReaderIP/.DS_Store deleted file mode 100644 index 8b159c0..0000000 Binary files a/examples/esp32/KeybusReaderIP/.DS_Store and /dev/null differ diff --git a/examples/esp32/KeybusReaderIP/KeybusReaderIP.ino b/examples/esp32/KeybusReaderIP/KeybusReaderIP.ino index 301dda5..53b363d 100644 --- a/examples/esp32/KeybusReaderIP/KeybusReaderIP.ino +++ b/examples/esp32/KeybusReaderIP/KeybusReaderIP.ino @@ -1,5 +1,5 @@ /* - * DSC Keybus Reader IP 1.2 (esp32) + * DSC Keybus Reader IP 1.3 (esp32) * * Decodes and prints data from the Keybus to a TCP connection including virtual keyboard over IP. This is * primarily to help decode the Keybus protocol - see the Status example to put the interface to productive use. @@ -9,6 +9,7 @@ * 2. For macOS/Linux: telnet dsc.local * * Release notes: + * 1.3 - Added DSC Classic series support * 1.2 - Updated to connect via telnet * Handle spurious data while keybus is disconnected * Removed redundant data processing @@ -19,17 +20,24 @@ * * DSC Aux(-) --- esp32 Ground * - * +--- dscClockPin (esp32: 4,13,16-39) + * +--- dscClockPin // Default: 18 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp32: 4,13,16-39) + * +--- dscReadPin // Default: 19 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp32: 4,13,16-33) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: 21 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -45,6 +53,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -57,12 +68,17 @@ const int serverPort = 23; // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. -#define dscClockPin 18 // esp32: 4,13,16-39 -#define dscReadPin 19 // esp32: 4,13,16-39 -#define dscWritePin 21 // esp32: 4,13,16-33 +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 +#define dscWritePin 21 // 4,13,16-33 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin); +#endif WiFiServer ipServer(serverPort); WiFiClient ipClient; @@ -73,7 +89,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -145,7 +161,7 @@ void loop() { if (dsc.loop()) { // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { ipClient.print(F("Keybus buffer overflow")); dsc.bufferOverflow = false; diff --git a/examples/esp32/KeypadInterface-MQTT/KeypadInterface-MQTT.ino b/examples/esp32/KeypadInterface-MQTT/KeypadInterface-MQTT.ino new file mode 100644 index 0000000..9c0136d --- /dev/null +++ b/examples/esp32/KeypadInterface-MQTT/KeypadInterface-MQTT.ino @@ -0,0 +1,365 @@ +/* + * DSC Keypad Interface-MQTT 1.2 (esp32) + * + * Emulates a DSC panel to directly interface DSC PowerSeries or Classic series keypads as physical + * input devices for any general purpose, without needing a DSC panel. This sketch uses MQTT to + * send pressed keypad keys and receive commands to control keypad lights and tones. + * + * PowerSeries keypad features: + * - Read keypad key button presses, including fire/aux/panic alarm keys: dsc.key + * - Set keypad lights: Ready, Armed, Trouble, Memory, Bypass, Fire, Program, Backlight, Zones 1-8: dsc.lightReady, dsc.lightZone1, etc + * - Set keypad beeps, 1-128: dsc.beep(3) + * - Set keypad buzzer in seconds, 1-255: dsc.tone(5) + * - Set keypad tone pattern with a number of beeps, an optional constant tone, and the interval in seconds between beeps: + * 2 beeps, no constant tone, 4 second interval: dsc.tone(2, false, 4) + * 3 beeps, constant tone, 2 second interval: dsc.tone(3, true, 2) + * Disable the tone: dsc.tone() or dsc.tone(0, false, 0) + * + * Classic keypad features: + * - Read keypad key button presses, including fire/aux/panic alarm keys: dsc.key + * - Set keypad lights: Ready, Armed, Trouble, Memory, Bypass, Fire, Program, Zones 1-8: dsc.lightReady, dsc.lightZone1, etc + * + * This interface uses a different wiring setup from the standard Keybus interface, adding + * an NPN transistor on dscClockPin. The DSC keypads require a 12v DC power source, though + * lower voltages down to 7v may work for key presses (the LEDs will be dim). + * + * Release notes: + * 1.3 - Add Classic keypad support - PC2550RK + * 1.2 - Add Classic keypad support - PC1500RK + * 1.1 - Add keypad beep, buzzer, constant tone + * 1.0 - Initial release + * + * Wiring: + * DSC Keypad R --- 12v DC + * + * DSC Keypad B --- esp32 ground + * + * DSC Keypad Y ---+--- 1k ohm resistor --- 12v DC + * | + * +--- NPN collector --\ + * |-- NPN base --- 1k ohm resistor --- dscClockPin // esp32: 18 + * Ground --- NPN emitter --/ + * + * DSC Keypad G ---+--- 1k ohm resistor --- 12v DC + * | + * +--- 33k ohm resistor ---+--- dscReadPin // esp32: 19 + * | | + * | +--- 10k ohm resistor --- Ground + * | + * +--- NPN collector --\ + * |-- NPN base --- 1k ohm resistor --- dscWritePin // esp32: 21 + * Ground --- NPN emitter --/ + * + * The keypad interface uses NPN transistors to pull the clock and data lines low - most small + * signal NPN transistors should be suitable, for example: + * - 2N3904 + * - BC547, BC548, BC549 + * + * Issues and (especially) pull requests are welcome: + * https://github.com/taligentx/dscKeybusInterface + * + * This example code is in the public domain. + */ + +// Set the keypad type +#define dscKeypad +//#define dscClassicKeypad + +#include +#include +#include + +// Settings +const char* wifiSSID = ""; +const char* wifiPassword = ""; +const char* mqttServer = ""; // MQTT server domain name or IP address +const int mqttPort = 1883; // MQTT server port +const char* mqttUsername = ""; // Optional, leave blank if not required +const char* mqttPassword = ""; // Optional, leave blank if not required + +// MQTT topics +const char* mqttClientName = "dscKeypadInterface"; +const char* mqttKeyTopic = "dsc/Key"; // Sends keypad keys +const char* mqttSubscribeTopic = "dsc/Set"; // Receives messages to send to the keypad + +// Configures the Keybus interface with the specified pins +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscWritePin 21 // 4,13,16-33 + +// Initialize components +#ifdef dscKeypad +dscKeypadInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicKeypadInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#endif +bool lightOff, lightBlink, inputReceived; +const byte inputLimit = 255; +char input[inputLimit]; +byte beepLength, buzzerLength, toneLength; +WiFiClient ipClient; +PubSubClient mqtt(mqttServer, mqttPort, ipClient); +unsigned long mqttPreviousTime; + + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println(); + Serial.println(); + + Serial.print(F("WiFi....")); + WiFi.mode(WIFI_STA); + WiFi.begin(wifiSSID, wifiPassword); + while (WiFi.status() != WL_CONNECTED) { + Serial.print("."); + delay(500); + } + Serial.print(F("connected: ")); + Serial.println(WiFi.localIP()); + + mqtt.setCallback(mqttCallback); + if (mqttConnect()) mqttPreviousTime = millis(); + else mqttPreviousTime = 0; + + Serial.print(F("Keybus....")); + dsc.begin(); + Serial.println(F("connected.")); + Serial.println(F("DSC Keypad Interface is online.")); +} + +void loop() { + mqttHandle(); + + /* + * Sets keypad status via serial with the listed keys. Light status uses custom + * values for control: off, on, blink (example: dsc.lightReady = blink;) + * + * Light on: Send the keys listed below. Turning on the armed light: "a" + * Light off: Send "-" before a light key to turn it off. Turning off the zone 4 light: "-4" + * Light blink: Send "!" before a light key to blink. Blinking the ready light: "!r" + * Beep: Send "b" followed by the number of beeps, 1-128. Setting 2 beeps: "b2" + * Buzzer: Send "z" followed by the buzzer length in seconds, 1-255. Setting the buzzer to 5 seconds: "z5" + * Tone pattern: Send "n" followed by the number of beeps 1-7, constant tone true "t" or false "f", interval between beeps 1-15s + * Setting a tone pattern with 2 beeps, no constant tone, 4 second interval: "n2f4" + * Setting a tone pattern with 3 beeps, constant tone, 2 second interval: "n3t2" + * Disabling the tone pattern: "n" + */ + if (inputReceived) { + inputReceived = false; + + for (byte i = 0; i < strlen(input); i++) { + switch (input[i]) { + case 'r': case 'R': dsc.lightReady = setLight(); break; + case 'a': case 'A': dsc.lightArmed = setLight(); break; + case 'm': case 'M': dsc.lightMemory = setLight(); break; + case 'y': case 'Y': dsc.lightBypass = setLight(); break; + case 't': case 'T': dsc.lightTrouble = setLight(); break; + case 'p': case 'P': dsc.lightProgram = setLight(); break; + case 'f': case 'F': dsc.lightFire = setLight(); break; + case 'l': case 'L': dsc.lightBacklight = setLight(); break; + case '1': dsc.lightZone1 = setLight(); break; + case '2': dsc.lightZone2 = setLight(); break; + case '3': dsc.lightZone3 = setLight(); break; + case '4': dsc.lightZone4 = setLight(); break; + case '5': dsc.lightZone5 = setLight(); break; + case '6': dsc.lightZone6 = setLight(); break; + case '7': dsc.lightZone7 = setLight(); break; + case '8': dsc.lightZone8 = setLight(); break; + case 'b': case 'B': sendBeeps(i); i += beepLength; break; + case 'n': case 'N': sendTone(i); i+= toneLength; break; + case 'z': case 'Z': sendBuzzer(i); i+= buzzerLength; break; + case '-': lightOff = true; break; + case '!': lightBlink = true; break; + default: break; + } + } + } + + dsc.loop(); + + // Checks for a keypad key press + if (dsc.keyAvailable) { + dsc.keyAvailable = false; + switch (dsc.key) { + case 0x00: mqtt.publish(mqttKeyTopic, "0", false); break; + case 0x05: mqtt.publish(mqttKeyTopic, "1", false); break; + case 0x0A: mqtt.publish(mqttKeyTopic, "2", false); break; + case 0x0F: mqtt.publish(mqttKeyTopic, "3", false); break; + case 0x11: mqtt.publish(mqttKeyTopic, "4", false); break; + case 0x16: mqtt.publish(mqttKeyTopic, "5", false); break; + case 0x1B: mqtt.publish(mqttKeyTopic, "6", false); break; + case 0x1C: mqtt.publish(mqttKeyTopic, "7", false); break; + case 0x22: mqtt.publish(mqttKeyTopic, "8", false); break; + case 0x27: mqtt.publish(mqttKeyTopic, "9", false); break; + case 0x28: mqtt.publish(mqttKeyTopic, "*", false); break; + case 0x2D: mqtt.publish(mqttKeyTopic, "#", false); break; + case 0x82: mqtt.publish(mqttKeyTopic, "Enter", false); break; + case 0xAF: mqtt.publish(mqttKeyTopic, "Arm: Stay", false); break; + case 0xB1: mqtt.publish(mqttKeyTopic, "Arm: Away", false); break; + case 0xBB: mqtt.publish(mqttKeyTopic, "Door chime", false); break; + case 0xDA: mqtt.publish(mqttKeyTopic, "Reset", false); break; + case 0xE1: mqtt.publish(mqttKeyTopic, "Quick exit", false); break; + case 0xF7: mqtt.publish(mqttKeyTopic, "Menu navigation", false); break; + case 0x0B: mqtt.publish(mqttKeyTopic, "Fire alarm", false); break; + case 0x0D: mqtt.publish(mqttKeyTopic, "Aux alarm", false); break; + case 0x0E: mqtt.publish(mqttKeyTopic, "Panic alarm", false); break; + } + mqtt.subscribe(mqttSubscribeTopic); + } +} + + +// Parse the number of beeps from the input +void sendBeeps(byte position) { + char inputNumber[4]; + byte beeps = 0; + beepLength = 0; + + for (byte i = position + 1; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + inputNumber[beepLength] = input[i]; + beepLength++; + if (beepLength >= 3) break; + } + else break; + } + + inputNumber[beepLength] = '\0'; + beeps = atoi(inputNumber); + if (beeps > 128) beeps = 128; + + dsc.beep(beeps); +} + + +// Parse the buzzer length in seconds from the input +void sendBuzzer(byte position) { + char inputNumber[4]; + byte buzzerSeconds = 0; + buzzerLength = 0; + + for (byte i = position + 1; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + inputNumber[buzzerLength] = input[i]; + buzzerLength++; + if (buzzerLength >= 3) break; + } + else break; + } + + inputNumber[buzzerLength] = '\0'; + buzzerSeconds = atoi(inputNumber); + dsc.buzzer(buzzerSeconds); +} + + +// Parse the tone pattern number of beeps, constant tone state, and interval in seconds from the input +void sendTone(byte position) { + byte beeps = 0, interval = 0, intervalLength = 0; + char beepNumber[2]; + bool toneState; + char intervalNumber[3]; + toneLength = 0; + + if (strlen(input) < 4) { + dsc.tone(0, false, 0); + return; + } + + // Parse beeps 0-7 + if (input[position + 1] >= '0' && input[position + 1] <= '9') { + beepNumber[0] = input[position + 1]; + beeps = atoi(beepNumber); + if (beeps > 7) beeps = 7; + toneLength++; + } + else return; + + // Parse constant tone value + switch (input[position + 2]) { + case 't': + case 'T': toneState = true; toneLength++; break; + case 'f': + case 'F': toneState = false; toneLength++; break; + default: toneLength--; return; + } + + // Parse interval + for (byte i = position + 3; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + intervalNumber[intervalLength] = input[i]; + intervalLength++; + toneLength++; + if (intervalLength >= 2) break; + } + else break; + } + intervalNumber[intervalLength] = '\0'; + interval = atoi(intervalNumber); + if (interval > 15) interval = 15; + + dsc.tone(beeps, toneState, interval); +} + + +// Sets keypad lights state - lights use custom values for control: off, on, blink (example: dsc.lightReady = blink;) +Light setLight() { + if (lightOff) { + lightOff = false; + return off; + } + else if (lightBlink) { + lightBlink = false; + return blink; + } + else return on; +} + + +// Handles messages received in the mqttSubscribeTopic +void mqttCallback(char* topic, byte* payload, unsigned int length) { + + // Handles unused parameters + (void)topic; + + for (unsigned int i = 0; i < length; i++) { + input[i] = payload[i]; + } + + input[length] = '\0'; + if (input[0] == '\0') inputReceived = false; + else inputReceived = true; +} + + +void mqttHandle() { + if (!mqtt.connected()) { + unsigned long mqttCurrentTime = millis(); + if (mqttCurrentTime - mqttPreviousTime > 5000) { + mqttPreviousTime = mqttCurrentTime; + if (mqttConnect()) { + Serial.println(F("MQTT disconnected, successfully reconnected.")); + mqttPreviousTime = 0; + mqtt.subscribe(mqttSubscribeTopic); + } + else Serial.println(F("MQTT disconnected, failed to reconnect.")); + } + } + else mqtt.loop(); +} + + +bool mqttConnect() { + Serial.print(F("MQTT....")); + if (mqtt.connect(mqttClientName, mqttUsername, mqttPassword)) { + Serial.print(F("connected: ")); + Serial.println(mqttServer); + mqtt.subscribe(mqttSubscribeTopic); + } + else { + Serial.print(F("connection error: ")); + Serial.println(mqttServer); + } + return mqtt.connected(); +} diff --git a/examples/esp32/KeypadInterface/KeypadInterface.ino b/examples/esp32/KeypadInterface/KeypadInterface.ino new file mode 100644 index 0000000..89ffe47 --- /dev/null +++ b/examples/esp32/KeypadInterface/KeypadInterface.ino @@ -0,0 +1,343 @@ +/* + * DSC Keypad Interface 1.4 (esp32) + * + * Emulates a DSC panel to directly interface DSC PowerSeries or Classic series keypads as physical + * input devices for any general purpose, without needing a DSC panel. + * + * PowerSeries keypad features: + * - Read keypad key button presses, including fire/aux/panic alarm keys: dsc.key + * - Set keypad lights: Ready, Armed, Trouble, Memory, Bypass, Fire, Program, Backlight, Zones 1-8: dsc.lightReady, dsc.lightZone1, etc + * - Set keypad beeps, 1-128: dsc.beep(3) + * - Set keypad buzzer in seconds, 1-255: dsc.tone(5) + * - Set keypad tone pattern with a number of beeps, an optional constant tone, and the interval in seconds between beeps: + * 2 beeps, no constant tone, 4 second interval: dsc.tone(2, false, 4) + * 3 beeps, constant tone, 2 second interval: dsc.tone(3, true, 2) + * Disable the tone: dsc.tone() or dsc.tone(0, false, 0) + * - Set LCD keypad messages (on cmd 0x05/byte3) with entering HEX input into serial console: + * According to printPanelMessages in dscKeybusPrintData.cpp, through it doesn't seem to fully match + * Change Function keys 1-5 with entering: 0x70 - 0x74 + * Change LCD keypad time by entering: 0x2A (slight delay before LCD will show to input time data) + * Change LCD Brightness/contrast/buzzer level by entering: 0x29 and scrolling to desired setting then pressing (*) + * + * Classic keypad features: + * - Read keypad key button presses, including fire/aux/panic alarm keys: dsc.key + * - Set keypad lights: Ready, Armed, Trouble, Memory, Bypass, Fire, Program, Zones 1-8: dsc.lightReady, dsc.lightZone1, etc + * + * This interface uses a different wiring setup from the standard Keybus interface, adding + * an NPN transistor on dscClockPin. The DSC keypads require a 12v DC power source, though + * lower voltages down to 7v may work for key presses (the LEDs will be dim). + * + * Release notes: + * 1.4 - Added ability to change LCD keypad messages + * 1.3 - Add Classic keypad support - PC2550RK + * 1.2 - Add Classic keypad support - PC1500RK + * 1.1 - Add keypad beep, buzzer, constant tone + * 1.0 - Initial release + * + * Wiring: + * DSC Keypad R --- 12v DC + * + * DSC Keypad B --- esp32 ground + * + * DSC Keypad Y ---+--- 1k ohm resistor --- 12v DC + * | + * +--- NPN collector --\ + * |-- NPN base --- 1k ohm resistor --- dscClockPin // esp32: 18 + * Ground --- NPN emitter --/ + * + * DSC Keypad G ---+--- 1k ohm resistor --- 12v DC + * | + * +--- 33k ohm resistor ---+--- dscReadPin // esp32: 19 + * | | + * | +--- 10k ohm resistor --- Ground + * | + * +--- NPN collector --\ + * |-- NPN base --- 1k ohm resistor --- dscWritePin // esp32: 21 + * Ground --- NPN emitter --/ + * + * The keypad interface uses NPN transistors to pull the clock and data lines low - most small + * signal NPN transistors should be suitable, for example: + * - 2N3904 + * - BC547, BC548, BC549 + * + * Issues and (especially) pull requests are welcome: + * https://github.com/taligentx/dscKeybusInterface + * + * This example code is in the public domain. + */ + +// Set the keypad type +#define dscKeypad +//#define dscClassicKeypad + +#include + +// Configures the Keybus interface with the specified pins +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscWritePin 21 // 4,13,16-33 + +// Initialize components +#ifdef dscKeypad +dscKeypadInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicKeypadInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#endif +bool lightOff, lightBlink, inputReceived; +const byte inputLimit = 50; +char input[inputLimit]; +byte beepLength, buzzerLength, toneLength; + + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println(); + Serial.println(); + + Serial.print(F("Keybus....")); + dsc.begin(); + Serial.println(F("connected.")); + Serial.println(F("DSC Keypad Interface is online.")); +} + +void loop() { + + inputSerial(); // Stores Serial data in input[], requires a newline character (NL, CR, or both) + + /* + * Sets keypad status via serial with the listed keys. Light status uses custom + * values for control: off, on, blink (example: dsc.lightReady = blink;) + * + * Light on: Send the keys listed below. Turning on the armed light: "a" + * Light off: Send "-" before a light key to turn it off. Turning off the zone 4 light: "-4" + * Light blink: Send "!" before a light key to blink. Blinking the ready light: "!r" + * Beep: Send "b" followed by the number of beeps, 1-128. Setting 2 beeps: "b2" + * Buzzer: Send "z" followed by the buzzer length in seconds, 1-255. Setting the buzzer to 5 seconds: "z5" + * Tone pattern: Send "n" followed by the number of beeps 1-7, constant tone true "t" or false "f", interval between beeps 1-15s + * Setting a tone pattern with 2 beeps, no constant tone, 4 second interval: "n2f4" + * Setting a tone pattern with 3 beeps, constant tone, 2 second interval: "n3t2" + * Disabling the tone pattern: "n" + */ + if (inputReceived) { + inputReceived = false; + + #if defined(dscKeypad) + if (String(input).startsWith("0x")) dsc.panelCommand05[2] = strtoul(input, NULL, 16); + else { + for (byte i = 0; i < strlen(input); i++) { + switch (input[i]) { + case 'r': case 'R': dsc.lightReady = setLight(); break; + case 'a': case 'A': dsc.lightArmed = setLight(); break; + case 'm': case 'M': dsc.lightMemory = setLight(); break; + case 'y': case 'Y': dsc.lightBypass = setLight(); break; + case 't': case 'T': dsc.lightTrouble = setLight(); break; + case 'p': case 'P': dsc.lightProgram = setLight(); break; + case 'f': case 'F': dsc.lightFire = setLight(); break; + case 'l': case 'L': dsc.lightBacklight = setLight(); break; + case '1': dsc.lightZone1 = setLight(); break; + case '2': dsc.lightZone2 = setLight(); break; + case '3': dsc.lightZone3 = setLight(); break; + case '4': dsc.lightZone4 = setLight(); break; + case '5': dsc.lightZone5 = setLight(); break; + case '6': dsc.lightZone6 = setLight(); break; + case '7': dsc.lightZone7 = setLight(); break; + case '8': dsc.lightZone8 = setLight(); break; + case 'b': case 'B': sendBeeps(i); i += beepLength; break; + case 'n': case 'N': sendTone(i); i+= toneLength; break; + case 'z': case 'Z': sendBuzzer(i); i+= buzzerLength; break; + case '-': lightOff = true; break; + case '!': lightBlink = true; break; + default: break; + } + } + } + #else + for (byte i = 0; i < strlen(input); i++) { + switch (input[i]) { + case 'r': case 'R': dsc.lightReady = setLight(); break; + case 'a': case 'A': dsc.lightArmed = setLight(); break; + case 'm': case 'M': dsc.lightMemory = setLight(); break; + case 'y': case 'Y': dsc.lightBypass = setLight(); break; + case 't': case 'T': dsc.lightTrouble = setLight(); break; + case 'p': case 'P': dsc.lightProgram = setLight(); break; + case 'f': case 'F': dsc.lightFire = setLight(); break; + case 'l': case 'L': dsc.lightBacklight = setLight(); break; + case '1': dsc.lightZone1 = setLight(); break; + case '2': dsc.lightZone2 = setLight(); break; + case '3': dsc.lightZone3 = setLight(); break; + case '4': dsc.lightZone4 = setLight(); break; + case '5': dsc.lightZone5 = setLight(); break; + case '6': dsc.lightZone6 = setLight(); break; + case '7': dsc.lightZone7 = setLight(); break; + case '8': dsc.lightZone8 = setLight(); break; + case 'b': case 'B': sendBeeps(i); i += beepLength; break; + case 'n': case 'N': sendTone(i); i+= toneLength; break; + case 'z': case 'Z': sendBuzzer(i); i+= buzzerLength; break; + case '-': lightOff = true; break; + case '!': lightBlink = true; break; + default: break; + } + } + #endif + } + + dsc.loop(); + + // Checks for a keypad key press + if (dsc.keyAvailable) { + dsc.keyAvailable = false; + switch (dsc.key) { + case 0x00: Serial.println("0"); break; + case 0x05: Serial.println("1"); break; + case 0x0A: Serial.println("2"); break; + case 0x0F: Serial.println("3"); break; + case 0x11: Serial.println("4"); break; + case 0x16: Serial.println("5"); break; + case 0x1B: Serial.println("6"); break; + case 0x1C: Serial.println("7"); break; + case 0x22: Serial.println("8"); break; + case 0x27: Serial.println("9"); break; + case 0x28: Serial.println("*"); break; + case 0x2D: Serial.println("#"); break; + case 0x82: Serial.println(F("Enter")); break; + case 0xAF: Serial.println(F("Arm: Stay")); break; + case 0xB1: Serial.println(F("Arm: Away")); break; + case 0xBB: Serial.println(F("Door chime")); break; + case 0xDA: Serial.println(F("Reset")); break; + case 0xE1: Serial.println(F("Quick exit")); break; + case 0xF7: Serial.println(F("Menu navigation")); break; + case 0x0B: Serial.println(F("Fire alarm")); break; + case 0x0D: Serial.println(F("Aux alarm")); break; + case 0x0E: Serial.println(F("Panic alarm")); break; + default: break; + } + } +} + + +// Parse the number of beeps from the input +void sendBeeps(byte position) { + char inputNumber[4]; + byte beeps = 0; + beepLength = 0; + + for (byte i = position + 1; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + inputNumber[beepLength] = input[i]; + beepLength++; + if (beepLength >= 3) break; + } + else break; + } + + inputNumber[beepLength] = '\0'; + beeps = atoi(inputNumber); + if (beeps > 128) beeps = 128; + + dsc.beep(beeps); +} + + +// Parse the buzzer length in seconds from the input +void sendBuzzer(byte position) { + char inputNumber[4]; + byte buzzerSeconds = 0; + buzzerLength = 0; + + for (byte i = position + 1; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + inputNumber[buzzerLength] = input[i]; + buzzerLength++; + if (buzzerLength >= 3) break; + } + else break; + } + + inputNumber[buzzerLength] = '\0'; + buzzerSeconds = atoi(inputNumber); + dsc.buzzer(buzzerSeconds); +} + + +// Parse the tone pattern number of beeps, constant tone state, and interval in seconds from the input +void sendTone(byte position) { + byte beeps = 0, interval = 0, intervalLength = 0; + char beepNumber[2]; + bool toneState; + char intervalNumber[3]; + toneLength = 0; + + if (strlen(input) < 4) { + dsc.tone(0, false, 0); + return; + } + + // Parse beeps 0-7 + if (input[position + 1] >= '0' && input[position + 1] <= '9') { + beepNumber[0] = input[position + 1]; + beeps = atoi(beepNumber); + if (beeps > 7) beeps = 7; + toneLength++; + } + else return; + + // Parse constant tone value + switch (input[position + 2]) { + case 't': + case 'T': toneState = true; toneLength++; break; + case 'f': + case 'F': toneState = false; toneLength++; break; + default: toneLength--; return; + } + + // Parse interval + for (byte i = position + 3; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + intervalNumber[intervalLength] = input[i]; + intervalLength++; + toneLength++; + if (intervalLength >= 2) break; + } + else break; + } + intervalNumber[intervalLength] = '\0'; + interval = atoi(intervalNumber); + if (interval > 15) interval = 15; + + dsc.tone(beeps, toneState, interval); +} + + +// Sets keypad lights state - lights use custom values for control: off, on, blink (example: dsc.lightReady = blink;) +Light setLight() { + if (lightOff) { + lightOff = false; + return off; + } + else if (lightBlink) { + lightBlink = false; + return blink; + } + else return on; +} + + +// Stores Serial data in input[], requires a newline character (NL, CR, or both) +void inputSerial() { + static byte inputCount = 0; + if (!inputReceived) { + while (Serial.available() > 0 && inputCount < inputLimit) { + input[inputCount] = Serial.read(); + if (input[inputCount] == '\n' || input[inputCount] == '\r') { + input[inputCount] = '\0'; + inputCount = 0; + inputReceived = true; + break; + } + else inputCount++; + yield(); + } + if (input[0] == '\0') inputReceived = false; + } +} diff --git a/examples/esp32/OpenHAB-MQTT/OpenHAB-MQTT.ino b/examples/esp32/OpenHAB-MQTT/OpenHAB-MQTT.ino index 2788627..65067db 100644 --- a/examples/esp32/OpenHAB-MQTT/OpenHAB-MQTT.ino +++ b/examples/esp32/OpenHAB-MQTT/OpenHAB-MQTT.ino @@ -1,5 +1,5 @@ /* - * OpenHAB-MQTT 1.2 (esp32) + * OpenHAB-MQTT 1.3 (esp32) * * Processes the security system status and allows for control using OpenHAB. This uses MQTT to * interface with OpenHAB and the MQTT binding and demonstrates sending the panel status as a @@ -89,6 +89,7 @@ Contact zone3 "Zone 3" {channel="mqtt:topic:mymqtt:dsc:zone3"} * Closed: "0" * * Release notes: + * 1.3 - Added DSC Classic series support * 1.2 - Added PGM outputs 1-14 status * 1.1 - Removed partition exit delay MQTT message, not used in this OpenHAB example * 1.0 - Initial release @@ -98,17 +99,24 @@ Contact zone3 "Zone 3" {channel="mqtt:topic:mymqtt:dsc:zone3"} * * DSC Aux(-) --- esp32 Ground * - * +--- dscClockPin (esp32: 4,13,16-39) + * +--- dscClockPin // Default: 18 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp32: 4,13,16-39) + * +--- dscReadPin // Default: 19 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp32: 4,13,16-33) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: 21 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -122,6 +130,9 @@ Contact zone3 "Zone 3" {channel="mqtt:topic:mymqtt:dsc:zone3"} * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -129,7 +140,7 @@ Contact zone3 "Zone 3" {channel="mqtt:topic:mymqtt:dsc:zone3"} // Settings const char* wifiSSID = ""; const char* wifiPassword = ""; -const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm based on panel configuration. +const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm or enable command outputs based on panel configuration. const char* mqttServer = ""; // MQTT server domain name or IP address const int mqttPort = 1883; // MQTT server port const char* mqttUsername = ""; // Optional, leave blank if not required @@ -150,12 +161,17 @@ const char* mqttSubscribeTopic = "dsc/Set"; // Receives messages to w // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. -#define dscClockPin 18 // esp32: 4,13,16-39 -#define dscReadPin 19 // esp32: 4,13,16-39 -#define dscWritePin 21 // esp32: 4,13,16-33 +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 +#define dscWritePin 21 // 4,13,16-33 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif WiFiClient ipClient; PubSubClient mqtt(mqttServer, mqttPort, ipClient); unsigned long mqttPreviousTime; @@ -167,7 +183,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi...")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -197,7 +213,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -210,7 +226,7 @@ void loop() { else mqtt.publish(mqttStatusTopic, mqttLwtMessage, true); } - // Sends the access code when needed by the panel for arming + // Sends the access code when needed by the panel for arming or command outputs if (dsc.accessCodePrompt) { dsc.accessCodePrompt = false; dsc.write(accessCode); @@ -255,7 +271,7 @@ void loop() { dsc.exitDelayChanged[partition] = false; // Resets the exit delay status flag // Disarmed during exit delay - else if (!dsc.exitDelay[partition] && !dsc.armed[partition]) { + if (!dsc.exitDelay[partition] && !dsc.armed[partition]) { publishState(mqttPartitionTopic, partition, "D"); } } @@ -460,6 +476,7 @@ void publishMessage(const char* sourceTopic, byte partition) { case 0x03: mqtt.publish(publishTopic, "Zones open", true); break; case 0x04: mqtt.publish(publishTopic, "Armed stay", true); break; case 0x05: mqtt.publish(publishTopic, "Armed away", true); break; + case 0x06: mqtt.publish(publishTopic, "No entry delay", true); break; case 0x07: mqtt.publish(publishTopic, "Failed to arm", true); break; case 0x08: mqtt.publish(publishTopic, "Exit delay", true); break; case 0x09: mqtt.publish(publishTopic, "No entry delay", true); break; @@ -469,7 +486,7 @@ void publishMessage(const char* sourceTopic, byte partition) { case 0x10: mqtt.publish(publishTopic, "Keypad lockout", true); break; case 0x11: mqtt.publish(publishTopic, "Alarm", true); break; case 0x14: mqtt.publish(publishTopic, "Auto-arm", true); break; - case 0x15: mqtt.publish(publishTopic, "Arm with bypass"); break; + case 0x15: mqtt.publish(publishTopic, "Arm with bypass", true); break; case 0x16: mqtt.publish(publishTopic, "No entry delay", true); break; case 0x22: mqtt.publish(publishTopic, "Alarm memory", true); break; case 0x33: mqtt.publish(publishTopic, "Busy", true); break; @@ -525,4 +542,3 @@ void publishMessage(const char* sourceTopic, byte partition) { default: return; } } - diff --git a/examples/esp32/Pushbullet/Pushbullet.ino b/examples/esp32/Pushbullet/Pushbullet.ino index bf5deb1..c29d2d2 100644 --- a/examples/esp32/Pushbullet/Pushbullet.ino +++ b/examples/esp32/Pushbullet/Pushbullet.ino @@ -1,5 +1,5 @@ /* - * Pushbullet Push Notification 1.4 (esp32) + * Pushbullet Push Notification 1.5 (esp32) * * Processes the security system status and demonstrates how to send a push notification when the status has changed. * This example sends notifications via Pushbullet: https://www.pushbullet.com @@ -11,6 +11,8 @@ * 4. Upload the sketch. * * Release notes: + * 1.5 - Update HTTPS root certificate for api.pushbullet.com + * Added DSC Classic series support * 1.4 - Add HTTPS certificate validation, add customizable message prefix * 1.0 - Initial release * @@ -19,23 +21,21 @@ * * DSC Aux(-) --- esp32 Ground * - * +--- dscClockPin (esp32: 4,13,16-39) + * +--- dscClockPin // Default: 18 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp32: 4,13,16-39) + * +--- dscReadPin // Default: 19 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): - * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp32: 4,13,16-33) - * Ground --- NPN emitter --/ + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground * - * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should - * be suitable, for example: - * -- 2N3904 - * -- BC547, BC548, BC549 * * Issues and (especially) pull requests are welcome: * https://github.com/taligentx/dscKeybusInterface @@ -43,6 +43,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include @@ -53,40 +56,51 @@ const char* pushbulletToken = ""; // Set the access token generated in the Push const char* messagePrefix = "[Security system] "; // Set a prefix for all messages // Configures the Keybus interface with the specified pins. -#define dscClockPin 18 // esp32: 4,13,16-39 -#define dscReadPin 19 // esp32: 4,13,16-39 +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 -// HTTPS root certificate for api.pushbullet.com: GlobalSign Root CA - R2, expires 2021.12.15 +// HTTPS root certificate for api.pushbullet.com: Google Trust Services GTS Root R1, expires 2036.06.21 const char pushbulletCertificateRoot[] = R"=EOF=( -----BEGIN CERTIFICATE----- -MIIESjCCAzKgAwIBAgINAeO0nXfN9AwGGRa24zANBgkqhkiG9w0BAQsFADBMMSAw -HgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFs -U2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjAeFw0xNzA2MTUwMDAwNDJaFw0yMTEy -MTUwMDAwNDJaMEIxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVHb29nbGUgVHJ1c3Qg -U2VydmljZXMxEzARBgNVBAMTCkdUUyBDQSAxRDIwggEiMA0GCSqGSIb3DQEBAQUA -A4IBDwAwggEKAoIBAQCy2Xvh4dc/HJFy//kQzYcVeXS3PkeLsmFV/Qw2xn53Qjqy -+lJbC3GB1k3V6SskTSNeiytyXyFVtSnvRMvrglKrPiekkklBSt6o3THgPN9tek0t -1m0JsA7jYfKy/pBsWnsQZEm0CzwI8up5DGymGolqVjKgKaIwgo+BUQzzornZdbki -nicUukovLGNYh/FdEOZfkbu5W8xH4h51toyPzHVdVwXngsaEDnRyKss7VfVucOtm -acMkuziTNZtoYS+b1q6md3J8cUhYMxCv6YCCHbUHQBv2PeyirUedtJQpNLOML80l -A1g1wCWkVV/hswdWPcjQY7gg+4wdQyz4+anV7G+XAgMBAAGjggEzMIIBLzAOBgNV -HQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBIGA1Ud -EwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLHdMl3otzdy0s5czib+R3niAQjpMB8G -A1UdIwQYMBaAFJviB1dnHB7AagbeWbSaLd/cGYYuMDUGCCsGAQUFBwEBBCkwJzAl -BggrBgEFBQcwAYYZaHR0cDovL29jc3AucGtpLmdvb2cvZ3NyMjAyBgNVHR8EKzAp -MCegJaAjhiFodHRwOi8vY3JsLnBraS5nb29nL2dzcjIvZ3NyMi5jcmwwPwYDVR0g -BDgwNjA0BgZngQwBAgEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly9wa2kuZ29vZy9y -ZXBvc2l0b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAcUrEwyOu9+OyAnmME+hTjoDF -8OPvcWCpqXs0ZYU0vUc7A1cWAJlIOuDg8OrNtkg81aty8NAby2QtOw10aNd0iDF8 -aroO8IxNeM7aEPSKlkWXqZetxTUaGGTok7YNnR+5Xh2A6udbnI6uDqaE0tEXzrP7 -9oFPPOZon8/xpnbFfafz3X1YD+D2YQEcUY52MytInVyBUXIIF7r9AdPuRvn0smhA -mTEBbE8bxlbrgXPSeVIFkiZbcc2dxNLOI3cPQXppXiElxvi3/3r3R97CAHucWkWc -Kk5GkNl1LNj/jO7M3GnrbOYV0KP/SAusVd/fJZ1CtlGjZpVgxdAi5yJ6UaXMhw== +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c -----END CERTIFICATE----- )=EOF="; // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin); +#endif WiFiClientSecure ipClient; bool wifiConnected = true; @@ -97,7 +111,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi...")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); ipClient.setCACert(pushbulletCertificateRoot); @@ -108,7 +122,7 @@ void setup() { Serial.print(F("connected: ")); Serial.println(WiFi.localIP()); - Serial.print(F("NTP time...")); + Serial.print(F("NTP time....")); configTime(0, 0, "pool.ntp.org"); time_t now = time(nullptr); while (now < 24 * 3600) @@ -151,7 +165,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -173,11 +187,11 @@ void loop() { // Checks armed status if (dsc.armedChanged[partition]) { if (dsc.armed[partition]) { - char messageContent[25]; + char messageContent[30]; - if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night: Partition "); + if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night away: Partition "); else if (dsc.armedAway[partition]) strcpy(messageContent, "Armed away: Partition "); - else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night: Partition "); + else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night stay: Partition "); else if (dsc.armedStay[partition]) strcpy(messageContent, "Armed stay: Partition "); appendPartition(partition, messageContent); // Appends the message with the partition number @@ -313,9 +327,10 @@ void loop() { } -bool sendMessage(const char* pushMessage) { +bool sendMessage(const char* messageContent) { + ipClient.setHandshakeTimeout(30); // Workaround for https://github.com/espressif/arduino-esp32/issues/6165 - // Connects and sends the message as JSON + // Connects and sends the message as a Pushbullet note-type push if (!ipClient.connect("api.pushbullet.com", 443)) return false; ipClient.println(F("POST /v2/pushes HTTP/1.1")); ipClient.println(F("Host: api.pushbullet.com")); @@ -323,13 +338,13 @@ bool sendMessage(const char* pushMessage) { ipClient.println(F("Accept: */*")); ipClient.println(F("Content-Type: application/json")); ipClient.print(F("Content-Length: ")); - ipClient.println(strlen(pushMessage) + strlen(messagePrefix) + 25); // Length including JSON data + ipClient.println(strlen(messagePrefix) + strlen(messageContent) + 25); ipClient.print(F("Access-Token: ")); ipClient.println(pushbulletToken); ipClient.println(); ipClient.print(F("{\"body\":\"")); ipClient.print(messagePrefix); - ipClient.print(pushMessage); + ipClient.print(messageContent); ipClient.print(F("\",\"type\":\"note\"}")); // Waits for a response @@ -337,6 +352,7 @@ bool sendMessage(const char* pushMessage) { while (!ipClient.available()) { dsc.loop(); if (millis() - previousMillis > 3000) { + Serial.println(); Serial.println(F("Connection timed out waiting for a response.")); ipClient.stop(); return false; @@ -361,6 +377,7 @@ bool sendMessage(const char* pushMessage) { // Unsuccessful, prints the response to serial to help debug else { + Serial.println(); Serial.println(F("Push notification error, response:")); Serial.print(statusCode); while (ipClient.available()) Serial.print((char)ipClient.read()); diff --git a/examples/esp32/Pushover/Pushover.ino b/examples/esp32/Pushover/Pushover.ino new file mode 100644 index 0000000..c27d490 --- /dev/null +++ b/examples/esp32/Pushover/Pushover.ino @@ -0,0 +1,387 @@ +/* + * Pushover Push Notification 1.0 (esp32) + * + * Processes the security system status and demonstrates how to send a push notification when the status has changed. + * This example sends notifications via Pushover: https://www.pushover.net + * + * Usage: + * 1. Set the WiFi SSID and password in the sketch. + * 2. Create a Pushover account: https://www.pushover.net + * 3. Copy the user key to pushoverUserKey. + * 4. Create a Pushover application to get an API token: https://pushover.net/apps/build + * 5. Copy the API token to pushoverAPIToken. + * 6. Upload the sketch. + * + * Release notes: + * 1.0 - Initial release + * + * Wiring: + * DSC Aux(+) --- 5v voltage regulator --- esp32 development board 5v pin + * + * DSC Aux(-) --- esp32 Ground + * + * +--- dscClockPin // Default: 18 + * DSC Yellow --- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * +--- dscReadPin // Default: 19 + * DSC Green ---- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * + * Issues and (especially) pull requests are welcome: + * https://github.com/taligentx/dscKeybusInterface + * + * This example code is in the public domain. + */ + +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + +#include +#include + +// Settings +const char* wifiSSID = ""; +const char* wifiPassword = ""; +const char* pushoverUserKey = ""; // Set the user key generated in the Pushover account settings +const char* pushoverAPIToken = ""; // Set the API token generated in the Pushover account settings +const char* messagePrefix = "[Security system] "; // Set a prefix for all messages + +// Configures the Keybus interface with the specified pins. +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 + +// HTTPS root certificate for api.pushover.net: DigiCert Global Root CA, expires 2031.11.10 +const char pushoverCertificateRoot[] = R"=EOF=( +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- +)=EOF="; + +// Initialize components +#ifndef dscClassicSeries +dscKeybusInterface dsc(dscClockPin, dscReadPin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin); +#endif +WiFiClientSecure ipClient; +bool wifiConnected = true; + + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println(); + Serial.println(); + + Serial.print(F("WiFi....")); + WiFi.mode(WIFI_STA); + WiFi.begin(wifiSSID, wifiPassword); + ipClient.setCACert(pushoverCertificateRoot); + while (WiFi.status() != WL_CONNECTED) { + Serial.print("."); + delay(500); + } + Serial.print(F("connected: ")); + Serial.println(WiFi.localIP()); + + Serial.print(F("NTP time....")); + configTime(0, 0, "pool.ntp.org"); + time_t now = time(nullptr); + while (now < 24 * 3600) + { + Serial.print("."); + delay(2000); + now = time(nullptr); + } + Serial.println(F("synchronized.")); + + // Sends a push notification on startup to verify connectivity + Serial.print(F("Pushover....")); + if (sendMessage("Initializing")) Serial.println(F("connected.")); + else Serial.println(F("connection error.")); + + // Starts the Keybus interface + dsc.begin(); + Serial.println(F("DSC Keybus Interface is online.")); +} + + +void loop() { + + // Updates status if WiFi drops and reconnects + if (!wifiConnected && WiFi.status() == WL_CONNECTED) { + Serial.println("WiFi reconnected"); + wifiConnected = true; + dsc.pauseStatus = false; + dsc.statusChanged = true; + } + else if (WiFi.status() != WL_CONNECTED && wifiConnected) { + Serial.println("WiFi disconnected"); + wifiConnected = false; + dsc.pauseStatus = true; + } + + dsc.loop(); + + if (dsc.statusChanged) { // Checks if the security system status has changed + dsc.statusChanged = false; // Reset the status tracking flag + + // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h + if (dsc.bufferOverflow) { + Serial.println(F("Keybus buffer overflow")); + dsc.bufferOverflow = false; + } + + // Checks if the interface is connected to the Keybus + if (dsc.keybusChanged) { + dsc.keybusChanged = false; // Resets the Keybus data status flag + if (dsc.keybusConnected) sendMessage("Connected"); + else sendMessage("Disconnected"); + } + + // Checks status per partition + for (byte partition = 0; partition < dscPartitions; partition++) { + + // Skips processing if the partition is disabled or in installer programming + if (dsc.disabled[partition]) continue; + + // Checks armed status + if (dsc.armedChanged[partition]) { + if (dsc.armed[partition]) { + char messageContent[30]; + + if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night away: Partition "); + else if (dsc.armedAway[partition]) strcpy(messageContent, "Armed away: Partition "); + else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night stay: Partition "); + else if (dsc.armedStay[partition]) strcpy(messageContent, "Armed stay: Partition "); + + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else { + char pushMessage[22] = "Disarmed: Partition "; + appendPartition(partition, pushMessage); // Appends the push message with the partition number + sendMessage(pushMessage); + } + } + + // Checks exit delay status + if (dsc.exitDelayChanged[partition]) { + dsc.exitDelayChanged[partition] = false; // Resets the exit delay status flag + + if (dsc.exitDelay[partition]) { + char messageContent[36] = "Exit delay in progress: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else if (!dsc.exitDelay[partition] && !dsc.armed[partition]) { + char messageContent[22] = "Disarmed: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + + // Checks alarm triggered status + if (dsc.alarmChanged[partition]) { + dsc.alarmChanged[partition] = false; // Resets the partition alarm status flag + + if (dsc.alarm[partition]) { + char messageContent[19] = "Alarm: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else if (!dsc.armedChanged[partition]) { + char messageContent[22] = "Disarmed: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + dsc.armedChanged[partition] = false; // Resets the partition armed status flag + + // Checks fire alarm status + if (dsc.fireChanged[partition]) { + dsc.fireChanged[partition] = false; // Resets the fire status flag + + if (dsc.fire[partition]) { + char messageContent[24] = "Fire alarm: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else { + char messageContent[33] = "Fire alarm restored: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + } + + // Checks for zones in alarm + // Zone alarm status is stored in the alarmZones[] and alarmZonesChanged[] arrays using 1 bit per zone, up to 64 zones + // alarmZones[0] and alarmZonesChanged[0]: Bit 0 = Zone 1 ... Bit 7 = Zone 8 + // alarmZones[1] and alarmZonesChanged[1]: Bit 0 = Zone 9 ... Bit 7 = Zone 16 + // ... + // alarmZones[7] and alarmZonesChanged[7]: Bit 0 = Zone 57 ... Bit 7 = Zone 64 + if (dsc.alarmZonesStatusChanged) { + dsc.alarmZonesStatusChanged = false; // Resets the alarm zones status flag + for (byte zoneGroup = 0; zoneGroup < dscZones; zoneGroup++) { + for (byte zoneBit = 0; zoneBit < 8; zoneBit++) { + if (bitRead(dsc.alarmZonesChanged[zoneGroup], zoneBit)) { // Checks an individual alarm zone status flag + bitWrite(dsc.alarmZonesChanged[zoneGroup], zoneBit, 0); // Resets the individual alarm zone status flag + if (bitRead(dsc.alarmZones[zoneGroup], zoneBit)) { // Zone alarm + char pushMessage[15] = "Zone alarm: "; + char zoneNumber[3]; + itoa((zoneBit + 1 + (zoneGroup * 8)), zoneNumber, 10); // Determines the zone number + strcat(pushMessage, zoneNumber); + sendMessage(pushMessage); + } + else { + char pushMessage[24] = "Zone alarm restored: "; + char zoneNumber[3]; + itoa((zoneBit + 1 + (zoneGroup * 8)), zoneNumber, 10); // Determines the zone number + strcat(pushMessage, zoneNumber); + sendMessage(pushMessage); + } + } + } + } + } + + // Checks trouble status + if (dsc.troubleChanged) { + dsc.troubleChanged = false; // Resets the trouble status flag + if (dsc.trouble) sendMessage("Trouble status on"); + else sendMessage("Trouble status restored"); + } + + // Checks for AC power status + if (dsc.powerChanged) { + dsc.powerChanged = false; // Resets the battery trouble status flag + if (dsc.powerTrouble) sendMessage("AC power trouble"); + else sendMessage("AC power restored"); + } + + // Checks panel battery status + if (dsc.batteryChanged) { + dsc.batteryChanged = false; // Resets the battery trouble status flag + if (dsc.batteryTrouble) sendMessage("Panel battery trouble"); + else sendMessage("Panel battery restored"); + } + + // Checks for keypad fire alarm status + if (dsc.keypadFireAlarm) { + dsc.keypadFireAlarm = false; // Resets the keypad fire alarm status flag + sendMessage("Keypad Fire alarm"); + } + + // Checks for keypad aux auxiliary alarm status + if (dsc.keypadAuxAlarm) { + dsc.keypadAuxAlarm = false; // Resets the keypad auxiliary alarm status flag + sendMessage("Keypad Aux alarm"); + } + + // Checks for keypad panic alarm status + if (dsc.keypadPanicAlarm) { + dsc.keypadPanicAlarm = false; // Resets the keypad panic alarm status flag + sendMessage("Keypad Panic alarm"); + } + } +} + + +bool sendMessage(const char* messageContent) { + ipClient.setHandshakeTimeout(30); // Workaround for https://github.com/espressif/arduino-esp32/issues/6165 + + if (!ipClient.connect("api.pushover.net", 443)) return false; + ipClient.println(F("POST /1/messages.json HTTP/1.1")); + ipClient.println(F("Host: api.pushover.net")); + ipClient.println(F("User-Agent: ESP32")); + ipClient.println(F("Accept: */*")); + ipClient.println(F("Content-Type: application/json")); + ipClient.print(F("Content-Length: ")); + ipClient.println(strlen(pushoverAPIToken) + strlen(pushoverUserKey) + strlen(messagePrefix) + strlen(messageContent) + 35); + ipClient.println(); + ipClient.print(F("{\"token\":\"")); + ipClient.print(pushoverAPIToken); + ipClient.print(F("\",\"user\":\"")); + ipClient.print(pushoverUserKey); + ipClient.print(F("\",\"message\":\"")); + ipClient.print(messagePrefix); + ipClient.print(messageContent); + ipClient.print(F("\"}")); + + // Waits for a response + unsigned long previousMillis = millis(); + while (!ipClient.available()) { + dsc.loop(); + if (millis() - previousMillis > 3000) { + Serial.println(); + Serial.println(F("Connection timed out waiting for a response.")); + ipClient.stop(); + return false; + } + } + + // Reads the response until the first space - the next characters will be the HTTP status code + while (ipClient.available()) { + if (ipClient.read() == ' ') break; + } + + // Checks the first character of the HTTP status code - the message was sent successfully if the status code + // begins with "2" + char statusCode = ipClient.read(); + + // Successful, reads the remaining response to clear the client buffer + if (statusCode == '2') { + while (ipClient.available()) ipClient.read(); + ipClient.stop(); + return true; + } + + // Unsuccessful, prints the response to serial to help debug + else { + Serial.println(); + Serial.println(F("Push notification error, response:")); + Serial.print(statusCode); + while (ipClient.available()) Serial.print((char)ipClient.read()); + Serial.println(); + ipClient.stop(); + return false; + } +} + + +void appendPartition(byte sourceNumber, char* pushMessage) { + char partitionNumber[2]; + itoa(sourceNumber + 1, partitionNumber, 10); + strcat(pushMessage, partitionNumber); +} diff --git a/examples/esp32/Pushsafer/Pushsafer.ino b/examples/esp32/Pushsafer/Pushsafer.ino new file mode 100644 index 0000000..10d34ca --- /dev/null +++ b/examples/esp32/Pushsafer/Pushsafer.ino @@ -0,0 +1,422 @@ +/* + * Pushsafer Push Notification 1.0 (esp32) + * + * Processes the security system status and demonstrates how to send a push notification when the status has changed. + * This example sends notifications via Pushsafer: https://www.pushsafer.com + * + * Usage: + * 1. Set the WiFi SSID and password in the sketch. + * 2. Create a Pushsafer account: https://www.pushsafer.com + * 3. Copy the private key to pushsaferKey. + * 4. Upload the sketch. + * + * Release notes: + * 1.0 - Initial release + * + * Wiring: + * DSC Aux(+) --- 5v voltage regulator --- esp32 development board 5v pin + * + * DSC Aux(-) --- esp32 Ground + * + * +--- dscClockPin // Default: 18 + * DSC Yellow --- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * +--- dscReadPin // Default: 19 + * DSC Green ---- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Issues and (especially) pull requests are welcome: + * https://github.com/taligentx/dscKeybusInterface + * + * This example code is in the public domain. + */ + +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + +#include +#include + +// Settings +const char* wifiSSID = ""; +const char* wifiPassword = ""; +const char* pushsaferKey = ""; // Set the private key generated in the Pushsafer account settings +const char* messagePrefix = "[Security system] "; // Set a prefix for all messages + +// Configures the Keybus interface with the specified pins. +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 + +// HTTPS root certificate for www.pushsafer.com: ISRG Root X1, expires 2035.06.04 +const char pushsaferCertificateRoot[] = R"=EOF=( +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- +)=EOF="; + +// Initialize components +#ifndef dscClassicSeries +dscKeybusInterface dsc(dscClockPin, dscReadPin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin); +#endif +WiFiClientSecure ipClient; +bool wifiConnected = true; +char encodedMessagePrefix[128], encodedMessageContent[480]; + + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println(); + Serial.println(); + + Serial.print(F("WiFi....")); + WiFi.mode(WIFI_STA); + WiFi.begin(wifiSSID, wifiPassword); + ipClient.setCACert(pushsaferCertificateRoot); + while (WiFi.status() != WL_CONNECTED) { + Serial.print("."); + delay(500); + } + Serial.print(F("connected: ")); + Serial.println(WiFi.localIP()); + + Serial.print(F("NTP time....")); + configTime(0, 0, "pool.ntp.org"); + time_t now = time(nullptr); + while (now < 24 * 3600) + { + Serial.print("."); + delay(2000); + now = time(nullptr); + } + Serial.println(F("synchronized.")); + + // Encodes message prefix in URL encoding + encodeURL(messagePrefix, encodedMessagePrefix); + + // Sends a message on startup to verify connectivity + Serial.print(F("Pushsafer....")); + if (sendMessage("Initializing")) Serial.println(F("connected.")); + else Serial.println(F("connection error.")); + + // Starts the Keybus interface + dsc.begin(); + Serial.println(F("DSC Keybus Interface is online.")); +} + + +void loop() { + + // Updates status if WiFi drops and reconnects + if (!wifiConnected && WiFi.status() == WL_CONNECTED) { + Serial.println("WiFi reconnected"); + wifiConnected = true; + dsc.pauseStatus = false; + dsc.statusChanged = true; + } + else if (WiFi.status() != WL_CONNECTED && wifiConnected) { + Serial.println("WiFi disconnected"); + wifiConnected = false; + dsc.pauseStatus = true; + } + + dsc.loop(); + + if (dsc.statusChanged) { // Checks if the security system status has changed + dsc.statusChanged = false; // Reset the status tracking flag + + // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h + if (dsc.bufferOverflow) { + Serial.println(F("Keybus buffer overflow")); + dsc.bufferOverflow = false; + } + + // Checks if the interface is connected to the Keybus + if (dsc.keybusChanged) { + dsc.keybusChanged = false; // Resets the Keybus data status flag + if (dsc.keybusConnected) sendMessage("Connected"); + else sendMessage("Disconnected"); + } + + // Checks status per partition + for (byte partition = 0; partition < dscPartitions; partition++) { + + // Skips processing if the partition is disabled or in installer programming + if (dsc.disabled[partition]) continue; + + // Checks armed status + if (dsc.armedChanged[partition]) { + if (dsc.armed[partition]) { + char messageContent[30]; + + if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night away: Partition "); + else if (dsc.armedAway[partition]) strcpy(messageContent, "Armed away: Partition "); + else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night stay: Partition "); + else if (dsc.armedStay[partition]) strcpy(messageContent, "Armed stay: Partition "); + + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else { + char messageContent[22] = "Disarmed: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + + // Checks exit delay status + if (dsc.exitDelayChanged[partition]) { + dsc.exitDelayChanged[partition] = false; // Resets the exit delay status flag + + if (dsc.exitDelay[partition]) { + char messageContent[36] = "Exit delay in progress: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else if (!dsc.exitDelay[partition] && !dsc.armed[partition]) { + char messageContent[22] = "Disarmed: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + + // Checks alarm triggered status + if (dsc.alarmChanged[partition]) { + dsc.alarmChanged[partition] = false; // Resets the partition alarm status flag + + if (dsc.alarm[partition]) { + char messageContent[19] = "Alarm: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else if (!dsc.armedChanged[partition]) { + char messageContent[22] = "Disarmed: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + dsc.armedChanged[partition] = false; // Resets the partition armed status flag + + // Checks fire alarm status + if (dsc.fireChanged[partition]) { + dsc.fireChanged[partition] = false; // Resets the fire status flag + + if (dsc.fire[partition]) { + char messageContent[24] = "Fire alarm: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else { + char messageContent[33] = "Fire alarm restored: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + } + + // Checks for zones in alarm + // Zone alarm status is stored in the alarmZones[] and alarmZonesChanged[] arrays using 1 bit per zone, up to 64 zones + // alarmZones[0] and alarmZonesChanged[0]: Bit 0 = Zone 1 ... Bit 7 = Zone 8 + // alarmZones[1] and alarmZonesChanged[1]: Bit 0 = Zone 9 ... Bit 7 = Zone 16 + // ... + // alarmZones[7] and alarmZonesChanged[7]: Bit 0 = Zone 57 ... Bit 7 = Zone 64 + if (dsc.alarmZonesStatusChanged) { + dsc.alarmZonesStatusChanged = false; // Resets the alarm zones status flag + for (byte zoneGroup = 0; zoneGroup < dscZones; zoneGroup++) { + for (byte zoneBit = 0; zoneBit < 8; zoneBit++) { + if (bitRead(dsc.alarmZonesChanged[zoneGroup], zoneBit)) { // Checks an individual alarm zone status flag + bitWrite(dsc.alarmZonesChanged[zoneGroup], zoneBit, 0); // Resets the individual alarm zone status flag + if (bitRead(dsc.alarmZones[zoneGroup], zoneBit)) { // Zone alarm + char messageContent[15] = "Zone alarm: "; + char zoneNumber[3]; + itoa((zoneBit + 1 + (zoneGroup * 8)), zoneNumber, 10); // Determines the zone number + strcat(messageContent, zoneNumber); + sendMessage(messageContent); + } + else { + char messageContent[24] = "Zone alarm restored: "; + char zoneNumber[3]; + itoa((zoneBit + 1 + (zoneGroup * 8)), zoneNumber, 10); // Determines the zone number + strcat(messageContent, zoneNumber); + sendMessage(messageContent); + } + } + } + } + } + + // Checks trouble status + if (dsc.troubleChanged) { + dsc.troubleChanged = false; // Resets the trouble status flag + if (dsc.trouble) sendMessage("Trouble status on"); + else sendMessage("Trouble status restored"); + } + + // Checks for AC power status + if (dsc.powerChanged) { + dsc.powerChanged = false; // Resets the battery trouble status flag + if (dsc.powerTrouble) sendMessage("AC power trouble"); + else sendMessage("AC power restored"); + } + + // Checks panel battery status + if (dsc.batteryChanged) { + dsc.batteryChanged = false; // Resets the battery trouble status flag + if (dsc.batteryTrouble) sendMessage("Panel battery trouble"); + else sendMessage("Panel battery restored"); + } + + // Checks for keypad fire alarm status + if (dsc.keypadFireAlarm) { + dsc.keypadFireAlarm = false; // Resets the keypad fire alarm status flag + sendMessage("Keypad Fire alarm"); + } + + // Checks for keypad aux auxiliary alarm status + if (dsc.keypadAuxAlarm) { + dsc.keypadAuxAlarm = false; // Resets the keypad auxiliary alarm status flag + sendMessage("Keypad Aux alarm"); + } + + // Checks for keypad panic alarm status + if (dsc.keypadPanicAlarm) { + dsc.keypadPanicAlarm = false; // Resets the keypad panic alarm status flag + sendMessage("Keypad Panic alarm"); + } + } +} + + +bool sendMessage(const char* messageContent) { + ipClient.setHandshakeTimeout(30); // Workaround for https://github.com/espressif/arduino-esp32/issues/6165 + encodeURL(messageContent, encodedMessageContent); // Encodes message content in URL encoding + + if (!ipClient.connect("www.pushsafer.com", 443)) return false; + ipClient.println(F("POST /api HTTP/1.1")); + ipClient.println(F("Host: www.pushsafer.com")); + ipClient.println(F("User-Agent: ESP32")); + ipClient.println(F("Accept: */*")); + ipClient.println(F("Content-Type: application/x-www-form-urlencoded")); + ipClient.print(F("Content-Length: ")); + ipClient.println(strlen(pushsaferKey) + strlen(encodedMessagePrefix) + strlen(encodedMessageContent) + 5); + ipClient.println(); + ipClient.print(F("k=")); + ipClient.print(pushsaferKey); + ipClient.print(F("&m=")); + ipClient.print(encodedMessagePrefix); + ipClient.print(encodedMessageContent); + + // Waits for a response + unsigned long previousMillis = millis(); + while (!ipClient.available()) { + dsc.loop(); + if (millis() - previousMillis > 3000) { + Serial.println(); + Serial.println(F("Connection timed out waiting for a response.")); + ipClient.stop(); + return false; + } + } + + // Reads the response until the first space - the next characters will be the HTTP status code + while (ipClient.available()) { + if (ipClient.read() == ' ') break; + } + + // Checks the first character of the HTTP status code - the message was sent successfully if the status code + // begins with "2" + char statusCode = ipClient.read(); + + // Successful, reads the remaining response to clear the client buffer + if (statusCode == '2') { + while (ipClient.available()) ipClient.read(); + ipClient.stop(); + return true; + } + + // Unsuccessful, prints the response to serial to help debug + else { + Serial.println(); + Serial.println(F("Push notification error, response:")); + Serial.print(statusCode); + while (ipClient.available()) Serial.print((char)ipClient.read()); + Serial.println(); + ipClient.stop(); + return false; + } +} + + +void appendPartition(byte sourceNumber, char* messageContent) { + char partitionNumber[2]; + itoa(sourceNumber + 1, partitionNumber, 10); + strcat(messageContent, partitionNumber); +} + + +// Helper for encodeURL() +static char encodeHex(char c) { + return "0123456789ABCDEF"[c & 0x0F]; +} + + +// Encodes a char array to URL encoded using '+' for spaces as required for application/x-www-form-urlencoded +char *encodeURL(const char *src, char *dst) { + char c, *d = dst; + while (c = *src++) { + if (c == ' ') { + *d++ = '+'; + continue; + } + else if (!('a' <= c && c <= 'z') + && !('A' <= c && c <= 'Z') + && !('0' <= c && c <= '9')) { + *d++ = '%'; + *d++ = encodeHex(c >> 4); + c = encodeHex(c); + } + *d++ = c; + } + *d = '\0'; + return dst; +} diff --git a/examples/esp32/Status/Status.ino b/examples/esp32/Status/Status.ino index 783241c..da18c26 100644 --- a/examples/esp32/Status/Status.ino +++ b/examples/esp32/Status/Status.ino @@ -1,11 +1,12 @@ /* - * DSC Status 1.3 (esp32) + * DSC Status 1.4 (esp32) * * Processes and prints the security system status to a serial interface, including reading from serial for the * virtual keypad. This demonstrates how to determine if the security system status has changed, what has * changed, and how to take action based on those changes. * * Release notes: + * 1.4 - Added DSC Classic series support * 1.3 - Added PGM outputs 1-14 status * 1.0 - Initial release * @@ -14,17 +15,24 @@ * * DSC Aux(-) --- esp32 Ground * - * +--- dscClockPin (esp32: 4,13,16-39) + * +--- dscClockPin // Default: 18 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp32: 4,13,16-39) + * +--- dscReadPin // Default: 19 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp32: 4,13,16-33) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: 21 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -38,16 +46,24 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. -#define dscClockPin 18 // esp32: 4,13,16-39 -#define dscReadPin 19 // esp32: 4,13,16-39 -#define dscWritePin 21 // esp32: 4,13,16-33 +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 +#define dscWritePin 21 // 4,13,16-33 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif void setup() { @@ -74,7 +90,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -96,7 +112,7 @@ void loop() { if (dsc.disabled[partition]) { Serial.print(F("Partition ")); Serial.print(partition + 1); - Serial.println(F(" disabled")); + Serial.println(F(": Disabled")); } } if (dsc.disabled[partition]) continue; diff --git a/examples/esp32/Telegram/Telegram.ino b/examples/esp32/Telegram/Telegram.ino index 67b04b4..e0c35d3 100644 --- a/examples/esp32/Telegram/Telegram.ino +++ b/examples/esp32/Telegram/Telegram.ino @@ -1,5 +1,5 @@ /* - * Telegram Bot 1.0 (esp32) + * Telegram Bot 1.2 (esp32) * * Processes the security system status and allows for control via a Telegram bot: https://www.telegram.org * @@ -27,6 +27,8 @@ * - Disarm: /disarm * * Release notes: + * 1.2 - Workaround for upstream esp32 TLS handshake issue https://github.com/espressif/arduino-esp32/issues/6165 + * 1.1 - Added DSC Classic series support * 1.0 - Initial release * * Wiring: @@ -34,17 +36,24 @@ * * DSC Aux(-) --- esp32 Ground * - * +--- dscClockPin (esp32: 4,13,16-39) + * +--- dscClockPin // Default: 18 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp32: 4,13,16-39) + * +--- dscReadPin // Default: 19 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp32: 4,13,16-33) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: 21 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -58,6 +67,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -67,18 +79,23 @@ // Settings const char* wifiSSID = ""; const char* wifiPassword = ""; -const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm (based on panel configuration) +const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm or enable command outputs based on panel configuration. const char* telegramBotToken = ""; // Set the Telegram bot access token const char* telegramUserID = ""; // Set the Telegram chat user ID const char* messagePrefix = "[Security system] "; // Set a prefix for all messages // Configures the Keybus interface with the specified pins. -#define dscClockPin 18 // esp32: 4,13,16-39 -#define dscReadPin 19 // esp32: 4,13,16-39 -#define dscWritePin 21 // esp32: 4,13,16-33 +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 +#define dscWritePin 21 // 4,13,16-33 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif WiFiClientSecure ipClient; UniversalTelegramBot telegramBot(telegramBotToken, ipClient); const int telegramCheckInterval = 1000; @@ -90,7 +107,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi...")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); ipClient.setCACert(TELEGRAM_CERTIFICATE_ROOT); @@ -101,7 +118,7 @@ void setup() { Serial.print(F("connected: ")); Serial.println(WiFi.localIP()); - Serial.print(F("NTP time...")); + Serial.print(F("NTP time....")); configTime(0, 0, "pool.ntp.org"); time_t now = time(nullptr); while (now < 24 * 3600) @@ -141,6 +158,8 @@ void loop() { // Checks for incoming Telegram messages static unsigned long telegramPreviousTime; if (millis() - telegramPreviousTime > telegramCheckInterval) { + ipClient.setHandshakeTimeout(30); // Workaround for https://github.com/espressif/arduino-esp32/issues/6165 + byte telegramMessages = telegramBot.getUpdates(telegramBot.last_message_received + 1); while (telegramMessages) { handleTelegram(telegramMessages); @@ -155,7 +174,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -168,7 +187,7 @@ void loop() { else sendMessage("Disconnected"); } - // Sends the access code when needed by the panel for arming + // Sends the access code when needed by the panel for arming or command outputs if (dsc.accessCodePrompt) { dsc.accessCodePrompt = false; dsc.write(accessCode); @@ -183,11 +202,11 @@ void loop() { // Checks armed status if (dsc.armedChanged[partition]) { if (dsc.armed[partition]) { - char messageContent[25]; + char messageContent[30]; - if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night: Partition "); + if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night away: Partition "); else if (dsc.armedAway[partition]) strcpy(messageContent, "Armed away: Partition "); - else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night: Partition "); + else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night stay: Partition "); else if (dsc.armedStay[partition]) strcpy(messageContent, "Armed stay: Partition "); appendPartition(partition, messageContent); // Appends the message with the partition number @@ -372,6 +391,7 @@ void handleTelegram(byte telegramMessages) { bool sendMessage(const char* messageContent) { + ipClient.setHandshakeTimeout(30); // Workaround for https://github.com/espressif/arduino-esp32/issues/6165 byte messageLength = strlen(messagePrefix) + strlen(messageContent) + 1; char message[messageLength]; strcpy(message, messagePrefix); diff --git a/examples/esp32/TimeSyncNTP/TimeSyncNTP.ino b/examples/esp32/TimeSyncNTP/TimeSyncNTP.ino index 8533b14..d2da322 100644 --- a/examples/esp32/TimeSyncNTP/TimeSyncNTP.ino +++ b/examples/esp32/TimeSyncNTP/TimeSyncNTP.ino @@ -14,17 +14,17 @@ * * DSC Aux(-) --- esp32 Ground * - * +--- dscClockPin (esp32: 4,13,16-39) + * +--- dscClockPin // Default: 18 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp32: 4,13,16-39) + * +--- dscReadPin // Default: 19 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp32: 4,13,16-33) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: 21 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -52,9 +52,9 @@ const char* ntpServer = "pool.ntp.org"; // Set the NTP server const byte timePartition = 1; // Set the partition to use for setting the time // Configures the Keybus interface with the specified pins. -#define dscClockPin 18 // esp32: 4,13,16-39 -#define dscReadPin 19 // esp32: 4,13,16-39 -#define dscWritePin 21 // esp32: 4,13,16-33 +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscWritePin 21 // 4,13,16-33 // Initialize components dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); @@ -71,7 +71,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi...")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -81,7 +81,7 @@ void setup() { Serial.print(F("connected: ")); Serial.println(WiFi.localIP()); - Serial.print(F("NTP time...")); + Serial.print(F("NTP time....")); configTime(timeZoneOffset, daylightOffset, ntpServer); // Initiates the NTP client, synced hourly while (!getLocalTime(&ntpTime)) { Serial.print("."); @@ -136,7 +136,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; diff --git a/examples/esp32/TinyGSM-SMS/TinyGSM-SMS.ino b/examples/esp32/TinyGSM-SMS/TinyGSM-SMS.ino index 7909b35..c55a9fc 100644 --- a/examples/esp32/TinyGSM-SMS/TinyGSM-SMS.ino +++ b/examples/esp32/TinyGSM-SMS/TinyGSM-SMS.ino @@ -1,5 +1,5 @@ /* - * TinyGSM SMS Notification 1.0 (esp32) + * TinyGSM SMS Notification 1.1 (esp32) * * Processes the security system status and demonstrates how to send an SMS text message when the status has * changed. This example sends SMS text messages via a TinyGSM-compatible module which can be integrated @@ -11,6 +11,7 @@ * 2. Set the destination phone numbers in the sketch settings. * * Release notes: + * 1.1 - Added DSC Classic series support * 1.0 - Initial release * * Wiring: @@ -18,14 +19,22 @@ * * DSC Aux(-) --- esp32 Ground * - * +--- dscClockPin (esp32: 4,13,16-39) + * +--- dscClockPin // Default: 18 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp32: 4,13,16-39) + * +--- dscReadPin // Default: 19 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * * Issues and (especially) pull requests are welcome: * https://github.com/taligentx/dscKeybusInterface * @@ -34,6 +43,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + // Configures GSM modem model. Must be done before including TinyGsmClient library. #define TINY_GSM_MODEM_SIM800 @@ -56,11 +68,16 @@ const char* sendToPhoneNumbers[] = { #define MODEM_RX 26 // Configures the Keybus interface with the specified pins. -#define dscClockPin 18 // esp32: 4,13,16-39 -#define dscReadPin 19 // esp32: 4,13,16-39 +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin); +#endif TinyGsm modem(Serial1); void setup() { @@ -85,13 +102,13 @@ void setup() { Serial1.begin(115200, SERIAL_8N1, MODEM_RX, MODEM_TX); while (!modem.isNetworkConnected()) { - Serial.print(F("GSM...")); + Serial.print(F("GSM....")); while (!modem.restart()) { Serial.print("."); } Serial.println(); - Serial.print(F("Waiting for network...")); + Serial.print(F("Waiting for network....")); if (modem.waitForNetwork(600000L) && modem.isNetworkConnected()) { Serial.println(F("connected.")); } @@ -115,7 +132,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; diff --git a/examples/esp32/Twilio-SMS/Twilio-SMS.ino b/examples/esp32/Twilio-SMS/Twilio-SMS.ino index a54342f..a6e8c61 100644 --- a/examples/esp32/Twilio-SMS/Twilio-SMS.ino +++ b/examples/esp32/Twilio-SMS/Twilio-SMS.ino @@ -1,10 +1,13 @@ /* - * Twilio SMS Notification 1.0 (esp32) + * Twilio SMS Notification 1.1 (esp32) * * Processes the security system status and demonstrates how to send an SMS text message when the status has * changed. This example sends SMS text messages via Twilio: https://www.twilio.com * * Release notes: + * 1.2 - Add TLS root certificate for Twilio + * Encode authorization data in base64 directly within the sketch + * 1.1 - Added DSC Classic series support * 1.0 - Initial release * * Wiring: @@ -12,14 +15,22 @@ * * DSC Aux(-) --- esp32 Ground * - * +--- dscClockPin (esp32: 4,13,16-39) + * +--- dscClockPin // Default: 18 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp32: 4,13,16-39) + * +--- dscReadPin // Default: 19 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * * Issues and (especially) pull requests are welcome: * https://github.com/taligentx/dscKeybusInterface * @@ -28,27 +39,65 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include +#include "mbedtls/base64.h" // Settings const char* wifiSSID = ""; const char* wifiPassword = ""; -const char* AccountSID = ""; // Set the account SID from the Twilio Account Dashboard -const char* AuthToken = ""; // Set the auth token from the Twilio Account Dashboard -const char* Base64EncodedAuth = ""; // macOS/Linux terminal: $ echo -n "AccountSID:AuthToken" | base64 -w 0 -const char* From = ""; // i.e. 16041234567 -const char* To = ""; // i.e. 16041234567 +const char* AccountSID = ""; // Set the account SID from the Twilio Account Dashboard +const char* AuthToken = ""; // Set the auth token from the Twilio Account Dashboard +const char* From = ""; // From phone number, starting with the country code without the + sign: 18005551234 +const char* To = ""; // To phone number, starting with the country code without the + sign: 18005551234 const char* messagePrefix = "[Security system] "; // Set a prefix for all messages // Configures the Keybus interface with the specified pins. -#define dscClockPin 18 // esp32: 4,13,16-39 -#define dscReadPin 19 // esp32: 4,13,16-39 +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 + +// HTTPS root certificate for api.twilio.com: DigiCert Global Root CA, expires 2031.11.10 +const char twilioCertificateRoot[] = R"=EOF=( +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- +)=EOF="; // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin); +#endif WiFiClientSecure ipClient; bool wifiConnected = true; +char twilioAuth[128]; +size_t twilioAuthLength = 128; +unsigned char encodedTwilioAuth[128]; +char encodedMessagePrefix[128], encodedMessageContent[480]; void setup() { @@ -57,9 +106,10 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi...")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); + ipClient.setCACert(twilioCertificateRoot); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(500); @@ -67,6 +117,24 @@ void setup() { Serial.print(F("connected: ")); Serial.println(WiFi.localIP()); + Serial.print(F("NTP time....")); + configTime(0, 0, "pool.ntp.org"); + time_t now = time(nullptr); + while (now < 24 * 3600) + { + Serial.print("."); + delay(2000); + now = time(nullptr); + } + Serial.println(F("synchronized.")); + + // Encodes authentication in base64 and message prefix in URL encoding + strcat(twilioAuth, AccountSID); + strcat(twilioAuth, ":"); + strcat(twilioAuth, AuthToken); + mbedtls_base64_encode(encodedTwilioAuth, twilioAuthLength, &twilioAuthLength, (unsigned char*)twilioAuth, strlen(twilioAuth)); + encodeURL(messagePrefix, encodedMessagePrefix); + // Sends a message on startup to verify connectivity Serial.print(F("Twilio....")); if (sendMessage("Initializing")) Serial.println(F("connected.")); @@ -99,7 +167,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -121,11 +189,11 @@ void loop() { // Checks armed status if (dsc.armedChanged[partition]) { if (dsc.armed[partition]) { - char messageContent[25]; + char messageContent[30]; - if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night: Partition "); + if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night away: Partition "); else if (dsc.armedAway[partition]) strcpy(messageContent, "Armed away: Partition "); - else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night: Partition "); + else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night stay: Partition "); else if (dsc.armedStay[partition]) strcpy(messageContent, "Armed stay: Partition "); appendPartition(partition, messageContent); // Appends the message with the partition number @@ -262,35 +330,37 @@ void loop() { bool sendMessage(const char* messageContent) { + ipClient.setHandshakeTimeout(30); // Workaround for https://github.com/espressif/arduino-esp32/issues/6165 + encodeURL(messageContent, encodedMessageContent); // Encodes message content in URL encoding - // Connects and sends the message as x-www-form-urlencoded if (!ipClient.connect("api.twilio.com", 443)) return false; ipClient.print(F("POST https://api.twilio.com/2010-04-01/Accounts/")); ipClient.print(AccountSID); ipClient.println(F("/Messages.json HTTP/1.1")); ipClient.print(F("Authorization: Basic ")); - ipClient.println(Base64EncodedAuth); + ipClient.println((char*)encodedTwilioAuth); ipClient.println(F("Host: api.twilio.com")); ipClient.println(F("User-Agent: ESP32")); ipClient.println(F("Accept: */*")); ipClient.println(F("Content-Type: application/x-www-form-urlencoded")); ipClient.print(F("Content-Length: ")); - ipClient.println(strlen(To) + strlen(From) + strlen(messagePrefix) + strlen(messageContent) + 18); // Length including data + ipClient.println(strlen(To) + strlen(From) + strlen(encodedMessagePrefix) + strlen(encodedMessageContent) + 21); ipClient.println("Connection: Close"); ipClient.println(); - ipClient.print(F("To=+")); + ipClient.print(F("To=%2B")); ipClient.print(To); - ipClient.print(F("&From=+")); + ipClient.print(F("&From=%2B")); ipClient.print(From); ipClient.print(F("&Body=")); - ipClient.print(messagePrefix); - ipClient.println(messageContent); + ipClient.print(encodedMessagePrefix); + ipClient.print(encodedMessageContent); // Waits for a response unsigned long previousMillis = millis(); while (!ipClient.available()) { dsc.loop(); if (millis() - previousMillis > 3000) { + Serial.println(); Serial.println(F("Connection timed out waiting for a response.")); ipClient.stop(); return false; @@ -315,6 +385,7 @@ bool sendMessage(const char* messageContent) { // Unsuccessful, prints the response to serial to help debug else { + Serial.println(); Serial.println(F("SMS messaging error, response:")); Serial.print(statusCode); while (ipClient.available()) Serial.print((char)ipClient.read()); @@ -330,3 +401,31 @@ void appendPartition(byte sourceNumber, char* messageContent) { itoa(sourceNumber + 1, partitionNumber, 10); strcat(messageContent, partitionNumber); } + + +// Helper for encodeURL() +static char encodeHex(char c) { + return "0123456789ABCDEF"[c & 0x0F]; +} + + +// Encodes a char array to URL encoded using '+' for spaces as required for application/x-www-form-urlencoded +char *encodeURL(const char *src, char *dst) { + char c, *d = dst; + while (c = *src++) { + if (c == ' ') { + *d++ = '+'; + continue; + } + else if (!('a' <= c && c <= 'z') + && !('A' <= c && c <= 'Z') + && !('0' <= c && c <= '9')) { + *d++ = '%'; + *d++ = encodeHex(c >> 4); + c = encodeHex(c); + } + *d++ = c; + } + *d = '\0'; + return dst; +} diff --git a/examples/esp32/Unlocker/Unlocker.ino b/examples/esp32/Unlocker/Unlocker.ino index f505321..c382a3f 100644 --- a/examples/esp32/Unlocker/Unlocker.ino +++ b/examples/esp32/Unlocker/Unlocker.ino @@ -63,7 +63,7 @@ * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad: + * Virtual keypad: * DSC Green ---- NPN collector --\ * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp32: 4,13,16-33) * Ground --- NPN emitter --/ @@ -515,7 +515,7 @@ void setup() { // Starts the Keybus interface and optionally specifies how to print data. // begin() sets Serial by default and can accept a different stream: begin(Serial1), etc. dsc.begin(); - Serial.print(F("DSC Keybus Interface...")); + Serial.print(F("DSC Keybus Interface....")); // Loops until partition 1 is ready for key presses in status "Partition ready" (0x01), // "Stay/away zones open" (0x02), or "Zones open" (0x03) diff --git a/examples/esp32/VirtualKeypad-Blynk/VirtualKeypad-Blynk.ino b/examples/esp32/VirtualKeypad-Blynk/VirtualKeypad-Blynk.ino index f214a14..19eb0e7 100644 --- a/examples/esp32/VirtualKeypad-Blynk/VirtualKeypad-Blynk.ino +++ b/examples/esp32/VirtualKeypad-Blynk/VirtualKeypad-Blynk.ino @@ -1,12 +1,21 @@ /* - * VirtualKeypad-Blynk 1.3 (esp32) + * VirtualKeypad-Blynk 1.4 (esp32) * - * Provides a virtual keypad interface for the free Blynk (https://www.blynk.cc) app on iOS and Android, similar - * to a physical DSC LED keypad. Note that while the Blynk app has an LCD to display the partition status, the - * sketch currently does not emulate the menu navigation features of the DSC LCD keypads (PK5500, etc). + * Provides a virtual keypad interface for the free Blynk legacy (https://www.blynk.cc) app on iOS and Android, similar + * to a physical DSC LED keypad (the newer Blynk.Cloud app is not currently supported): + * + * iOS: https://apps.apple.com/us/app/blynk-0-1-legacy/id808760481 + * Android: https://play.google.com/store/apps/details?id=cc.blynk&hl=en&gl=US + * + * Installing Blynk as a local server (https://github.com/blynkkk/blynk-server) is recommended to keep control of the + * security system internal to your network. This also lets you use as many widgets as needed for free - local + * servers can setup users with any amount of Blynk Energy. + * + * Note that while the Blynk legacy app has an LCD to display the partition status, the sketch currently does + * not emulate the menu navigation features of the DSC LCD keypads (PK5500, etc). * * Usage: - * 1. Scan one of the following QR codes from within the Blynk app for an example keypad layout - as QR codes + * 1. Scan one of the following QR codes from within the Blynk legacy app for an example keypad layout - as QR codes * can contain a limited amount of objects, only the 8 and 16-zone template includes PGM outputs 1-8. Use * cloning within the Blynk app to add up to 64 zones and up to 14 PGM outputs. Some Android devices have * issues reading these QR codes and may need to be used with a different monitor/device. @@ -21,11 +30,6 @@ * 5. Add the auth token to the sketch below. * 6. Upload the sketch. * - * Installing Blynk as a local server (https://github.com/blynkkk/blynk-server) is recommended to keep control of the - * security system internal to your network. This also lets you use as many widgets as needed for free - local - * servers can setup users with any amount of Blynk Energy. Using the default Blynk cloud service with the above - * example layouts requires more of Blynk's Energy units than available on the free usage tier. - * * The Blynk layout can be customized with widgets using these virtual pin mappings: * V0 - Keypad 0 ... V9 - Keypad 9 V10 - Keypad @@ -53,6 +57,7 @@ V61 - Zone 1 ... V124 - Zone 64 * * Release notes: + * 1.4 - Added DSC Classic series support * 1.3 - Display alarm memory, programming zone lights, and event buffer * Add PGM outputs 1-14 status * 1.0 - Initial release @@ -62,17 +67,24 @@ * * DSC Aux(-) --- esp32 Ground * - * +--- dscClockPin (esp32: 4,13,16-39) + * +--- dscClockPin // Default: 18 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp32: 4,13,16-39) + * +--- dscReadPin // Default: 19 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp32: 4,13,16-33) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: 21 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -86,6 +98,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -96,18 +111,24 @@ // Settings const char* wifiSSID = ""; const char* wifiPassword = ""; +const char* accessCode = ""; // Classic series only, an access code is required to arm with the stay/away buttons. const char* blynkAuthToken = ""; // Token generated from within the Blynk app const char* blynkServer = ""; // Blynk local server address const int blynkPort = 8080; // Blynk local server port bool showLCDoutput = true; // Control if LCD programming output is displayed on VirtualPin20 // Configures the Keybus interface with the specified pins -#define dscClockPin 18 // esp32: 4,13,16-39 -#define dscReadPin 19 // esp32: 4,13,16-39 -#define dscWritePin 21 // esp32: 4,13,16-33 +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 +#define dscWritePin 21 // 4,13,16-33 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif bool wifiConnected = true; bool partitionChanged, pausedZones, extendedBuffer; bool decimalOutput, inputDigits; @@ -210,13 +231,13 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi...")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); Blynk.begin(blynkAuthToken, wifiSSID, wifiPassword, blynkServer, blynkPort); while (WiFi.status() != WL_CONNECTED) yield(); Serial.print(F("connected: ")); Serial.println(WiFi.localIP()); - Serial.print(F("Blynk...")); + Serial.print(F("Blynk....")); while (!Blynk.connected()) { Blynk.run(); yield(); @@ -255,7 +276,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -399,10 +420,12 @@ void loop() { if (dsc.powerTrouble) { lcd.print(0, 0, "Power "); lcd.print(0, 1, "trouble "); + Blynk.notify("Power trouble"); } else { lcd.print(0, 0, "Power "); lcd.print(0, 1, "restored "); + Blynk.notify("Power restored"); } } @@ -411,10 +434,32 @@ void loop() { if (dsc.batteryTrouble) { lcd.print(0, 0, "Battery "); lcd.print(0, 1, "trouble "); + Blynk.notify("Battery trouble"); } else { lcd.print(0, 0, "Battery "); lcd.print(0, 1, "restored "); + Blynk.notify("Battery restored"); + } + } + + if (dsc.troubleChanged) { + dsc.troubleChanged = false; + if (dsc.trouble) Blynk.notify("Trouble status on"); + else Blynk.notify("Trouble status restored"); + } + + for (byte partition = 0; partition < dscPartitions; partition++) { + if (dsc.disabled[partition]) continue; + if (dsc.alarmChanged[partition]) { + dsc.alarmChanged[partition] = false; + if (dsc.alarm[partition]) { + char alarmPartition[19] = "Alarm: Partition "; + char partitionNumber[2]; + itoa(partition + 1, partitionNumber, 10); + strcat(alarmPartition, partitionNumber); + Blynk.notify(alarmPartition); + } } } } @@ -442,7 +487,7 @@ void setStatus(byte partition, bool forceUpdate) { case 0x05: lcd.print(0, 0, "Armed: "); lcd.print(0, 1, "Away "); if (pausedZones) resetZones(); break; - case 0x06: lcd.print(0, 0, "Armed: "); + case 0x06: lcd.print(0, 0, "Armed: Stay "); lcd.print(0, 1, "No entry delay "); if (pausedZones) resetZones(); break; case 0x07: lcd.print(0, 0, "Failed "); @@ -470,7 +515,7 @@ void setStatus(byte partition, bool forceUpdate) { lcd.print(0, 1, "in progress "); break; case 0x15: lcd.print(0, 0, "Arming with "); lcd.print(0, 1, "bypass zones "); break; - case 0x16: lcd.print(0, 0, "Armed: "); + case 0x16: lcd.print(0, 0, "Armed: Away "); lcd.print(0, 1, "No entry delay "); if (pausedZones) resetZones(); break; case 0x17: lcd.print(0, 0, "Power saving "); @@ -851,19 +896,27 @@ void setLights(byte partition, bool forceUpdate) { // Processes status data not natively handled within the library void processStatus() { + #ifndef dscClassicSeries switch (dsc.panelData[0]) { - case 0x05: - if ((dsc.panelData[3] == 0x9E || dsc.panelData[3] == 0xA5 || dsc.panelData[3] == 0xB7 || dsc.panelData[3] == 0xB8) && !pausedZones) { - pauseZones(); - } + case 0x05: //Enter (*) function key, enter (*) function key while armed, enter installer code, enter master code status messages for partitions 1-4 calls pauseZones + if ((dsc.panelData[3] == 0x9E || dsc.panelData[3] == 0xA5 || dsc.panelData[3] == 0xB7 || dsc.panelData[3] == 0xB8) && !pausedZones && dsc.writePartition == 1) pauseZones(); + if ((dsc.panelData[5] == 0x9E || dsc.panelData[5] == 0xA5 || dsc.panelData[5] == 0xB7 || dsc.panelData[5] == 0xB8) && !pausedZones && dsc.writePartition == 2) pauseZones(); + if ((dsc.panelData[7] == 0x9E || dsc.panelData[7] == 0xA5 || dsc.panelData[7] == 0xB7 || dsc.panelData[7] == 0xB8) && !pausedZones && dsc.writePartition == 3) pauseZones(); + if ((dsc.panelData[9] == 0x9E || dsc.panelData[9] == 0xA5 || dsc.panelData[9] == 0xB7 || dsc.panelData[9] == 0xB8) && !pausedZones && dsc.writePartition == 4) pauseZones(); break; - case 0x0A: - if ((dsc.panelData[3] == 0x9E || dsc.panelData[3] == 0xA5 || dsc.panelData[3] == 0xB7 || dsc.panelData[3] == 0xB8) && !pausedZones) { - pauseZones(); - } - if (pausedZones) { - processProgramZones(4, ledProgramZonesColor); - } + case 0x0A: //Call processProgramZones on partition 1 + if ((dsc.panelData[3] == 0x9E || dsc.panelData[3] == 0xA5 || dsc.panelData[3] == 0xB7 || dsc.panelData[3] == 0xB8) && !pausedZones && dsc.writePartition == 1) pauseZones(); + if (pausedZones) processProgramZones(4, ledProgramZonesColor); + break; + case 0x0F: //Call processProgramZones on partition 2 + if ((dsc.panelData[3] == 0x9E || dsc.panelData[3] == 0xA5 || dsc.panelData[3] == 0xB7 || dsc.panelData[3] == 0xB8) && !pausedZones && dsc.writePartition == 2) pauseZones(); + if (pausedZones) processProgramZones(4, ledProgramZonesColor); + break; + case 0x1B: //Enter (*) function key, enter (*) function key while armed, enter installer code, enter master code status messages for partitions 4-8 calls pauseZones + if ((dsc.panelData[3] == 0x9E || dsc.panelData[3] == 0xA5 || dsc.panelData[3] == 0xB7 || dsc.panelData[3] == 0xB8) && !pausedZones && dsc.writePartition == 5) pauseZones(); + if ((dsc.panelData[5] == 0x9E || dsc.panelData[5] == 0xA5 || dsc.panelData[5] == 0xB7 || dsc.panelData[5] == 0xB8) && !pausedZones && dsc.writePartition == 6) pauseZones(); + if ((dsc.panelData[7] == 0x9E || dsc.panelData[7] == 0xA5 || dsc.panelData[7] == 0xB7 || dsc.panelData[7] == 0xB8) && !pausedZones && dsc.writePartition == 7) pauseZones(); + if ((dsc.panelData[9] == 0x9E || dsc.panelData[9] == 0xA5 || dsc.panelData[9] == 0xB7 || dsc.panelData[9] == 0xB8) && !pausedZones && dsc.writePartition == 8) pauseZones(); break; case 0x5D: if ((dsc.panelData[2] & 0x04) == 0x04) { // Alarm memory zones 1-32 @@ -888,6 +941,7 @@ void processStatus() { break; case 0xEC: if (pausedZones) processEventBufferEC(); break; } + #endif } @@ -917,6 +971,7 @@ void processProgramZones(byte startByte, const char* ledColor) { void processLCDoutputData() { + #ifndef dscClassicSeries if (!showLCDoutput) return; // Do not display LCD output data if showLCDoutput is false char dataInfo[21] = "LCD Display: "; char dataBuffer[4]; @@ -935,10 +990,12 @@ void processLCDoutputData() { } } Blynk.virtualWrite(V20, dataInfo); + #endif } void processEventBufferAA() { + #ifndef dscClassicSeries if (extendedBuffer) return; // Skips 0xAA data when 0xEC extended event buffer data is available char eventInfo[45] = "Event: "; @@ -993,10 +1050,12 @@ void processEventBufferAA() { case 0x02: printPanelStatus2(6); break; case 0x03: printPanelStatus3(6); break; } + #endif } void processEventBufferEC() { + #ifndef dscClassicSeries if (!extendedBuffer) extendedBuffer = true; char eventInfo[45] = "Event: "; @@ -1068,6 +1127,7 @@ void processEventBufferEC() { case 0x18: printPanelStatus18(8); break; case 0x1B: printPanelStatus1B(8); break; } + #endif } diff --git a/examples/esp32/VirtualKeypad-Web/VirtualKeypad-Web.ino b/examples/esp32/VirtualKeypad-Web/VirtualKeypad-Web.ino index 6f454e7..632e36b 100644 --- a/examples/esp32/VirtualKeypad-Web/VirtualKeypad-Web.ino +++ b/examples/esp32/VirtualKeypad-Web/VirtualKeypad-Web.ino @@ -1,5 +1,5 @@ /* - * VirtualKeypad-Web 1.0 (esp32) + * VirtualKeypad-Web 1.5 (esp32) * * Provides a virtual keypad web interface using the esp32 as a standalone web server, including * alarm memory, programming zone lights, and viewing the event buffer. To access the event buffer, @@ -7,8 +7,10 @@ * * Usage: * 1. Install the following libraries directly from each Github repository: - * ESPAsyncWebServer: https://github.com/me-no-dev/ESPAsyncWebServer * AsyncTCP: https://github.com/me-no-dev/AsyncTCP + * ESPAsyncWebServer: https://github.com/arjenhiemstra/ESPAsyncWebServer + * * This is a fork of the original ESPAsyncWebServer that fixes the web server crashing + * when used with recent versions of Safari on macOS and iOS. * * 2. Install the Arduino ESP32 filesystem uploader to enable uploading web server files: * https://github.com/me-no-dev/arduino-esp32fs-plugin @@ -29,6 +31,9 @@ * the serial output or http://dsc.local (for clients and networks that support mDNS). * * Release notes: + * 1.5 - Added DSC Classic series support + * Changed ESPAsyncWebServer to a newer fork to fix web server crashes with Safari + * 1.4 - Fix crash when pressing keys while Keybus is disconnected * 1.0 - Initial release * * Wiring: @@ -36,17 +41,24 @@ * * DSC Aux(-) --- esp32 Ground * - * +--- dscClockPin (esp32: 4,13,16-39) + * +--- dscClockPin // Default: 18 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp32: 4,13,16-39) + * +--- dscReadPin // Default: 19 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: 17 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp32: 4,13,16-33) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: 21 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -62,6 +74,8 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries #include #include @@ -77,16 +91,22 @@ // Settings const char* wifiSSID = ""; const char* wifiPassword = ""; +const char* accessCode = ""; // Classic series only, an access code is required to arm with the stay/away buttons. const char* dnsHostname = "dsc"; // Sets the domain name - if set to "dsc", access via: http://dsc.local const byte dscPartition = 1; // Set the partition for the keypad // Configures the Keybus interface with the specified pins -#define dscClockPin 18 // esp32: 4,13,16-39 -#define dscReadPin 19 // esp32: 4,13,16-39 -#define dscWritePin 21 // esp32: 4,13,16-33 +#define dscClockPin 18 // 4,13,16-39 +#define dscReadPin 19 // 4,13,16-39 +#define dscPC16Pin 17 // DSC Classic Series only, 4,13,16-39 +#define dscWritePin 21 // 4,13,16-33 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif AsyncWebServer server(80); AsyncWebSocket ws("/ws"); Chrono ws_ping_pong(Chrono::SECONDS); @@ -102,7 +122,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi...")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -155,12 +175,42 @@ void loop() { dsc.statusChanged = false; // Resets the status flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; } + // Checks if the interface is connected to the Keybus + if (dsc.keybusChanged) { + dsc.keybusChanged = false; // Resets the Keybus data status flag + if (dsc.keybusConnected) { + Serial.println(F("Keybus connected")); + forceUpdate = true; + if (ws.count()) { + char outas[128]; + StaticJsonDocument<200> doc; + JsonObject root = doc.to(); + root["lcd_upper"] = "Keybus"; + root["lcd_lower"] = "connected"; + serializeJson(root, outas); + ws.textAll(outas); + } + } + else { + Serial.println(F("Keybus disconnected")); + if (ws.count()) { + char outas[128]; + StaticJsonDocument<200> doc; + JsonObject root = doc.to(); + root["lcd_upper"] = "Keybus"; + root["lcd_lower"] = "disconnected"; + serializeJson(root, outas); + ws.textAll(outas); + } + } + } + setLights(partition); setStatus(partition); @@ -278,7 +328,7 @@ void setStatus(byte partition) { case 0x05: root["lcd_upper"] = "Armed: "; root["lcd_lower"] = "Away "; if (pausedZones) resetZones(); break; - case 0x06: root["lcd_upper"] = "Armed: "; + case 0x06: root["lcd_upper"] = "Armed: Stay "; root["lcd_lower"] = "No entry delay "; if (pausedZones) resetZones(); break; case 0x07: root["lcd_upper"] = "Failed "; @@ -306,7 +356,7 @@ void setStatus(byte partition) { root["lcd_lower"] = "in progress "; break; case 0x15: root["lcd_upper"] = "Arming with "; root["lcd_lower"] = "bypass zones "; break; - case 0x16: root["lcd_upper"] = "Armed: "; + case 0x16: root["lcd_upper"] = "Armed: Away "; root["lcd_lower"] = "No entry delay "; if (pausedZones) resetZones(); break; case 0x17: root["lcd_upper"] = "Power saving "; @@ -462,6 +512,7 @@ void setLights(byte partition) { // Processes status data not natively handled within the library void processStatus() { + #ifndef dscClassicSeries switch (dsc.panelData[0]) { case 0x05: if ((dsc.panelData[3] == 0x9E || dsc.panelData[3] == 0xB8) && !pausedZones) { @@ -495,6 +546,7 @@ void processStatus() { break; case 0xEC: if (pausedZones) processEventBufferEC(); break; } + #endif } @@ -527,6 +579,7 @@ void processProgramZones(byte startByte) { void processEventBufferAA() { + #ifndef dscClassicSeries if (extendedBuffer) return; // Skips 0xAA data when 0xEC extended event buffer data is available char eventInfo[45] = "Event: "; @@ -588,10 +641,12 @@ void processEventBufferAA() { case 0x02: printPanelStatus2(6); break; case 0x03: printPanelStatus3(6); break; } + #endif } void processEventBufferEC() { + #ifndef dscClassicSeries if (!extendedBuffer) extendedBuffer = true; char eventInfo[45] = "Event: "; @@ -670,6 +725,7 @@ void processEventBufferEC() { case 0x18: printPanelStatus18(8); break; case 0x1B: printPanelStatus1B(8); break; } + #endif } @@ -1517,9 +1573,30 @@ void resetZones() { void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) { + if (type == WS_EVT_CONNECT) { client->printf("{\"connected_id\": %u}", client->id()); - forceUpdate = true; + if (dsc.keybusConnected && ws.count()) { + if (ws.count()) { + char outas[128]; + StaticJsonDocument<200> doc; + JsonObject root = doc.to(); + root["lcd_upper"] = "Keybus"; + root["lcd_lower"] = "connected"; + serializeJson(root, outas); + ws.textAll(outas); + } + forceUpdate = true; + } + else if (!dsc.keybusConnected && ws.count()) { + char outas[128]; + StaticJsonDocument<200> doc; + JsonObject root = doc.to(); + root["lcd_upper"] = "Keybus"; + root["lcd_lower"] = "disconnected"; + serializeJson(root, outas); + ws.textAll(outas); + } client->ping(); ws_ping_pong.restart(); } @@ -1562,7 +1639,7 @@ void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventT char * const sep_at = strchr(tmp, '_'); if (sep_at != NULL) { *sep_at = '\0'; - dsc.write(sep_at + 1); + if (dsc.keybusConnected) dsc.write(sep_at + 1); } } } diff --git a/examples/esp8266/Email/Email.ino b/examples/esp8266/Email/Email.ino index 7b8d9ae..3083146 100644 --- a/examples/esp8266/Email/Email.ino +++ b/examples/esp8266/Email/Email.ino @@ -1,5 +1,5 @@ /* - * Email Notification 1.3 (esp8266) + * Email Notification 1.4 (esp8266) * * Processes the security system status and demonstrates how to send an email when the status has changed. Configure * the email SMTP server settings in sendMessage(). @@ -9,6 +9,7 @@ * apps: https://support.google.com/accounts/answer/6010255 * * Release notes: + * 1.4 - Added DSC Classic series support * 1.3 - Updated esp8266 wiring diagram for 33k/10k resistors * 1.2 - Check if WiFi disconnects and wait to send updates until reconnection * Add appendPartition() to simplify sketch @@ -21,23 +22,20 @@ * * DSC Aux(-) --- esp8266 Ground * - * +--- dscClockPin (esp8266: D1, D2, D8) + * +--- dscClockPin // Default: D1, GPIO 5 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp8266: D1, D2, D8) + * +--- dscReadPin // Default: D2, GPIO 4 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): - * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) - * Ground --- NPN emitter --/ - * - * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should - * be suitable, for example: - * -- 2N3904 - * -- BC547, BC548, BC549 + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground * * Issues and (especially) pull requests are welcome: * https://github.com/taligentx/dscKeybusInterface @@ -45,6 +43,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include @@ -54,11 +55,16 @@ const char* wifiPassword = ""; const char* messagePrefix = "[Security system] "; // Set a prefix for all messages // Configures the Keybus interface with the specified pins. -#define dscClockPin D1 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscReadPin D2 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin); +#endif WiFiClientSecure ipClient; bool wifiConnected = true; @@ -69,7 +75,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -112,7 +118,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; diff --git a/examples/esp8266/HomeAssistant-MQTT/HomeAssistant-MQTT.ino b/examples/esp8266/HomeAssistant-MQTT/HomeAssistant-MQTT.ino index 9998bff..8117cb0 100644 --- a/examples/esp8266/HomeAssistant-MQTT/HomeAssistant-MQTT.ino +++ b/examples/esp8266/HomeAssistant-MQTT/HomeAssistant-MQTT.ino @@ -1,5 +1,5 @@ /* - * HomeAssistant-MQTT 1.4 (esp8266) + * HomeAssistant-MQTT 1.5 (esp8266) * * Processes the security system status and allows for control using Home Assistant via MQTT. * @@ -160,6 +160,8 @@ entity: alarm_control_panel.security_partition_1 * Closed: "0" * * Release notes: + * 1.5 - Added DSC Classic series support + * Fixed armed away with no entry delay status message * 1.4 - Added PGM outputs 1-14 status * 1.3 - Updated esp8266 wiring diagram for 33k/10k resistors * 1.2 - Added sensor component to display partition status messages @@ -176,17 +178,24 @@ entity: alarm_control_panel.security_partition_1 * * DSC Aux(-) --- esp8266 Ground * - * +--- dscClockPin (esp8266: D1, D2, D8) + * +--- dscClockPin // Default: D1, GPIO 5 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp8266: D1, D2, D8) + * +--- dscReadPin // Default: D2, GPIO 4 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: D8, GPIO 15 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -200,6 +209,9 @@ entity: alarm_control_panel.security_partition_1 * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -207,7 +219,7 @@ entity: alarm_control_panel.security_partition_1 // Settings const char* wifiSSID = ""; const char* wifiPassword = ""; -const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm based on panel configuration. +const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm or enable command outputs based on panel configuration. const char* mqttServer = ""; // MQTT server domain name or IP address const int mqttPort = 1883; // MQTT server port const char* mqttUsername = ""; // Optional, leave blank if not required @@ -228,12 +240,17 @@ const char* mqttSubscribeTopic = "dsc/Set"; // Receives messages to w // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. -#define dscClockPin D1 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscReadPin D2 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscWritePin D8 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 +#define dscWritePin D8 // GPIO 15 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif WiFiClient ipClient; PubSubClient mqtt(mqttServer, mqttPort, ipClient); unsigned long mqttPreviousTime; @@ -245,7 +262,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -275,7 +292,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -288,7 +305,7 @@ void loop() { else mqtt.publish(mqttStatusTopic, mqttLwtMessage, true); } - // Sends the access code when needed by the panel for arming + // Sends the access code when needed by the panel for arming or command outputs if (dsc.accessCodePrompt) { dsc.accessCodePrompt = false; dsc.write(accessCode); @@ -527,7 +544,7 @@ void publishMessage(const char* sourceTopic, byte partition) { case 0x03: mqtt.publish(publishTopic, "Zones open", true); break; case 0x04: mqtt.publish(publishTopic, "Armed: Stay", true); break; case 0x05: mqtt.publish(publishTopic, "Armed: Away", true); break; - case 0x06: mqtt.publish(publishTopic, "Armed: No entry delay", true); break; + case 0x06: mqtt.publish(publishTopic, "Armed: Stay with no entry delay", true); break; case 0x07: mqtt.publish(publishTopic, "Failed to arm", true); break; case 0x08: mqtt.publish(publishTopic, "Exit delay in progress", true); break; case 0x09: mqtt.publish(publishTopic, "Arming with no entry delay", true); break; @@ -540,7 +557,7 @@ void publishMessage(const char* sourceTopic, byte partition) { case 0x12: mqtt.publish(publishTopic, "Battery check in progress"); break; case 0x14: mqtt.publish(publishTopic, "Auto-arm in progress", true); break; case 0x15: mqtt.publish(publishTopic, "Arming with bypassed zones", true); break; - case 0x16: mqtt.publish(publishTopic, "Armed: No entry delay", true); break; + case 0x16: mqtt.publish(publishTopic, "Armed: Away with no entry delay", true); break; case 0x17: mqtt.publish(publishTopic, "Power saving: Keypad blanked", true); break; case 0x19: mqtt.publish(publishTopic, "Disarmed: Alarm memory"); break; case 0x22: mqtt.publish(publishTopic, "Disarmed: Recent closing", true); break; diff --git a/examples/esp8266/Homebridge-MQTT/Homebridge-MQTT.ino b/examples/esp8266/Homebridge-MQTT/Homebridge-MQTT.ino index 9a33226..252a59a 100644 --- a/examples/esp8266/Homebridge-MQTT/Homebridge-MQTT.ino +++ b/examples/esp8266/Homebridge-MQTT/Homebridge-MQTT.ino @@ -1,5 +1,5 @@ /* - * Homebridge-MQTT 1.4 (esp8266) + * Homebridge-MQTT 1.7 (esp8266) * * Processes the security system status and allows for control using Apple HomeKit, including the iOS Home app, * Siri, and Google Home. This uses MQTT to interface with Homebridge and the homebridge-mqttthing plugin for @@ -147,6 +147,9 @@ * Closed: "0" * * Release notes: + * 1.7 - Fixed exit delay states while multiple partitions are arming + * 1.6 - Added DSC Classic series support + * 1.5 - Support switching armed modes while armed * 1.4 - Added PGM outputs 1-14 status * Added notes on Google Home integration * 1.3 - Updated esp8266 wiring diagram for 33k/10k resistors @@ -162,17 +165,24 @@ * * DSC Aux(-) --- esp8266 Ground * - * +--- dscClockPin (esp8266: D1, D2, D8) + * +--- dscClockPin // Default: D1, GPIO 5 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp8266: D1, D2, D8) + * +--- dscReadPin // Default: D2, GPIO 4 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: D8, GPIO 15 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -186,6 +196,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -193,7 +206,7 @@ // Settings const char* wifiSSID = ""; const char* wifiPassword = ""; -const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm based on panel configuration. +const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm or enable command outputs based on panel configuration. const char* mqttServer = ""; // MQTT server domain name or IP address const int mqttPort = 1883; // MQTT server port const char* mqttUsername = ""; // Optional, leave blank if not required @@ -209,16 +222,21 @@ const char* mqttSubscribeTopic = "dsc/Set"; // Receives messages to w // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. -#define dscClockPin D1 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscReadPin D2 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscWritePin D8 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 +#define dscWritePin D8 // GPIO 15 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif WiFiClient ipClient; PubSubClient mqtt(mqttServer, mqttPort, ipClient); unsigned long mqttPreviousTime; -char exitState; +char exitState[8]; void setup() { @@ -227,7 +245,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -257,13 +275,13 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; } - // Sends the access code when needed by the panel for arming + // Sends the access code when needed by the panel for arming or command outputs if (dsc.accessCodePrompt) { dsc.accessCodePrompt = false; dsc.write(accessCode); @@ -278,7 +296,7 @@ void loop() { // Publishes armed/disarmed status if (dsc.armedChanged[partition]) { if (dsc.armed[partition]) { - exitState = 0; + exitState[partition] = 0; // Night armed away if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) { @@ -313,21 +331,21 @@ void loop() { if (dsc.exitDelay[partition]) { // Sets the arming target state if the panel is armed externally - if (exitState == 0 || dsc.exitStateChanged[partition]) { + if (exitState[partition] == 0 || dsc.exitStateChanged[partition]) { dsc.exitStateChanged[partition] = 0; switch (dsc.exitState[partition]) { case DSC_EXIT_STAY: { - exitState = 'S'; + exitState[partition] = 'S'; publishState(mqttPartitionTopic, partition, "S", 0); break; } case DSC_EXIT_AWAY: { - exitState = 'A'; + exitState[partition] = 'A'; publishState(mqttPartitionTopic, partition, "A", 0); break; } case DSC_EXIT_NO_ENTRY_DELAY: { - exitState = 'N'; + exitState[partition] = 'N'; publishState(mqttPartitionTopic, partition, "N", 0); break; } @@ -337,7 +355,7 @@ void loop() { // Disarmed during exit delay else if (!dsc.armed[partition]) { - exitState = 0; + exitState[partition] = 0; publishState(mqttPartitionTopic, partition, "D", "D"); } } @@ -442,7 +460,52 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { payloadIndex = 1; } - // Resets the HomeKit target state if attempting to change the armed mode while armed or not ready + // Sets night arm (no entry delay) while armed + if (payload[payloadIndex] == 'N' && dsc.armed[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write('n'); // Keypad no entry delay + publishState(mqttPartitionTopic, partition, "N", 0); + exitState[partition] = 'N'; + return; + } + + // Disables night arm while armed stay + if (payload[payloadIndex] == 'S' && dsc.armedStay[partition] && dsc.noEntryDelay[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write('n'); // Keypad no entry delay + publishState(mqttPartitionTopic, partition, "S", 0); + exitState[partition] = 'S'; + return; + } + + // Disables night arm while armed away + if (payload[payloadIndex] == 'A' && dsc.armedAway[partition] && dsc.noEntryDelay[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write('n'); // Keypad no entry delay + publishState(mqttPartitionTopic, partition, "A", 0); + exitState[partition] = 'A'; + return; + } + + // Changes from arm away to arm stay after the exit delay + if (payload[payloadIndex] == 'S' && dsc.armedAway[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write("s"); + publishState(mqttPartitionTopic, partition, "S", 0); + exitState[partition] = 'S'; + return; + } + + // Changes from arm stay to arm away after the exit delay + if (payload[payloadIndex] == 'A' && dsc.armedStay[partition]) { + dsc.writePartition = partition + 1; // Sets writes to the partition number + dsc.write("w"); + publishState(mqttPartitionTopic, partition, "A", 0); + exitState[partition] = 'A'; + return; + } + + // Resets the HomeKit target state if attempting to change the armed mode while not ready if (payload[payloadIndex] != 'D' && !dsc.ready[partition]) { dsc.armedChanged[partition] = true; dsc.statusChanged = true; @@ -450,10 +513,10 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { } // Resets the HomeKit target state if attempting to change the arming mode during the exit delay - if (payload[payloadIndex] != 'D' && dsc.exitDelay[partition] && exitState != 0) { - if (exitState == 'S') publishState(mqttPartitionTopic, partition, "S", 0); - else if (exitState == 'A') publishState(mqttPartitionTopic, partition, "A", 0); - else if (exitState == 'N') publishState(mqttPartitionTopic, partition, "N", 0); + if (payload[payloadIndex] != 'D' && dsc.exitDelay[partition] && exitState[partition] != 0) { + if (exitState[partition] == 'S') publishState(mqttPartitionTopic, partition, "S", 0); + else if (exitState[partition] == 'A') publishState(mqttPartitionTopic, partition, "A", 0); + else if (exitState[partition] == 'N') publishState(mqttPartitionTopic, partition, "N", 0); } @@ -462,7 +525,7 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { dsc.writePartition = partition + 1; // Sets writes to the partition number dsc.write('s'); // Keypad stay arm publishState(mqttPartitionTopic, partition, "S", 0); - exitState = 'S'; + exitState[partition] = 'S'; return; } @@ -471,7 +534,7 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { dsc.writePartition = partition + 1; // Sets writes to the partition number dsc.write('w'); // Keypad away arm publishState(mqttPartitionTopic, partition, "A", 0); - exitState = 'A'; + exitState[partition] = 'A'; return; } @@ -480,7 +543,7 @@ void mqttCallback(char* topic, byte* payload, unsigned int length) { dsc.writePartition = partition + 1; // Sets writes to the partition number dsc.write('n'); // Keypad arm with no entry delay publishState(mqttPartitionTopic, partition, "N", 0); - exitState = 'N'; + exitState[partition] = 'N'; return; } diff --git a/examples/esp8266/Homey/Homey.ino b/examples/esp8266/Homey/Homey.ino index a0e4b1d..9f00f33 100755 --- a/examples/esp8266/Homey/Homey.ino +++ b/examples/esp8266/Homey/Homey.ino @@ -1,5 +1,5 @@ /* - * Homey 1.2 (esp8266) + * Homey 1.3 (esp8266) * * Processes the security system status for partition 1 and allows for control using Athom Homey. * @@ -12,6 +12,7 @@ * Zone states are published by Homey.trigger command including the zone number. * * Release notes: + * 1.3 - Added DSC Classic series support * 1.2 - Updated esp8266 wiring diagram for 33k/10k resistors * 1.1 - Added status update on WiFi reconnection * Removed writeReady check, moved into library @@ -22,17 +23,24 @@ * * DSC Aux(-) --- esp8266 Ground * - * +--- dscClockPin (esp8266: D1, D2, D8) + * +--- dscClockPin // Default: D1, GPIO 5 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp8266: D1, D2, D8) + * +--- dscReadPin // Default: D2, GPIO 4 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: D8, GPIO 15 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -48,6 +56,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -59,12 +70,17 @@ const char* accessCode = ""; // An access code is required to disarm/night arm // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. -#define dscClockPin D1 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscReadPin D2 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscWritePin D8 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 +#define dscWritePin D8 // GPIO 15 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif bool wifiConnected = true; @@ -74,7 +90,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -127,7 +143,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; diff --git a/examples/esp8266/KeybusReader/KeybusReader.ino b/examples/esp8266/KeybusReader/KeybusReader.ino index 874c4af..01bbce5 100644 --- a/examples/esp8266/KeybusReader/KeybusReader.ino +++ b/examples/esp8266/KeybusReader/KeybusReader.ino @@ -1,11 +1,12 @@ /* - * DSC Keybus Reader 1.2 (esp8266) + * DSC Keybus Reader 1.3 (esp8266) * * Decodes and prints data from the Keybus to a serial interface, including reading from serial for the virtual * keypad. This is primarily to help decode the Keybus protocol - see the Status example to put the interface * to productive use. * * Release notes: + * 1.3 - Added DSC Classic series support * 1.2 - Handle spurious data while keybus is disconnected * Removed redundant data processing * 1.1 - Updated esp8266 wiring diagram for 33k/10k resistors @@ -16,17 +17,24 @@ * * DSC Aux(-) --- esp8266 Ground * - * +--- dscClockPin (esp8266: D1, D2, D8) + * +--- dscClockPin // Default: D1, GPIO 5 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp8266: D1, D2, D8) + * +--- dscReadPin // Default: D2, GPIO 4 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: D8, GPIO 15 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -40,16 +48,24 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. -#define dscClockPin D1 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscReadPin D2 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscWritePin D8 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 +#define dscWritePin D8 // GPIO 15 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin); +#endif void setup() { @@ -89,7 +105,7 @@ void loop() { } // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; diff --git a/examples/esp8266/KeybusReaderIP/KeybusReaderIP.ino b/examples/esp8266/KeybusReaderIP/KeybusReaderIP.ino index ecbf98c..7047dc3 100644 --- a/examples/esp8266/KeybusReaderIP/KeybusReaderIP.ino +++ b/examples/esp8266/KeybusReaderIP/KeybusReaderIP.ino @@ -1,5 +1,5 @@ /* - * DSC Keybus Reader IP 1.2 (esp8266) + * DSC Keybus Reader IP 1.3 (esp8266) * * Decodes and prints data from the Keybus to a TCP connection including virtual keyboard over IP. This is * primarily to help decode the Keybus protocol - see the Status example to put the interface to productive use. @@ -9,6 +9,7 @@ * 2. For macOS/Linux: telnet dsc.local * * Release notes: + * 1.3 - Added DSC Classic series support * 1.2 - Updated to connect via telnet * Handle spurious data while keybus is disconnected * Removed redundant data processing @@ -20,17 +21,24 @@ * * DSC Aux(-) --- esp8266 Ground * - * +--- dscClockPin (esp8266: D1, D2, D8) + * +--- dscClockPin // Default: D1, GPIO 5 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp8266: D1, D2, D8) + * +--- dscReadPin // Default: D2, GPIO 4 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: D8, GPIO 15 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -46,6 +54,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -58,12 +69,17 @@ const int serverPort = 23; // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. -#define dscClockPin D1 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscReadPin D2 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscWritePin D8 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 +#define dscWritePin D8 // GPIO 15 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin); +#endif WiFiServer ipServer(serverPort); WiFiClient ipClient; @@ -74,7 +90,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -148,7 +164,7 @@ void loop() { if (dsc.loop()) { // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { ipClient.print(F("Keybus buffer overflow")); dsc.bufferOverflow = false; diff --git a/examples/esp8266/KeypadInterface-MQTT/KeypadInterface-MQTT.ino b/examples/esp8266/KeypadInterface-MQTT/KeypadInterface-MQTT.ino new file mode 100644 index 0000000..6cf96f1 --- /dev/null +++ b/examples/esp8266/KeypadInterface-MQTT/KeypadInterface-MQTT.ino @@ -0,0 +1,365 @@ +/* + * DSC Keypad Interface-MQTT 1.2 (esp8266) + * + * Emulates a DSC panel to directly interface DSC PowerSeries or Classic series keypads as physical + * input devices for any general purpose, without needing a DSC panel. This sketch uses MQTT to + * send pressed keypad keys and receive commands to control keypad lights and tones. + * + * PowerSeries keypad features: + * - Read keypad key button presses, including fire/aux/panic alarm keys: dsc.key + * - Set keypad lights: Ready, Armed, Trouble, Memory, Bypass, Fire, Program, Backlight, Zones 1-8: dsc.lightReady, dsc.lightZone1, etc + * - Set keypad beeps, 1-128: dsc.beep(3) + * - Set keypad buzzer in seconds, 1-255: dsc.tone(5) + * - Set keypad tone pattern with a number of beeps, an optional constant tone, and the interval in seconds between beeps: + * 2 beeps, no constant tone, 4 second interval: dsc.tone(2, false, 4) + * 3 beeps, constant tone, 2 second interval: dsc.tone(3, true, 2) + * Disable the tone: dsc.tone() or dsc.tone(0, false, 0) + * + * Classic keypad features: + * - Read keypad key button presses, including fire/aux/panic alarm keys: dsc.key + * - Set keypad lights: Ready, Armed, Trouble, Memory, Bypass, Fire, Program, Zones 1-8: dsc.lightReady, dsc.lightZone1, etc + * + * This interface uses a different wiring setup from the standard Keybus interface, adding + * an NPN transistor on dscClockPin. The DSC keypads require a 12v DC power source, though + * lower voltages down to 7v may work for key presses (the LEDs will be dim). + * + * Release notes: + * 1.3 - Add Classic keypad support - PC2550RK + * 1.2 - Add Classic keypad support - PC1500RK + * 1.1 - Add keypad beep, buzzer, constant tone + * 1.0 - Initial release + * + * Wiring: + * DSC Keypad R --- 12v DC + * + * DSC Keypad B --- esp8266 ground + * + * DSC Keypad Y ---+--- 1k ohm resistor --- 12v DC + * | + * +--- NPN collector --\ + * |-- NPN base --- 1k ohm resistor --- dscClockPin // esp8266: D1, GPIO 5 + * Ground --- NPN emitter --/ + * + * DSC Keypad G ---+--- 1k ohm resistor --- 12v DC + * | + * +--- 33k ohm resistor ---+--- dscReadPin // esp8266: D2, GPIO 4 + * | | + * | +--- 10k ohm resistor --- Ground + * | + * +--- NPN collector --\ + * |-- NPN base --- 1k ohm resistor --- dscWritePin // esp8266: D8, GPIO 15 + * Ground --- NPN emitter --/ + * + * The keypad interface uses NPN transistors to pull the clock and data lines low - most small + * signal NPN transistors should be suitable, for example: + * - 2N3904 + * - BC547, BC548, BC549 + * + * Issues and (especially) pull requests are welcome: + * https://github.com/taligentx/dscKeybusInterface + * + * This example code is in the public domain. + */ + +// Set the keypad type +#define dscKeypad +//#define dscClassicKeypad + +#include +#include +#include + +// Settings +const char* wifiSSID = ""; +const char* wifiPassword = ""; +const char* mqttServer = ""; // MQTT server domain name or IP address +const int mqttPort = 1883; // MQTT server port +const char* mqttUsername = ""; // Optional, leave blank if not required +const char* mqttPassword = ""; // Optional, leave blank if not required + +// MQTT topics +const char* mqttClientName = "dscKeypadInterface"; +const char* mqttKeyTopic = "dsc/Key"; // Sends keypad keys +const char* mqttSubscribeTopic = "dsc/Set"; // Receives messages to send to the keypad + +// Configures the Keybus interface with the specified pins +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscWritePin D8 // GPIO 15 + +// Initialize components +#ifdef dscKeypad +dscKeypadInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicKeypadInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#endif +bool lightOff, lightBlink, inputReceived; +const byte inputLimit = 255; +char input[inputLimit]; +byte beepLength, buzzerLength, toneLength; +WiFiClient ipClient; +PubSubClient mqtt(mqttServer, mqttPort, ipClient); +unsigned long mqttPreviousTime; + + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println(); + Serial.println(); + + Serial.print(F("WiFi....")); + WiFi.mode(WIFI_STA); + WiFi.begin(wifiSSID, wifiPassword); + while (WiFi.status() != WL_CONNECTED) { + Serial.print("."); + delay(500); + } + Serial.print(F("connected: ")); + Serial.println(WiFi.localIP()); + + mqtt.setCallback(mqttCallback); + if (mqttConnect()) mqttPreviousTime = millis(); + else mqttPreviousTime = 0; + + Serial.print(F("Keybus....")); + dsc.begin(); + Serial.println(F("connected.")); + Serial.println(F("DSC Keypad Interface is online.")); +} + +void loop() { + mqttHandle(); + + /* + * Sets keypad status via serial with the listed keys. Light status uses custom + * values for control: off, on, blink (example: dsc.lightReady = blink;) + * + * Light on: Send the keys listed below. Turning on the armed light: "a" + * Light off: Send "-" before a light key to turn it off. Turning off the zone 4 light: "-4" + * Light blink: Send "!" before a light key to blink. Blinking the ready light: "!r" + * Beep: Send "b" followed by the number of beeps, 1-128. Setting 2 beeps: "b2" + * Buzzer: Send "z" followed by the buzzer length in seconds, 1-255. Setting the buzzer to 5 seconds: "z5" + * Tone pattern: Send "n" followed by the number of beeps 1-7, constant tone true "t" or false "f", interval between beeps 1-15s + * Setting a tone pattern with 2 beeps, no constant tone, 4 second interval: "n2f4" + * Setting a tone pattern with 3 beeps, constant tone, 2 second interval: "n3t2" + * Disabling the tone pattern: "n" + */ + if (inputReceived) { + inputReceived = false; + + for (byte i = 0; i < strlen(input); i++) { + switch (input[i]) { + case 'r': case 'R': dsc.lightReady = setLight(); break; + case 'a': case 'A': dsc.lightArmed = setLight(); break; + case 'm': case 'M': dsc.lightMemory = setLight(); break; + case 'y': case 'Y': dsc.lightBypass = setLight(); break; + case 't': case 'T': dsc.lightTrouble = setLight(); break; + case 'p': case 'P': dsc.lightProgram = setLight(); break; + case 'f': case 'F': dsc.lightFire = setLight(); break; + case 'l': case 'L': dsc.lightBacklight = setLight(); break; + case '1': dsc.lightZone1 = setLight(); break; + case '2': dsc.lightZone2 = setLight(); break; + case '3': dsc.lightZone3 = setLight(); break; + case '4': dsc.lightZone4 = setLight(); break; + case '5': dsc.lightZone5 = setLight(); break; + case '6': dsc.lightZone6 = setLight(); break; + case '7': dsc.lightZone7 = setLight(); break; + case '8': dsc.lightZone8 = setLight(); break; + case 'b': case 'B': sendBeeps(i); i += beepLength; break; + case 'n': case 'N': sendTone(i); i+= toneLength; break; + case 'z': case 'Z': sendBuzzer(i); i+= buzzerLength; break; + case '-': lightOff = true; break; + case '!': lightBlink = true; break; + default: break; + } + } + } + + dsc.loop(); + + // Checks for a keypad key press + if (dsc.keyAvailable) { + dsc.keyAvailable = false; + switch (dsc.key) { + case 0x00: mqtt.publish(mqttKeyTopic, "0", false); break; + case 0x05: mqtt.publish(mqttKeyTopic, "1", false); break; + case 0x0A: mqtt.publish(mqttKeyTopic, "2", false); break; + case 0x0F: mqtt.publish(mqttKeyTopic, "3", false); break; + case 0x11: mqtt.publish(mqttKeyTopic, "4", false); break; + case 0x16: mqtt.publish(mqttKeyTopic, "5", false); break; + case 0x1B: mqtt.publish(mqttKeyTopic, "6", false); break; + case 0x1C: mqtt.publish(mqttKeyTopic, "7", false); break; + case 0x22: mqtt.publish(mqttKeyTopic, "8", false); break; + case 0x27: mqtt.publish(mqttKeyTopic, "9", false); break; + case 0x28: mqtt.publish(mqttKeyTopic, "*", false); break; + case 0x2D: mqtt.publish(mqttKeyTopic, "#", false); break; + case 0x82: mqtt.publish(mqttKeyTopic, "Enter", false); break; + case 0xAF: mqtt.publish(mqttKeyTopic, "Arm: Stay", false); break; + case 0xB1: mqtt.publish(mqttKeyTopic, "Arm: Away", false); break; + case 0xBB: mqtt.publish(mqttKeyTopic, "Door chime", false); break; + case 0xDA: mqtt.publish(mqttKeyTopic, "Reset", false); break; + case 0xE1: mqtt.publish(mqttKeyTopic, "Quick exit", false); break; + case 0xF7: mqtt.publish(mqttKeyTopic, "Menu navigation", false); break; + case 0x0B: mqtt.publish(mqttKeyTopic, "Fire alarm", false); break; + case 0x0D: mqtt.publish(mqttKeyTopic, "Aux alarm", false); break; + case 0x0E: mqtt.publish(mqttKeyTopic, "Panic alarm", false); break; + } + mqtt.subscribe(mqttSubscribeTopic); + } +} + + +// Parse the number of beeps from the input +void sendBeeps(byte position) { + char inputNumber[4]; + byte beeps = 0; + beepLength = 0; + + for (byte i = position + 1; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + inputNumber[beepLength] = input[i]; + beepLength++; + if (beepLength >= 3) break; + } + else break; + } + + inputNumber[beepLength] = '\0'; + beeps = atoi(inputNumber); + if (beeps > 128) beeps = 128; + + dsc.beep(beeps); +} + + +// Parse the buzzer length in seconds from the input +void sendBuzzer(byte position) { + char inputNumber[4]; + byte buzzerSeconds = 0; + buzzerLength = 0; + + for (byte i = position + 1; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + inputNumber[buzzerLength] = input[i]; + buzzerLength++; + if (buzzerLength >= 3) break; + } + else break; + } + + inputNumber[buzzerLength] = '\0'; + buzzerSeconds = atoi(inputNumber); + dsc.buzzer(buzzerSeconds); +} + + +// Parse the tone pattern number of beeps, constant tone state, and interval in seconds from the input +void sendTone(byte position) { + byte beeps = 0, interval = 0, intervalLength = 0; + char beepNumber[2]; + bool toneState; + char intervalNumber[3]; + toneLength = 0; + + if (strlen(input) < 4) { + dsc.tone(0, false, 0); + return; + } + + // Parse beeps 0-7 + if (input[position + 1] >= '0' && input[position + 1] <= '9') { + beepNumber[0] = input[position + 1]; + beeps = atoi(beepNumber); + if (beeps > 7) beeps = 7; + toneLength++; + } + else return; + + // Parse constant tone value + switch (input[position + 2]) { + case 't': + case 'T': toneState = true; toneLength++; break; + case 'f': + case 'F': toneState = false; toneLength++; break; + default: toneLength--; return; + } + + // Parse interval + for (byte i = position + 3; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + intervalNumber[intervalLength] = input[i]; + intervalLength++; + toneLength++; + if (intervalLength >= 2) break; + } + else break; + } + intervalNumber[intervalLength] = '\0'; + interval = atoi(intervalNumber); + if (interval > 15) interval = 15; + + dsc.tone(beeps, toneState, interval); +} + + +// Sets keypad lights state - lights use custom values for control: off, on, blink (example: dsc.lightReady = blink;) +Light setLight() { + if (lightOff) { + lightOff = false; + return off; + } + else if (lightBlink) { + lightBlink = false; + return blink; + } + else return on; +} + + +// Handles messages received in the mqttSubscribeTopic +void mqttCallback(char* topic, byte* payload, unsigned int length) { + + // Handles unused parameters + (void)topic; + + for (unsigned int i = 0; i < length; i++) { + input[i] = payload[i]; + } + + input[length] = '\0'; + if (input[0] == '\0') inputReceived = false; + else inputReceived = true; +} + + +void mqttHandle() { + if (!mqtt.connected()) { + unsigned long mqttCurrentTime = millis(); + if (mqttCurrentTime - mqttPreviousTime > 5000) { + mqttPreviousTime = mqttCurrentTime; + if (mqttConnect()) { + Serial.println(F("MQTT disconnected, successfully reconnected.")); + mqttPreviousTime = 0; + mqtt.subscribe(mqttSubscribeTopic); + } + else Serial.println(F("MQTT disconnected, failed to reconnect.")); + } + } + else mqtt.loop(); +} + + +bool mqttConnect() { + Serial.print(F("MQTT....")); + if (mqtt.connect(mqttClientName, mqttUsername, mqttPassword)) { + Serial.print(F("connected: ")); + Serial.println(mqttServer); + mqtt.subscribe(mqttSubscribeTopic); + } + else { + Serial.print(F("connection error: ")); + Serial.println(mqttServer); + } + return mqtt.connected(); +} diff --git a/examples/esp8266/KeypadInterface/KeypadInterface.ino b/examples/esp8266/KeypadInterface/KeypadInterface.ino new file mode 100644 index 0000000..7303edf --- /dev/null +++ b/examples/esp8266/KeypadInterface/KeypadInterface.ino @@ -0,0 +1,343 @@ +/* + * DSC Keypad Interface 1.4 (esp8266) + * + * Emulates a DSC panel to directly interface DSC PowerSeries or Classic series keypads as physical + * input devices for any general purpose, without needing a DSC panel. + * + * PowerSeries keypad features: + * - Read keypad key button presses, including fire/aux/panic alarm keys: dsc.key + * - Set keypad lights: Ready, Armed, Trouble, Memory, Bypass, Fire, Program, Backlight, Zones 1-8: dsc.lightReady, dsc.lightZone1, etc + * - Set keypad beeps, 1-128: dsc.beep(3) + * - Set keypad buzzer in seconds, 1-255: dsc.tone(5) + * - Set keypad tone pattern with a number of beeps, an optional constant tone, and the interval in seconds between beeps: + * 2 beeps, no constant tone, 4 second interval: dsc.tone(2, false, 4) + * 3 beeps, constant tone, 2 second interval: dsc.tone(3, true, 2) + * Disable the tone: dsc.tone() or dsc.tone(0, false, 0) + * - Set LCD keypad messages (on cmd 0x05/byte3) with entering HEX input into serial console: + * According to printPanelMessages in dscKeybusPrintData.cpp, through it doesn't seem to fully match + * Change Function keys 1-5 with entering: 0x70 - 0x74 + * Change LCD keypad time by entering: 0x2A (slight delay before LCD will show to input time data) + * Change LCD Brightness/contrast/buzzer level by entering: 0x29 and scrolling to desired setting then pressing (*) + * + * Classic keypad features: + * - Read keypad key button presses, including fire/aux/panic alarm keys: dsc.key + * - Set keypad lights: Ready, Armed, Trouble, Memory, Bypass, Fire, Program, Zones 1-8: dsc.lightReady, dsc.lightZone1, etc + * + * This interface uses a different wiring setup from the standard Keybus interface, adding + * an NPN transistor on dscClockPin. The DSC keypads require a 12v DC power source, though + * lower voltages down to 7v may work for key presses (the LEDs will be dim). + * + * Release notes: + * 1.4 - Added ability to change LCD keypad messages + * 1.3 - Add Classic keypad support - PC2550RK + * 1.2 - Add Classic keypad support - PC1500RK + * 1.1 - Add keypad beep, buzzer, constant tone + * 1.0 - Initial release + * + * Wiring: + * DSC Keypad R --- 12v DC + * + * DSC Keypad B --- esp8266 ground + * + * DSC Keypad Y ---+--- 1k ohm resistor --- 12v DC + * | + * +--- NPN collector --\ + * |-- NPN base --- 1k ohm resistor --- dscClockPin // esp8266: D1, GPIO 5 + * Ground --- NPN emitter --/ + * + * DSC Keypad G ---+--- 1k ohm resistor --- 12v DC + * | + * +--- 33k ohm resistor ---+--- dscReadPin // esp8266: D2, GPIO 4 + * | | + * | +--- 10k ohm resistor --- Ground + * | + * +--- NPN collector --\ + * |-- NPN base --- 1k ohm resistor --- dscWritePin // esp8266: D8, GPIO 15 + * Ground --- NPN emitter --/ + * + * The keypad interface uses NPN transistors to pull the clock and data lines low - most small + * signal NPN transistors should be suitable, for example: + * - 2N3904 + * - BC547, BC548, BC549 + * + * Issues and (especially) pull requests are welcome: + * https://github.com/taligentx/dscKeybusInterface + * + * This example code is in the public domain. + */ + +// Set the keypad type +#define dscKeypad +//#define dscClassicKeypad + +#include + +// Configures the Keybus interface with the specified pins +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscWritePin D8 // GPIO 15 + +// Initialize components +#ifdef dscKeypad +dscKeypadInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicKeypadInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#endif +bool lightOff, lightBlink, inputReceived; +const byte inputLimit = 50; +char input[inputLimit]; +byte beepLength, buzzerLength, toneLength; + + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println(); + Serial.println(); + + Serial.print(F("Keybus....")); + dsc.begin(); + Serial.println(F("connected.")); + Serial.println(F("DSC Keypad Interface is online.")); +} + +void loop() { + + inputSerial(); // Stores Serial data in input[], requires a newline character (NL, CR, or both) + + /* + * Sets keypad status via serial with the listed keys. Light status uses custom + * values for control: off, on, blink (example: dsc.lightReady = blink;) + * + * Light on: Send the keys listed below. Turning on the armed light: "a" + * Light off: Send "-" before a light key to turn it off. Turning off the zone 4 light: "-4" + * Light blink: Send "!" before a light key to blink. Blinking the ready light: "!r" + * Beep: Send "b" followed by the number of beeps, 1-128. Setting 2 beeps: "b2" + * Buzzer: Send "z" followed by the buzzer length in seconds, 1-255. Setting the buzzer to 5 seconds: "z5" + * Tone pattern: Send "n" followed by the number of beeps 1-7, constant tone true "t" or false "f", interval between beeps 1-15s + * Setting a tone pattern with 2 beeps, no constant tone, 4 second interval: "n2f4" + * Setting a tone pattern with 3 beeps, constant tone, 2 second interval: "n3t2" + * Disabling the tone pattern: "n" + */ + if (inputReceived) { + inputReceived = false; + + #if defined(dscKeypad) + if (String(input).startsWith("0x")) dsc.panelCommand05[2] = strtoul(input, NULL, 16); + else { + for (byte i = 0; i < strlen(input); i++) { + switch (input[i]) { + case 'r': case 'R': dsc.lightReady = setLight(); break; + case 'a': case 'A': dsc.lightArmed = setLight(); break; + case 'm': case 'M': dsc.lightMemory = setLight(); break; + case 'y': case 'Y': dsc.lightBypass = setLight(); break; + case 't': case 'T': dsc.lightTrouble = setLight(); break; + case 'p': case 'P': dsc.lightProgram = setLight(); break; + case 'f': case 'F': dsc.lightFire = setLight(); break; + case 'l': case 'L': dsc.lightBacklight = setLight(); break; + case '1': dsc.lightZone1 = setLight(); break; + case '2': dsc.lightZone2 = setLight(); break; + case '3': dsc.lightZone3 = setLight(); break; + case '4': dsc.lightZone4 = setLight(); break; + case '5': dsc.lightZone5 = setLight(); break; + case '6': dsc.lightZone6 = setLight(); break; + case '7': dsc.lightZone7 = setLight(); break; + case '8': dsc.lightZone8 = setLight(); break; + case 'b': case 'B': sendBeeps(i); i += beepLength; break; + case 'n': case 'N': sendTone(i); i+= toneLength; break; + case 'z': case 'Z': sendBuzzer(i); i+= buzzerLength; break; + case '-': lightOff = true; break; + case '!': lightBlink = true; break; + default: break; + } + } + } + #else + for (byte i = 0; i < strlen(input); i++) { + switch (input[i]) { + case 'r': case 'R': dsc.lightReady = setLight(); break; + case 'a': case 'A': dsc.lightArmed = setLight(); break; + case 'm': case 'M': dsc.lightMemory = setLight(); break; + case 'y': case 'Y': dsc.lightBypass = setLight(); break; + case 't': case 'T': dsc.lightTrouble = setLight(); break; + case 'p': case 'P': dsc.lightProgram = setLight(); break; + case 'f': case 'F': dsc.lightFire = setLight(); break; + case 'l': case 'L': dsc.lightBacklight = setLight(); break; + case '1': dsc.lightZone1 = setLight(); break; + case '2': dsc.lightZone2 = setLight(); break; + case '3': dsc.lightZone3 = setLight(); break; + case '4': dsc.lightZone4 = setLight(); break; + case '5': dsc.lightZone5 = setLight(); break; + case '6': dsc.lightZone6 = setLight(); break; + case '7': dsc.lightZone7 = setLight(); break; + case '8': dsc.lightZone8 = setLight(); break; + case 'b': case 'B': sendBeeps(i); i += beepLength; break; + case 'n': case 'N': sendTone(i); i+= toneLength; break; + case 'z': case 'Z': sendBuzzer(i); i+= buzzerLength; break; + case '-': lightOff = true; break; + case '!': lightBlink = true; break; + default: break; + } + } + #endif + } + + dsc.loop(); + + // Checks for a keypad key press + if (dsc.keyAvailable) { + dsc.keyAvailable = false; + switch (dsc.key) { + case 0x00: Serial.println("0"); break; + case 0x05: Serial.println("1"); break; + case 0x0A: Serial.println("2"); break; + case 0x0F: Serial.println("3"); break; + case 0x11: Serial.println("4"); break; + case 0x16: Serial.println("5"); break; + case 0x1B: Serial.println("6"); break; + case 0x1C: Serial.println("7"); break; + case 0x22: Serial.println("8"); break; + case 0x27: Serial.println("9"); break; + case 0x28: Serial.println("*"); break; + case 0x2D: Serial.println("#"); break; + case 0x82: Serial.println(F("Enter")); break; + case 0xAF: Serial.println(F("Arm: Stay")); break; + case 0xB1: Serial.println(F("Arm: Away")); break; + case 0xBB: Serial.println(F("Door chime")); break; + case 0xDA: Serial.println(F("Reset")); break; + case 0xE1: Serial.println(F("Quick exit")); break; + case 0xF7: Serial.println(F("Menu navigation")); break; + case 0x0B: Serial.println(F("Fire alarm")); break; + case 0x0D: Serial.println(F("Aux alarm")); break; + case 0x0E: Serial.println(F("Panic alarm")); break; + default: break; + } + } +} + + +// Parse the number of beeps from the input +void sendBeeps(byte position) { + char inputNumber[4]; + byte beeps = 0; + beepLength = 0; + + for (byte i = position + 1; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + inputNumber[beepLength] = input[i]; + beepLength++; + if (beepLength >= 3) break; + } + else break; + } + + inputNumber[beepLength] = '\0'; + beeps = atoi(inputNumber); + if (beeps > 128) beeps = 128; + + dsc.beep(beeps); +} + + +// Parse the buzzer length in seconds from the input +void sendBuzzer(byte position) { + char inputNumber[4]; + byte buzzerSeconds = 0; + buzzerLength = 0; + + for (byte i = position + 1; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + inputNumber[buzzerLength] = input[i]; + buzzerLength++; + if (buzzerLength >= 3) break; + } + else break; + } + + inputNumber[buzzerLength] = '\0'; + buzzerSeconds = atoi(inputNumber); + dsc.buzzer(buzzerSeconds); +} + + +// Parse the tone pattern number of beeps, constant tone state, and interval in seconds from the input +void sendTone(byte position) { + byte beeps = 0, interval = 0, intervalLength = 0; + char beepNumber[2]; + bool toneState; + char intervalNumber[3]; + toneLength = 0; + + if (strlen(input) < 4) { + dsc.tone(0, false, 0); + return; + } + + // Parse beeps 0-7 + if (input[position + 1] >= '0' && input[position + 1] <= '9') { + beepNumber[0] = input[position + 1]; + beeps = atoi(beepNumber); + if (beeps > 7) beeps = 7; + toneLength++; + } + else return; + + // Parse constant tone value + switch (input[position + 2]) { + case 't': + case 'T': toneState = true; toneLength++; break; + case 'f': + case 'F': toneState = false; toneLength++; break; + default: toneLength--; return; + } + + // Parse interval + for (byte i = position + 3; i < strlen(input); i++) { + if (input[i] >= '0' && input[i] <= '9') { + intervalNumber[intervalLength] = input[i]; + intervalLength++; + toneLength++; + if (intervalLength >= 2) break; + } + else break; + } + intervalNumber[intervalLength] = '\0'; + interval = atoi(intervalNumber); + if (interval > 15) interval = 15; + + dsc.tone(beeps, toneState, interval); +} + + +// Sets keypad lights state - lights use custom values for control: off, on, blink (example: dsc.lightReady = blink;) +Light setLight() { + if (lightOff) { + lightOff = false; + return off; + } + else if (lightBlink) { + lightBlink = false; + return blink; + } + else return on; +} + + +// Stores Serial data in input[], requires a newline character (NL, CR, or both) +void inputSerial() { + static byte inputCount = 0; + if (!inputReceived) { + while (Serial.available() > 0 && inputCount < inputLimit) { + input[inputCount] = Serial.read(); + if (input[inputCount] == '\n' || input[inputCount] == '\r') { + input[inputCount] = '\0'; + inputCount = 0; + inputReceived = true; + break; + } + else inputCount++; + yield(); + } + if (input[0] == '\0') inputReceived = false; + } +} diff --git a/examples/esp8266/OpenHAB-MQTT/OpenHAB-MQTT.ino b/examples/esp8266/OpenHAB-MQTT/OpenHAB-MQTT.ino index 128a393..60a64f0 100644 --- a/examples/esp8266/OpenHAB-MQTT/OpenHAB-MQTT.ino +++ b/examples/esp8266/OpenHAB-MQTT/OpenHAB-MQTT.ino @@ -1,5 +1,5 @@ /* - * OpenHAB-MQTT 1.2 (esp8266) + * OpenHAB-MQTT 1.3 (esp8266) * * Processes the security system status and allows for control using OpenHAB. This uses MQTT to * interface with OpenHAB and the MQTT binding and demonstrates sending the panel status as a @@ -89,6 +89,7 @@ Contact zone3 "Zone 3" {channel="mqtt:topic:mymqtt:dsc:zone3"} * Closed: "0" * * Release notes: + * 1.3 - Added DSC Classic series support * 1.2 - Added PGM outputs 1-14 status * 1.1 - Removed partition exit delay MQTT message, not used in this OpenHAB example * Updated esp8266 wiring diagram for 33k/10k resistors @@ -99,17 +100,24 @@ Contact zone3 "Zone 3" {channel="mqtt:topic:mymqtt:dsc:zone3"} * * DSC Aux(-) --- esp8266 Ground * - * +--- dscClockPin (esp8266: D1, D2, D8) + * +--- dscClockPin // Default: D1, GPIO 5 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp8266: D1, D2, D8) + * +--- dscReadPin // Default: D2, GPIO 4 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: D8, GPIO 15 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -123,6 +131,9 @@ Contact zone3 "Zone 3" {channel="mqtt:topic:mymqtt:dsc:zone3"} * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -130,7 +141,7 @@ Contact zone3 "Zone 3" {channel="mqtt:topic:mymqtt:dsc:zone3"} // Settings const char* wifiSSID = ""; const char* wifiPassword = ""; -const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm based on panel configuration. +const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm or enable command outputs based on panel configuration. const char* mqttServer = ""; // MQTT server domain name or IP address const int mqttPort = 1883; // MQTT server port const char* mqttUsername = ""; // Optional, leave blank if not required @@ -151,12 +162,17 @@ const char* mqttSubscribeTopic = "dsc/Set"; // Receives messages to w // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. -#define dscClockPin D1 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscReadPin D2 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscWritePin D8 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 +#define dscWritePin D8 // GPIO 15 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif WiFiClient ipClient; PubSubClient mqtt(mqttServer, mqttPort, ipClient); unsigned long mqttPreviousTime; @@ -168,7 +184,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -198,7 +214,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -211,7 +227,7 @@ void loop() { else mqtt.publish(mqttStatusTopic, mqttLwtMessage, true); } - // Sends the access code when needed by the panel for arming + // Sends the access code when needed by the panel for arming or command outputs if (dsc.accessCodePrompt) { dsc.accessCodePrompt = false; dsc.write(accessCode); @@ -461,6 +477,7 @@ void publishMessage(const char* sourceTopic, byte partition) { case 0x03: mqtt.publish(publishTopic, "Zones open", true); break; case 0x04: mqtt.publish(publishTopic, "Armed stay", true); break; case 0x05: mqtt.publish(publishTopic, "Armed away", true); break; + case 0x06: mqtt.publish(publishTopic, "No entry delay", true); break; case 0x07: mqtt.publish(publishTopic, "Failed to arm", true); break; case 0x08: mqtt.publish(publishTopic, "Exit delay", true); break; case 0x09: mqtt.publish(publishTopic, "No entry delay", true); break; diff --git a/examples/esp8266/Pushbullet/Pushbullet.ino b/examples/esp8266/Pushbullet/Pushbullet.ino index 8765531..88395a6 100644 --- a/examples/esp8266/Pushbullet/Pushbullet.ino +++ b/examples/esp8266/Pushbullet/Pushbullet.ino @@ -1,5 +1,5 @@ /* - * Pushbullet Push Notification 1.4 (esp8266) + * Pushbullet Push Notification 1.5 (esp8266) * * Processes the security system status and demonstrates how to send a push notification when the status has changed. * This example sends notifications via Pushbullet: https://www.pushbullet.com @@ -11,6 +11,8 @@ * 4. Upload the sketch. * * Release notes: + * 1.5 - Update HTTPS root certificate for api.pushbullet.com + * Added DSC Classic series support * 1.4 - Add HTTPS certificate validation, add customizable message prefix * 1.3 - Updated esp8266 wiring diagram for 33k/10k resistors * 1.2 - Check if WiFi disconnects and wait to send updates until reconnection @@ -25,23 +27,20 @@ * * DSC Aux(-) --- esp8266 Ground * - * +--- dscClockPin (esp8266: D1, D2, D8) + * +--- dscClockPin // Default: D1, GPIO 5 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp8266: D1, D2, D8) + * +--- dscReadPin // Default: D2, GPIO 4 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): - * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) - * Ground --- NPN emitter --/ - * - * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should - * be suitable, for example: - * -- 2N3904 - * -- BC547, BC548, BC549 + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground * * Issues and (especially) pull requests are welcome: * https://github.com/taligentx/dscKeybusInterface @@ -49,6 +48,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include @@ -59,40 +61,51 @@ const char* pushbulletToken = ""; // Set the access token generated in the Push const char* messagePrefix = "[Security system] "; // Set a prefix for all messages // Configures the Keybus interface with the specified pins. -#define dscClockPin D1 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscReadPin D2 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 -// HTTPS root certificate for api.pushbullet.com: GlobalSign Root CA - R2, expires 2021.12.15 +// HTTPS root certificate for api.pushbullet.com: Google Trust Services GTS Root R1, expires 2036.06.21 const char pushbulletCertificateRoot[] = R"=EOF=( -----BEGIN CERTIFICATE----- -MIIESjCCAzKgAwIBAgINAeO0nXfN9AwGGRa24zANBgkqhkiG9w0BAQsFADBMMSAw -HgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEGA1UEChMKR2xvYmFs -U2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjAeFw0xNzA2MTUwMDAwNDJaFw0yMTEy -MTUwMDAwNDJaMEIxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVHb29nbGUgVHJ1c3Qg -U2VydmljZXMxEzARBgNVBAMTCkdUUyBDQSAxRDIwggEiMA0GCSqGSIb3DQEBAQUA -A4IBDwAwggEKAoIBAQCy2Xvh4dc/HJFy//kQzYcVeXS3PkeLsmFV/Qw2xn53Qjqy -+lJbC3GB1k3V6SskTSNeiytyXyFVtSnvRMvrglKrPiekkklBSt6o3THgPN9tek0t -1m0JsA7jYfKy/pBsWnsQZEm0CzwI8up5DGymGolqVjKgKaIwgo+BUQzzornZdbki -nicUukovLGNYh/FdEOZfkbu5W8xH4h51toyPzHVdVwXngsaEDnRyKss7VfVucOtm -acMkuziTNZtoYS+b1q6md3J8cUhYMxCv6YCCHbUHQBv2PeyirUedtJQpNLOML80l -A1g1wCWkVV/hswdWPcjQY7gg+4wdQyz4+anV7G+XAgMBAAGjggEzMIIBLzAOBgNV -HQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMBIGA1Ud -EwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFLHdMl3otzdy0s5czib+R3niAQjpMB8G -A1UdIwQYMBaAFJviB1dnHB7AagbeWbSaLd/cGYYuMDUGCCsGAQUFBwEBBCkwJzAl -BggrBgEFBQcwAYYZaHR0cDovL29jc3AucGtpLmdvb2cvZ3NyMjAyBgNVHR8EKzAp -MCegJaAjhiFodHRwOi8vY3JsLnBraS5nb29nL2dzcjIvZ3NyMi5jcmwwPwYDVR0g -BDgwNjA0BgZngQwBAgEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly9wa2kuZ29vZy9y -ZXBvc2l0b3J5LzANBgkqhkiG9w0BAQsFAAOCAQEAcUrEwyOu9+OyAnmME+hTjoDF -8OPvcWCpqXs0ZYU0vUc7A1cWAJlIOuDg8OrNtkg81aty8NAby2QtOw10aNd0iDF8 -aroO8IxNeM7aEPSKlkWXqZetxTUaGGTok7YNnR+5Xh2A6udbnI6uDqaE0tEXzrP7 -9oFPPOZon8/xpnbFfafz3X1YD+D2YQEcUY52MytInVyBUXIIF7r9AdPuRvn0smhA -mTEBbE8bxlbrgXPSeVIFkiZbcc2dxNLOI3cPQXppXiElxvi3/3r3R97CAHucWkWc -Kk5GkNl1LNj/jO7M3GnrbOYV0KP/SAusVd/fJZ1CtlGjZpVgxdAi5yJ6UaXMhw== +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c -----END CERTIFICATE----- )=EOF="; // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin); +#endif X509List pushbulletCert(pushbulletCertificateRoot); WiFiClientSecure ipClient; bool wifiConnected = true; @@ -104,7 +117,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); ipClient.setTrustAnchors(&pushbulletCert); @@ -115,7 +128,7 @@ void setup() { Serial.print(F("connected: ")); Serial.println(WiFi.localIP()); - Serial.print(F("NTP time")); + Serial.print(F("NTP time....")); configTime(0, 0, "pool.ntp.org"); time_t now = time(nullptr); while (now < 24 * 3600) @@ -158,7 +171,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -180,11 +193,11 @@ void loop() { // Checks armed status if (dsc.armedChanged[partition]) { if (dsc.armed[partition]) { - char messageContent[25]; + char messageContent[30]; - if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night: Partition "); + if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night away: Partition "); else if (dsc.armedAway[partition]) strcpy(messageContent, "Armed away: Partition "); - else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night: Partition "); + else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night stay: Partition "); else if (dsc.armedStay[partition]) strcpy(messageContent, "Armed stay: Partition "); appendPartition(partition, messageContent); // Appends the message with the partition number @@ -330,7 +343,7 @@ bool sendMessage(const char* messageContent) { ipClient.println(F("Accept: */*")); ipClient.println(F("Content-Type: application/json")); ipClient.print(F("Content-Length: ")); - ipClient.println(strlen(messageContent) + strlen(messagePrefix) + 25); // Length including JSON data + ipClient.println(strlen(messagePrefix) + strlen(messageContent) + 25); ipClient.print(F("Access-Token: ")); ipClient.println(pushbulletToken); ipClient.println(); diff --git a/examples/esp8266/Pushover/Pushover.ino b/examples/esp8266/Pushover/Pushover.ino new file mode 100644 index 0000000..6d8c1c0 --- /dev/null +++ b/examples/esp8266/Pushover/Pushover.ino @@ -0,0 +1,385 @@ +/* + * Pushover Push Notification 1.0 (esp8266) + * + * Processes the security system status and demonstrates how to send a push notification when the status has changed. + * This example sends notifications via Pushover: https://www.pushover.net + * + * Usage: + * 1. Set the WiFi SSID and password in the sketch. + * 2. Create a Pushover account: https://www.pushover.net + * 3. Copy the user key to pushoverUserKey. + * 4. Create a Pushover application to get an API token: https://pushover.net/apps/build + * 5. Copy the API token to pushoverAPIToken. + * 6. Upload the sketch. + * + * Release notes: + * 1.0 - Initial release + * + * Wiring: + * DSC Aux(+) --- 5v voltage regulator --- esp8266 development board 5v pin (NodeMCU, Wemos) + * + * DSC Aux(-) --- esp8266 Ground + * + * +--- dscClockPin // Default: D1, GPIO 5 + * DSC Yellow --- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * +--- dscReadPin // Default: D2, GPIO 4 + * DSC Green ---- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Issues and (especially) pull requests are welcome: + * https://github.com/taligentx/dscKeybusInterface + * + * This example code is in the public domain. + */ + +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + +#include +#include + +// Settings +const char* wifiSSID = ""; +const char* wifiPassword = ""; +const char* pushoverUserKey = ""; // Set the user key generated in the Pushover account settings +const char* pushoverAPIToken = ""; // Set the API token generated in the Pushover account settings +const char* messagePrefix = "[Security system] "; // Set a prefix for all messages + +// Configures the Keybus interface with the specified pins. +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 + +// HTTPS root certificate for api.pushover.net: DigiCert Global Root CA, expires 2031.11.10 +const char pushoverCertificateRoot[] = R"=EOF=( +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- +)=EOF="; + +// Initialize components +#ifndef dscClassicSeries +dscKeybusInterface dsc(dscClockPin, dscReadPin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin); +#endif +X509List pushoverCert(pushoverCertificateRoot); +WiFiClientSecure ipClient; +bool wifiConnected = true; + + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println(); + Serial.println(); + + Serial.print(F("WiFi....")); + WiFi.mode(WIFI_STA); + WiFi.begin(wifiSSID, wifiPassword); + ipClient.setTrustAnchors(&pushoverCert); + while (WiFi.status() != WL_CONNECTED) { + Serial.print("."); + delay(500); + } + Serial.print(F("connected: ")); + Serial.println(WiFi.localIP()); + + Serial.print(F("NTP time....")); + configTime(0, 0, "pool.ntp.org"); + time_t now = time(nullptr); + while (now < 24 * 3600) + { + Serial.print("."); + delay(2000); + now = time(nullptr); + } + Serial.println(F("synchronized.")); + + // Sends a message on startup to verify connectivity + Serial.print(F("Pushover....")); + if (sendMessage("Initializing")) Serial.println(F("connected.")); + else Serial.println(F("connection error.")); + + // Starts the Keybus interface + dsc.begin(); + Serial.println(F("DSC Keybus Interface is online.")); +} + + +void loop() { + + // Updates status if WiFi drops and reconnects + if (!wifiConnected && WiFi.status() == WL_CONNECTED) { + Serial.println("WiFi reconnected"); + wifiConnected = true; + dsc.pauseStatus = false; + dsc.statusChanged = true; + } + else if (WiFi.status() != WL_CONNECTED && wifiConnected) { + Serial.println("WiFi disconnected"); + wifiConnected = false; + dsc.pauseStatus = true; + } + + dsc.loop(); + + if (dsc.statusChanged) { // Checks if the security system status has changed + dsc.statusChanged = false; // Reset the status tracking flag + + // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h + if (dsc.bufferOverflow) { + Serial.println(F("Keybus buffer overflow")); + dsc.bufferOverflow = false; + } + + // Checks if the interface is connected to the Keybus + if (dsc.keybusChanged) { + dsc.keybusChanged = false; // Resets the Keybus data status flag + if (dsc.keybusConnected) sendMessage("Connected"); + else sendMessage("Disconnected"); + } + + // Checks status per partition + for (byte partition = 0; partition < dscPartitions; partition++) { + + // Skips processing if the partition is disabled or in installer programming + if (dsc.disabled[partition]) continue; + + // Checks armed status + if (dsc.armedChanged[partition]) { + if (dsc.armed[partition]) { + char messageContent[30]; + + if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night away: Partition "); + else if (dsc.armedAway[partition]) strcpy(messageContent, "Armed away: Partition "); + else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night stay: Partition "); + else if (dsc.armedStay[partition]) strcpy(messageContent, "Armed stay: Partition "); + + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else { + char messageContent[22] = "Disarmed: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + + // Checks exit delay status + if (dsc.exitDelayChanged[partition]) { + dsc.exitDelayChanged[partition] = false; // Resets the exit delay status flag + + if (dsc.exitDelay[partition]) { + char messageContent[36] = "Exit delay in progress: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else if (!dsc.exitDelay[partition] && !dsc.armed[partition]) { + char messageContent[22] = "Disarmed: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + + // Checks alarm triggered status + if (dsc.alarmChanged[partition]) { + dsc.alarmChanged[partition] = false; // Resets the partition alarm status flag + + if (dsc.alarm[partition]) { + char messageContent[19] = "Alarm: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else if (!dsc.armedChanged[partition]) { + char messageContent[22] = "Disarmed: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + dsc.armedChanged[partition] = false; // Resets the partition armed status flag + + // Checks fire alarm status + if (dsc.fireChanged[partition]) { + dsc.fireChanged[partition] = false; // Resets the fire status flag + + if (dsc.fire[partition]) { + char messageContent[24] = "Fire alarm: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else { + char messageContent[33] = "Fire alarm restored: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + } + + // Checks for zones in alarm + // Zone alarm status is stored in the alarmZones[] and alarmZonesChanged[] arrays using 1 bit per zone, up to 64 zones + // alarmZones[0] and alarmZonesChanged[0]: Bit 0 = Zone 1 ... Bit 7 = Zone 8 + // alarmZones[1] and alarmZonesChanged[1]: Bit 0 = Zone 9 ... Bit 7 = Zone 16 + // ... + // alarmZones[7] and alarmZonesChanged[7]: Bit 0 = Zone 57 ... Bit 7 = Zone 64 + if (dsc.alarmZonesStatusChanged) { + dsc.alarmZonesStatusChanged = false; // Resets the alarm zones status flag + for (byte zoneGroup = 0; zoneGroup < dscZones; zoneGroup++) { + for (byte zoneBit = 0; zoneBit < 8; zoneBit++) { + if (bitRead(dsc.alarmZonesChanged[zoneGroup], zoneBit)) { // Checks an individual alarm zone status flag + bitWrite(dsc.alarmZonesChanged[zoneGroup], zoneBit, 0); // Resets the individual alarm zone status flag + if (bitRead(dsc.alarmZones[zoneGroup], zoneBit)) { // Zone alarm + char messageContent[15] = "Zone alarm: "; + char zoneNumber[3]; + itoa((zoneBit + 1 + (zoneGroup * 8)), zoneNumber, 10); // Determines the zone number + strcat(messageContent, zoneNumber); + sendMessage(messageContent); + } + else { + char messageContent[24] = "Zone alarm restored: "; + char zoneNumber[3]; + itoa((zoneBit + 1 + (zoneGroup * 8)), zoneNumber, 10); // Determines the zone number + strcat(messageContent, zoneNumber); + sendMessage(messageContent); + } + } + } + } + } + + // Checks trouble status + if (dsc.troubleChanged) { + dsc.troubleChanged = false; // Resets the trouble status flag + if (dsc.trouble) sendMessage("Trouble status on"); + else sendMessage("Trouble status restored"); + } + + // Checks for AC power status + if (dsc.powerChanged) { + dsc.powerChanged = false; // Resets the battery trouble status flag + if (dsc.powerTrouble) sendMessage("AC power trouble"); + else sendMessage("AC power restored"); + } + + // Checks panel battery status + if (dsc.batteryChanged) { + dsc.batteryChanged = false; // Resets the battery trouble status flag + if (dsc.batteryTrouble) sendMessage("Panel battery trouble"); + else sendMessage("Panel battery restored"); + } + + // Checks for keypad fire alarm status + if (dsc.keypadFireAlarm) { + dsc.keypadFireAlarm = false; // Resets the keypad fire alarm status flag + sendMessage("Keypad Fire alarm"); + } + + // Checks for keypad aux auxiliary alarm status + if (dsc.keypadAuxAlarm) { + dsc.keypadAuxAlarm = false; // Resets the keypad auxiliary alarm status flag + sendMessage("Keypad Aux alarm"); + } + + // Checks for keypad panic alarm status + if (dsc.keypadPanicAlarm) { + dsc.keypadPanicAlarm = false; // Resets the keypad panic alarm status flag + sendMessage("Keypad Panic alarm"); + } + } +} + + +bool sendMessage(const char* messageContent) { + if (!ipClient.connect("api.pushover.net", 443)) return false; + ipClient.println(F("POST /1/messages.json HTTP/1.1")); + ipClient.println(F("Host: api.pushover.net")); + ipClient.println(F("User-Agent: ESP8266")); + ipClient.println(F("Accept: */*")); + ipClient.println(F("Content-Type: application/json")); + ipClient.print(F("Content-Length: ")); + ipClient.println(strlen(pushoverAPIToken) + strlen(pushoverUserKey) + strlen(messagePrefix) + strlen(messageContent) + 35); + ipClient.println(); + ipClient.print(F("{\"token\":\"")); + ipClient.print(pushoverAPIToken); + ipClient.print(F("\",\"user\":\"")); + ipClient.print(pushoverUserKey); + ipClient.print(F("\",\"message\":\"")); + ipClient.print(messagePrefix); + ipClient.print(messageContent); + ipClient.print(F("\"}")); + + // Waits for a response + unsigned long previousMillis = millis(); + while (!ipClient.available()) { + dsc.loop(); + if (millis() - previousMillis > 3000) { + Serial.println(); + Serial.println(F("Connection timed out waiting for a response.")); + ipClient.stop(); + return false; + } + } + + // Reads the response until the first space - the next characters will be the HTTP status code + while (ipClient.available()) { + if (ipClient.read() == ' ') break; + } + + // Checks the first character of the HTTP status code - the message was sent successfully if the status code + // begins with "2" + char statusCode = ipClient.read(); + + // Successful, reads the remaining response to clear the client buffer + if (statusCode == '2') { + while (ipClient.available()) ipClient.read(); + ipClient.stop(); + return true; + } + + // Unsuccessful, prints the response to serial to help debug + else { + Serial.println(); + Serial.println(F("Push notification error, response:")); + Serial.print(statusCode); + while (ipClient.available()) Serial.print((char)ipClient.read()); + Serial.println(); + ipClient.stop(); + return false; + } +} + + +void appendPartition(byte sourceNumber, char* messageContent) { + char partitionNumber[2]; + itoa(sourceNumber + 1, partitionNumber, 10); + strcat(messageContent, partitionNumber); +} diff --git a/examples/esp8266/Pushsafer/Pushsafer.ino b/examples/esp8266/Pushsafer/Pushsafer.ino new file mode 100644 index 0000000..91e18b0 --- /dev/null +++ b/examples/esp8266/Pushsafer/Pushsafer.ino @@ -0,0 +1,422 @@ +/* + * Pushsafer Push Notification 1.0 (esp8266) + * + * Processes the security system status and demonstrates how to send a push notification when the status has changed. + * This example sends notifications via Pushsafer: https://www.pushsafer.com + * + * Usage: + * 1. Set the WiFi SSID and password in the sketch. + * 2. Create a Pushsafer account: https://www.pushsafer.com + * 3. Copy the private key to pushsaferKey. + * 4. Upload the sketch. + * + * Release notes: + * 1.0 - Initial release + * + * Wiring: + * DSC Aux(+) --- 5v voltage regulator --- esp8266 development board 5v pin (NodeMCU, Wemos) + * + * DSC Aux(-) --- esp8266 Ground + * + * +--- dscClockPin // Default: D1, GPIO 5 + * DSC Yellow --- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * +--- dscReadPin // Default: D2, GPIO 4 + * DSC Green ---- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Issues and (especially) pull requests are welcome: + * https://github.com/taligentx/dscKeybusInterface + * + * This example code is in the public domain. + */ + +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + +#include +#include + +// Settings +const char* wifiSSID = ""; +const char* wifiPassword = ""; +const char* pushsaferKey = ""; // Set the private key generated in the Pushsafer account settings +const char* messagePrefix = "[Security system] "; // Set a prefix for all messages + +// Configures the Keybus interface with the specified pins. +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 + +// HTTPS root certificate for www.pushsafer.com: ISRG Root X1, expires 2035.06.04 +const char pushsaferCertificateRoot[] = R"=EOF=( +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- +)=EOF="; + +// Initialize components +#ifndef dscClassicSeries +dscKeybusInterface dsc(dscClockPin, dscReadPin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin); +#endif +X509List pushsaferCert(pushsaferCertificateRoot); +WiFiClientSecure ipClient; +bool wifiConnected = true; +char encodedMessagePrefix[128], encodedMessageContent[480]; + + +void setup() { + Serial.begin(115200); + delay(1000); + Serial.println(); + Serial.println(); + + Serial.print(F("WiFi....")); + WiFi.mode(WIFI_STA); + WiFi.begin(wifiSSID, wifiPassword); + ipClient.setTrustAnchors(&pushsaferCert); + while (WiFi.status() != WL_CONNECTED) { + Serial.print("."); + delay(500); + } + Serial.print(F("connected: ")); + Serial.println(WiFi.localIP()); + + Serial.print(F("NTP time....")); + configTime(0, 0, "pool.ntp.org"); + time_t now = time(nullptr); + while (now < 24 * 3600) + { + Serial.print("."); + delay(2000); + now = time(nullptr); + } + Serial.println(F("synchronized.")); + + // Encodes message prefix in URL encoding + encodeURL(messagePrefix, encodedMessagePrefix); + + // Sends a message on startup to verify connectivity + Serial.print(F("Pushsafer....")); + if (sendMessage("Initializing")) Serial.println(F("connected.")); + else Serial.println(F("connection error.")); + + // Starts the Keybus interface + dsc.begin(); + Serial.println(F("DSC Keybus Interface is online.")); +} + + +void loop() { + + // Updates status if WiFi drops and reconnects + if (!wifiConnected && WiFi.status() == WL_CONNECTED) { + Serial.println("WiFi reconnected"); + wifiConnected = true; + dsc.pauseStatus = false; + dsc.statusChanged = true; + } + else if (WiFi.status() != WL_CONNECTED && wifiConnected) { + Serial.println("WiFi disconnected"); + wifiConnected = false; + dsc.pauseStatus = true; + } + + dsc.loop(); + + if (dsc.statusChanged) { // Checks if the security system status has changed + dsc.statusChanged = false; // Reset the status tracking flag + + // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h + if (dsc.bufferOverflow) { + Serial.println(F("Keybus buffer overflow")); + dsc.bufferOverflow = false; + } + + // Checks if the interface is connected to the Keybus + if (dsc.keybusChanged) { + dsc.keybusChanged = false; // Resets the Keybus data status flag + if (dsc.keybusConnected) sendMessage("Connected"); + else sendMessage("Disconnected"); + } + + // Checks status per partition + for (byte partition = 0; partition < dscPartitions; partition++) { + + // Skips processing if the partition is disabled or in installer programming + if (dsc.disabled[partition]) continue; + + // Checks armed status + if (dsc.armedChanged[partition]) { + if (dsc.armed[partition]) { + char messageContent[30]; + + if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night away: Partition "); + else if (dsc.armedAway[partition]) strcpy(messageContent, "Armed away: Partition "); + else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night stay: Partition "); + else if (dsc.armedStay[partition]) strcpy(messageContent, "Armed stay: Partition "); + + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else { + char messageContent[22] = "Disarmed: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + + // Checks exit delay status + if (dsc.exitDelayChanged[partition]) { + dsc.exitDelayChanged[partition] = false; // Resets the exit delay status flag + + if (dsc.exitDelay[partition]) { + char messageContent[36] = "Exit delay in progress: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else if (!dsc.exitDelay[partition] && !dsc.armed[partition]) { + char messageContent[22] = "Disarmed: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + + // Checks alarm triggered status + if (dsc.alarmChanged[partition]) { + dsc.alarmChanged[partition] = false; // Resets the partition alarm status flag + + if (dsc.alarm[partition]) { + char messageContent[19] = "Alarm: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else if (!dsc.armedChanged[partition]) { + char messageContent[22] = "Disarmed: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + dsc.armedChanged[partition] = false; // Resets the partition armed status flag + + // Checks fire alarm status + if (dsc.fireChanged[partition]) { + dsc.fireChanged[partition] = false; // Resets the fire status flag + + if (dsc.fire[partition]) { + char messageContent[24] = "Fire alarm: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else { + char messageContent[33] = "Fire alarm restored: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + } + + // Checks for zones in alarm + // Zone alarm status is stored in the alarmZones[] and alarmZonesChanged[] arrays using 1 bit per zone, up to 64 zones + // alarmZones[0] and alarmZonesChanged[0]: Bit 0 = Zone 1 ... Bit 7 = Zone 8 + // alarmZones[1] and alarmZonesChanged[1]: Bit 0 = Zone 9 ... Bit 7 = Zone 16 + // ... + // alarmZones[7] and alarmZonesChanged[7]: Bit 0 = Zone 57 ... Bit 7 = Zone 64 + if (dsc.alarmZonesStatusChanged) { + dsc.alarmZonesStatusChanged = false; // Resets the alarm zones status flag + for (byte zoneGroup = 0; zoneGroup < dscZones; zoneGroup++) { + for (byte zoneBit = 0; zoneBit < 8; zoneBit++) { + if (bitRead(dsc.alarmZonesChanged[zoneGroup], zoneBit)) { // Checks an individual alarm zone status flag + bitWrite(dsc.alarmZonesChanged[zoneGroup], zoneBit, 0); // Resets the individual alarm zone status flag + if (bitRead(dsc.alarmZones[zoneGroup], zoneBit)) { // Zone alarm + char messageContent[15] = "Zone alarm: "; + char zoneNumber[3]; + itoa((zoneBit + 1 + (zoneGroup * 8)), zoneNumber, 10); // Determines the zone number + strcat(messageContent, zoneNumber); + sendMessage(messageContent); + } + else { + char messageContent[24] = "Zone alarm restored: "; + char zoneNumber[3]; + itoa((zoneBit + 1 + (zoneGroup * 8)), zoneNumber, 10); // Determines the zone number + strcat(messageContent, zoneNumber); + sendMessage(messageContent); + } + } + } + } + } + + // Checks trouble status + if (dsc.troubleChanged) { + dsc.troubleChanged = false; // Resets the trouble status flag + if (dsc.trouble) sendMessage("Trouble status on"); + else sendMessage("Trouble status restored"); + } + + // Checks for AC power status + if (dsc.powerChanged) { + dsc.powerChanged = false; // Resets the battery trouble status flag + if (dsc.powerTrouble) sendMessage("AC power trouble"); + else sendMessage("AC power restored"); + } + + // Checks panel battery status + if (dsc.batteryChanged) { + dsc.batteryChanged = false; // Resets the battery trouble status flag + if (dsc.batteryTrouble) sendMessage("Panel battery trouble"); + else sendMessage("Panel battery restored"); + } + + // Checks for keypad fire alarm status + if (dsc.keypadFireAlarm) { + dsc.keypadFireAlarm = false; // Resets the keypad fire alarm status flag + sendMessage("Keypad Fire alarm"); + } + + // Checks for keypad aux auxiliary alarm status + if (dsc.keypadAuxAlarm) { + dsc.keypadAuxAlarm = false; // Resets the keypad auxiliary alarm status flag + sendMessage("Keypad Aux alarm"); + } + + // Checks for keypad panic alarm status + if (dsc.keypadPanicAlarm) { + dsc.keypadPanicAlarm = false; // Resets the keypad panic alarm status flag + sendMessage("Keypad Panic alarm"); + } + } +} + + +bool sendMessage(const char* messageContent) { + encodeURL(messageContent, encodedMessageContent); // Encodes message content in URL encoding + + if (!ipClient.connect("www.pushsafer.com", 443)) return false; + ipClient.println(F("POST /api HTTP/1.1")); + ipClient.println(F("Host: www.pushsafer.com")); + ipClient.println(F("User-Agent: ESP8266")); + ipClient.println(F("Accept: */*")); + ipClient.println(F("Content-Type: application/x-www-form-urlencoded")); + ipClient.print(F("Content-Length: ")); + ipClient.println(strlen(pushsaferKey) + strlen(encodedMessagePrefix) + strlen(encodedMessageContent) + 5); + ipClient.println(); + ipClient.print(F("k=")); + ipClient.print(pushsaferKey); + ipClient.print(F("&m=")); + ipClient.print(encodedMessagePrefix); + ipClient.print(encodedMessageContent); + + // Waits for a response + unsigned long previousMillis = millis(); + while (!ipClient.available()) { + dsc.loop(); + if (millis() - previousMillis > 3000) { + Serial.println(); + Serial.println(F("Connection timed out waiting for a response.")); + ipClient.stop(); + return false; + } + } + + // Reads the response until the first space - the next characters will be the HTTP status code + while (ipClient.available()) { + if (ipClient.read() == ' ') break; + } + + // Checks the first character of the HTTP status code - the message was sent successfully if the status code + // begins with "2" + char statusCode = ipClient.read(); + + // Successful, reads the remaining response to clear the client buffer + if (statusCode == '2') { + while (ipClient.available()) ipClient.read(); + ipClient.stop(); + return true; + } + + // Unsuccessful, prints the response to serial to help debug + else { + Serial.println(); + Serial.println(F("Push notification error, response:")); + Serial.print(statusCode); + while (ipClient.available()) Serial.print((char)ipClient.read()); + Serial.println(); + ipClient.stop(); + return false; + } +} + + +void appendPartition(byte sourceNumber, char* messageContent) { + char partitionNumber[2]; + itoa(sourceNumber + 1, partitionNumber, 10); + strcat(messageContent, partitionNumber); +} + + +// Helper for encodeURL() +static char encodeHex(char c) { + return "0123456789ABCDEF"[c & 0x0F]; +} + + +// Encodes a char array to URL encoded using '+' for spaces as required for application/x-www-form-urlencoded +char *encodeURL(const char *src, char *dst) { + char c, *d = dst; + while (c = *src++) { + if (c == ' ') { + *d++ = '+'; + continue; + } + else if (!('a' <= c && c <= 'z') + && !('A' <= c && c <= 'Z') + && !('0' <= c && c <= '9')) { + *d++ = '%'; + *d++ = encodeHex(c >> 4); + c = encodeHex(c); + } + *d++ = c; + } + *d = '\0'; + return dst; +} diff --git a/examples/esp8266/Status/Status.ino b/examples/esp8266/Status/Status.ino index 219d72e..1d4ccc8 100644 --- a/examples/esp8266/Status/Status.ino +++ b/examples/esp8266/Status/Status.ino @@ -1,11 +1,12 @@ /* - * DSC Status 1.3 (esp8266) + * DSC Status 1.4 (esp8266) * * Processes and prints the security system status to a serial interface, including reading from serial for the * virtual keypad. This demonstrates how to determine if the security system status has changed, what has * changed, and how to take action based on those changes. * * Release notes: + * 1.4 - Added DSC Classic series support * 1.3 - Added PGM outputs 1-14 status * 1.2 - Updated esp8266 wiring diagram for 33k/10k resistors * 1.1 - Added partition ready, access code, and timestamp status @@ -16,17 +17,24 @@ * * DSC Aux(-) --- esp8266 Ground * - * +--- dscClockPin (esp8266: D1, D2, D8) + * +--- dscClockPin // Default: D1, GPIO 5 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp8266: D1, D2, D8) + * +--- dscReadPin // Default: D2, GPIO 4 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: D8, GPIO 15 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -40,16 +48,24 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include // Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the // virtual keypad. -#define dscClockPin D1 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscReadPin D2 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscWritePin D8 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 +#define dscWritePin D8 // GPIO 15 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin); +#endif void setup() { @@ -76,7 +92,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -98,7 +114,7 @@ void loop() { if (dsc.disabled[partition]) { Serial.print(F("Partition ")); Serial.print(partition + 1); - Serial.println(F(" disabled")); + Serial.println(F(": Disabled")); } } if (dsc.disabled[partition]) continue; @@ -109,12 +125,12 @@ void loop() { if (dsc.ready[partition]) { Serial.print(F("Partition ")); Serial.print(partition + 1); - Serial.println(F(" ready")); + Serial.println(F(": Ready")); } else { Serial.print(F("Partition ")); Serial.print(partition + 1); - Serial.println(F(" not ready")); + Serial.println(F(": Not ready")); } } diff --git a/examples/esp8266/Telegram/Telegram.ino b/examples/esp8266/Telegram/Telegram.ino index 1acedc5..a45e48b 100644 --- a/examples/esp8266/Telegram/Telegram.ino +++ b/examples/esp8266/Telegram/Telegram.ino @@ -1,5 +1,5 @@ /* - * Telegram Bot 1.0 (esp8266) + * Telegram Bot 1.1 (esp8266) * * Processes the security system status and allows for control via a Telegram bot: https://www.telegram.org * @@ -27,6 +27,7 @@ * - Disarm: /disarm * * Release notes: + * 1.1 - Added DSC Classic series support * 1.0 - Initial release * * Wiring: @@ -34,17 +35,24 @@ * * DSC Aux(-) --- esp8266 Ground * - * +--- dscClockPin (esp8266: D1, D2, D8) + * +--- dscClockPin // Default: D1, GPIO 5 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp8266: D1, D2, D8) + * +--- dscReadPin // Default: D2, GPIO 4 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: D8, GPIO 15 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -58,6 +66,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -67,18 +78,23 @@ // Settings const char* wifiSSID = ""; const char* wifiPassword = ""; -const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm (based on panel configuration) +const char* accessCode = ""; // An access code is required to disarm/night arm and may be required to arm or enable command outputs based on panel configuration. const char* telegramBotToken = ""; // Set the Telegram bot access token const char* telegramUserID = ""; // Set the Telegram chat user ID const char* messagePrefix = "[Security system] "; // Set a prefix for all messages // Configures the Keybus interface with the specified pins. -#define dscClockPin D1 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscReadPin D2 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscWritePin D8 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 +#define dscWritePin D8 // GPIO 15 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif X509List telegramCert(TELEGRAM_CERTIFICATE_ROOT); WiFiClientSecure ipClient; UniversalTelegramBot telegramBot(telegramBotToken, ipClient); @@ -92,7 +108,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); ipClient.setTrustAnchors(&telegramCert); @@ -103,7 +119,7 @@ void setup() { Serial.print(F("connected: ")); Serial.println(WiFi.localIP()); - Serial.print(F("NTP time")); + Serial.print(F("NTP time....")); configTime(0, 0, "pool.ntp.org"); time_t now = time(nullptr); while (now < 24 * 3600) @@ -157,7 +173,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -170,7 +186,7 @@ void loop() { else sendMessage("Disconnected"); } - // Sends the access code when needed by the panel for arming + // Sends the access code when needed by the panel for arming or command outputs if (dsc.accessCodePrompt) { dsc.accessCodePrompt = false; dsc.write(accessCode); @@ -185,11 +201,11 @@ void loop() { // Checks armed status if (dsc.armedChanged[partition]) { if (dsc.armed[partition]) { - char messageContent[25]; + char messageContent[30]; - if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night: Partition "); + if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night away: Partition "); else if (dsc.armedAway[partition]) strcpy(messageContent, "Armed away: Partition "); - else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night: Partition "); + else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night stay: Partition "); else if (dsc.armedStay[partition]) strcpy(messageContent, "Armed stay: Partition "); appendPartition(partition, messageContent); // Appends the message with the partition number diff --git a/examples/esp8266/TimeSyncNTP/TimeSyncNTP.ino b/examples/esp8266/TimeSyncNTP/TimeSyncNTP.ino index d514ea8..59e3fc7 100644 --- a/examples/esp8266/TimeSyncNTP/TimeSyncNTP.ino +++ b/examples/esp8266/TimeSyncNTP/TimeSyncNTP.ino @@ -14,17 +14,17 @@ * * DSC Aux(-) --- esp8266 Ground * - * +--- dscClockPin (esp8266: D1, D2, D8) + * +--- dscClockPin // Default: D1, GPIO 5 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp8266: D1, D2, D8) + * +--- dscReadPin // Default: D2, GPIO 4 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad: + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: D8, GPIO 15 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -52,9 +52,9 @@ const char* ntpServer = "pool.ntp.org"; // Set the NTP server const byte timePartition = 1; // Set the partition to use for setting the time // Configures the Keybus interface with the specified pins. -#define dscClockPin D1 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscReadPin D2 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscWritePin D8 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscWritePin D8 // GPIO 15 // Initialize components dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); @@ -72,7 +72,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi...")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -82,7 +82,7 @@ void setup() { Serial.print(F("connected: ")); Serial.println(WiFi.localIP()); - Serial.print(F("NTP time...")); + Serial.print(F("NTP time....")); configTime(ntpTimeZone, ntpServer); // Initiates the NTP client, synced hourly time(&ntpNow); while (ntpNow < 1606784461) @@ -140,7 +140,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; diff --git a/examples/esp8266/TinyGSM-SMS/TinyGSM-SMS.ino b/examples/esp8266/TinyGSM-SMS/TinyGSM-SMS.ino new file mode 100644 index 0000000..2def9b1 --- /dev/null +++ b/examples/esp8266/TinyGSM-SMS/TinyGSM-SMS.ino @@ -0,0 +1,294 @@ +/* + * TinyGSM SMS Notification 1.0 (esp8266) + * + * Processes the security system status and demonstrates how to send an SMS text message when the status has + * changed. This example sends SMS text messages via a TinyGSM-compatible SIM800L module which is connected + * onto ESP8266 board. Make sure that Micro-SIM card have PIN Code turned off (disable SIM Lock from phone). + * + * Usage: + * 1. Install the TinyGSM library, available in the Arduino IDE Library Manager and the Platform.io Library + * Registry: https://github.com/vshymanskyy/TinyGSM + * 2. Set the destination phone numbers in the sketch settings. + * + * Release notes: + * 1.0 - Just converted from Arduino to ESP8266. Tested with SIM800L module. + * + * Wiring: + * DSC Aux(+) --- 5v voltage regulator --- esp8266 development board 5v pin (NodeMCU, Wemos) + * + * DSC Aux(-) --- esp8266 Ground + * + * +--- dscClockPin // Default: D1, GPIO 5 + * DSC Yellow --- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * +--- dscReadPin // Default: D2, GPIO 4 + * DSC Green ---- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Connecting SIM800L module with ESP8266: + * SIM800 RX ---- SIM800TxPin // Default: D5, GPIO 14 + * + * SIM800 TX ---- SIM800RxPin // Default: D6, GPIO 12 + * + * SIM800 GND --- esp8266 development board ground (NodeMCU, Wemos) + * + * SIM800 Vcc --- (+) 3.7 - 4.4V power supply which can supply burst current of 2A + * (you can use LM2596 Buck converter set to output about 4V and connected to Bell+ and Aux-) + * + * Virtual keypad (optional): + * DSC Green ---- NPN collector --\ + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: D8, GPIO 15 + * Ground --- NPN emitter --/ + * + * + * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should + * be suitable, for example: + * -- 2N3904 + * -- BC547, BC548, BC549 + * + * Issues and (especially) pull requests are welcome: + * https://github.com/taligentx/dscKeybusInterface + * + * Based on TinyGSM-SMS example for ESP32 by jvitkauskas: https://github.com/jvitkauskas + * + * This example code is in the public domain. + */ + +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + +// Configures GSM modem model. Must be done before including TinyGsmClient library. +#define TINY_GSM_MODEM_SIM800 +#include +#include +#include + +// Settings +const char* sendToPhoneNumbers[] = { + "+1234567890", + "+2345678901" +}; + +#define phone_number_count (sizeof (sendToPhoneNumbers) / sizeof (const char *)) + +// Configures the Keybus interface with the specified pins - dscWritePin is optional, leaving it out disables the +// virtual keypad. +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define SIM800TxPin D5 // GPIO 14 +#define SIM800RxPin D6 // GPIO 12 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 +#define dscWritePin D8 // GPIO 15 + +// Settings +bool notifyOnPartitionAlarm = true; +bool notifyOnPowerTroubles = true; +bool notifyOnKeypadAlarm = true; +bool notifyOnDisArming = false; +bool notifyOnTrouble = true; + +// Initialize components +SoftwareSerial serialSIM800(SIM800RxPin,SIM800TxPin); +#ifndef dscClassicSeries +dscKeybusInterface dsc(dscClockPin, dscReadPin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin); +#endif +TinyGsm modem(serialSIM800); + +void setup() { + Serial.begin(9600); + delay(1000); + Serial.println(); + Serial.println(); + + serialSIM800.begin(9600); + + while (!modem.isNetworkConnected()) { + Serial.print(F("GSM...")); + while (!modem.restart()) { + Serial.print("."); + } + Serial.println(); + + Serial.print(F("Waiting for network...")); + if (modem.waitForNetwork(600000L) && modem.isNetworkConnected()) { + Serial.println(F("connected.")); + } + else { + Serial.println(F("connection error.")); + } + } + + // Starts the Keybus interface + dsc.begin(); + Serial.println(F("DSC Keybus Interface is online.")); +} + + +void loop() { + modem.maintain(); + + dsc.loop(); + + if (dsc.statusChanged) { // Checks if the security system status has changed + dsc.statusChanged = false; // Reset the status tracking flag + + // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call + // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + if (dsc.bufferOverflow) { + Serial.println(F("Keybus buffer overflow")); + dsc.bufferOverflow = false; + } + + // Checks status per partition + for (byte partition = 0; partition < dscPartitions; partition++) { + + // Skips processing if the partition is disabled or in installer programming + if (dsc.disabled[partition]) continue; + + // Checks alarm triggered status + if (notifyOnPartitionAlarm) { + if (dsc.alarmChanged[partition]) { + dsc.alarmChanged[partition] = false; // Resets the partition alarm status flag + + if (dsc.alarm[partition]) { + char messageContent[19] = "Alarm: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else { + char messageContent[34] = "Disarmed after alarm: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + + if (dsc.fireChanged[partition]) { + dsc.fireChanged[partition] = false; // Resets the fire status flag + + if (dsc.fire[partition]) { + char messageContent[24] = "Fire alarm: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + else { + char messageContent[33] = "Fire alarm restored: Partition "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + } + + // Publishes armed/disarmed status + if (notifyOnDisArming) { + if (dsc.armedChanged[partition]) { + if (dsc.armed[partition]) { + // Night armed away + if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) { + char messageContent[33] = "Armed away - night: Partition: "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + + // Armed away + else if (dsc.armedAway[partition]) { + char messageContent[25] = "Armed away: Partition: "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + + // Night armed stay + else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) { + char messageContent[33] = "Armed stay - night: Partition: "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + + // Armed stay + else if (dsc.armedStay[partition]) { + char messageContent[25] = "Armed stay: Partition: "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + } + + // Disarmed + else { + char messageContent[23] = "Disarmed: Partition: "; + appendPartition(partition, messageContent); // Appends the message with the partition number + sendMessage(messageContent); + } + dsc.armedChanged[partition] = false; // Resets the partition armed status flag + } + } + } + + // Checks trouble status + if (notifyOnTrouble) { + if (dsc.troubleChanged) { + dsc.troubleChanged = false; // Resets the trouble status flag + if (dsc.trouble) sendMessage("Trouble status on"); + else sendMessage("Trouble status restored"); + } + } + // Checks for AC power status + if (notifyOnPowerTroubles) { + if (dsc.powerChanged) { + dsc.powerChanged = false; // Resets the battery trouble status flag + if (dsc.powerTrouble) sendMessage("AC power trouble"); + else sendMessage("AC power restored"); + } + } + // Checks panel battery status + if (dsc.batteryChanged) { + dsc.batteryChanged = false; // Resets the battery trouble status flag + if (dsc.batteryTrouble) sendMessage("Panel battery trouble"); + else sendMessage("Panel battery restored"); + } + + if (notifyOnKeypadAlarm) { + // Checks for keypad fire alarm status + if (dsc.keypadFireAlarm) { + dsc.keypadFireAlarm = false; // Resets the keypad fire alarm status flag + sendMessage("Keypad Fire alarm"); + } + + // Checks for keypad aux auxiliary alarm status + if (dsc.keypadAuxAlarm) { + dsc.keypadAuxAlarm = false; // Resets the keypad auxiliary alarm status flag + sendMessage("Keypad Aux alarm"); + } + + // Checks for keypad panic alarm status + if (dsc.keypadPanicAlarm) { + dsc.keypadPanicAlarm = false; // Resets the keypad panic alarm status flag + sendMessage("Keypad Panic alarm"); + } + } + } +} + +bool sendMessage(const char* messageContent) { + bool result = true; + + for (int i = 0; i < phone_number_count; i++) { + result &= modem.sendSMS(sendToPhoneNumbers[i], messageContent); + } + + return result; +} + +void appendPartition(byte sourceNumber, char* pushMessage) { + char partitionNumber[2]; + itoa(sourceNumber + 1, partitionNumber, 10); + strcat(pushMessage, partitionNumber); +} diff --git a/examples/esp8266/Twilio-SMS/Twilio-SMS.ino b/examples/esp8266/Twilio-SMS/Twilio-SMS.ino index f174831..e817db8 100644 --- a/examples/esp8266/Twilio-SMS/Twilio-SMS.ino +++ b/examples/esp8266/Twilio-SMS/Twilio-SMS.ino @@ -1,10 +1,13 @@ /* - * Twilio SMS Notification 1.3 (esp8266) + * Twilio SMS Notification 1.5 (esp8266) * * Processes the security system status and demonstrates how to send an SMS text message when the status has * changed. This example sends SMS text messages via Twilio: https://www.twilio.com * * Release notes: + * 1.5 - Add TLS root certificate for Twilio + * Encode authorization data in base64 directly within the sketch + * 1.4 - Added DSC Classic series support * 1.3 - Updated esp8266 wiring diagram for 33k/10k resistors * 1.2 - Check if WiFi disconnects and wait to send updates until reconnection * Add appendPartition() to simplify sketch @@ -18,23 +21,20 @@ * * DSC Aux(-) --- esp8266 Ground * - * +--- dscClockPin (esp8266: D1, D2, D8) + * +--- dscClockPin // Default: D1, GPIO 5 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp8266: D1, D2, D8) + * +--- dscReadPin // Default: D2, GPIO 4 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): - * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) - * Ground --- NPN emitter --/ - * - * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should - * be suitable, for example: - * -- 2N3904 - * -- BC547, BC548, BC549 + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground * * Issues and (especially) pull requests are welcome: * https://github.com/taligentx/dscKeybusInterface @@ -44,27 +44,65 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include +#include "base64.h" // Settings const char* wifiSSID = ""; const char* wifiPassword = ""; -const char* AccountSID = ""; // Set the account SID from the Twilio Account Dashboard -const char* AuthToken = ""; // Set the auth token from the Twilio Account Dashboard -const char* Base64EncodedAuth = ""; // macOS/Linux terminal: $ echo -n "AccountSID:AuthToken" | base64 -w 0 -const char* From = ""; // i.e. 16041234567 -const char* To = ""; // i.e. 16041234567 +const char* AccountSID = ""; // Set the account SID from the Twilio Account Dashboard +const char* AuthToken = ""; // Set the auth token from the Twilio Account Dashboard +const char* From = ""; // From phone number, starting with the country code without the + sign: 18005551234 +const char* To = ""; // To phone number, starting with the country code without the + sign: 18005551234 const char* messagePrefix = "[Security system] "; // Set a prefix for all messages // Configures the Keybus interface with the specified pins. -#define dscClockPin D1 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscReadPin D2 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 + +// HTTPS root certificate for api.twilio.com: DigiCert Global Root CA, expires 2031.11.10 +const char twilioCertificateRoot[] = R"=EOF=( +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- +)=EOF="; // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin); +#endif +X509List twilioCert(twilioCertificateRoot); WiFiClientSecure ipClient; bool wifiConnected = true; +char twilioAuth[128]; +size_t twilioAuthLength = 128; +char encodedTwilioAuth[128], encodedMessagePrefix[128], encodedMessageContent[480]; void setup() { @@ -73,16 +111,34 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); + ipClient.setTrustAnchors(&twilioCert); while (WiFi.status() != WL_CONNECTED) { Serial.print("."); delay(500); } Serial.print(F("connected: ")); Serial.println(WiFi.localIP()); - ipClient.setInsecure(); + + Serial.print(F("NTP time....")); + configTime(0, 0, "pool.ntp.org"); + time_t now = time(nullptr); + while (now < 24 * 3600) + { + Serial.print("."); + delay(2000); + now = time(nullptr); + } + Serial.println(F("synchronized.")); + + // Encodes authentication in base64 and message prefix in URL encoding + strcat(twilioAuth, AccountSID); + strcat(twilioAuth, ":"); + strcat(twilioAuth, AuthToken); + base64::encode(String(twilioAuth)).toCharArray(encodedTwilioAuth, 128); + encodeURL(messagePrefix, encodedMessagePrefix); // Sends a message on startup to verify connectivity Serial.print(F("Twilio....")); @@ -116,7 +172,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -138,11 +194,11 @@ void loop() { // Checks armed status if (dsc.armedChanged[partition]) { if (dsc.armed[partition]) { - char messageContent[25]; + char messageContent[30]; - if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night: Partition "); + if (dsc.armedAway[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night away: Partition "); else if (dsc.armedAway[partition]) strcpy(messageContent, "Armed away: Partition "); - else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night: Partition "); + else if (dsc.armedStay[partition] && dsc.noEntryDelay[partition]) strcpy(messageContent, "Armed night stay: Partition "); else if (dsc.armedStay[partition]) strcpy(messageContent, "Armed stay: Partition "); appendPartition(partition, messageContent); // Appends the message with the partition number @@ -279,29 +335,29 @@ void loop() { bool sendMessage(const char* messageContent) { + encodeURL(messageContent, encodedMessageContent); // Encodes message content in URL encoding - // Connects and sends the message as x-www-form-urlencoded if (!ipClient.connect("api.twilio.com", 443)) return false; ipClient.print(F("POST https://api.twilio.com/2010-04-01/Accounts/")); ipClient.print(AccountSID); ipClient.println(F("/Messages.json HTTP/1.1")); ipClient.print(F("Authorization: Basic ")); - ipClient.println(Base64EncodedAuth); + ipClient.println(encodedTwilioAuth); ipClient.println(F("Host: api.twilio.com")); ipClient.println(F("User-Agent: ESP8266")); ipClient.println(F("Accept: */*")); ipClient.println(F("Content-Type: application/x-www-form-urlencoded")); ipClient.print(F("Content-Length: ")); - ipClient.println(strlen(To) + strlen(From) + strlen(messagePrefix) + strlen(messageContent) + 18); // Length including data + ipClient.println(strlen(To) + strlen(From) + strlen(encodedMessagePrefix) + strlen(encodedMessageContent) + 21); ipClient.println("Connection: Close"); ipClient.println(); - ipClient.print(F("To=+")); + ipClient.print(F("To=%2B")); ipClient.print(To); - ipClient.print(F("&From=+")); + ipClient.print(F("&From=%2B")); ipClient.print(From); ipClient.print(F("&Body=")); - ipClient.print(messagePrefix); - ipClient.println(messageContent); + ipClient.print(encodedMessagePrefix); + ipClient.print(encodedMessageContent); // Waits for a response unsigned long previousMillis = millis(); @@ -332,6 +388,7 @@ bool sendMessage(const char* messageContent) { // Unsuccessful, prints the response to serial to help debug else { + Serial.println(); Serial.println(F("SMS messaging error, response:")); Serial.print(statusCode); while (ipClient.available()) Serial.print((char)ipClient.read()); @@ -347,3 +404,31 @@ void appendPartition(byte sourceNumber, char* messageContent) { itoa(sourceNumber + 1, partitionNumber, 10); strcat(messageContent, partitionNumber); } + + +// Helper for encodeURL() +static char encodeHex(char c) { + return "0123456789ABCDEF"[c & 0x0F]; +} + + +// Encodes a char array to URL encoded using '+' for spaces as required for application/x-www-form-urlencoded +char *encodeURL(const char *src, char *dst) { + char c, *d = dst; + while (c = *src++) { + if (c == ' ') { + *d++ = '+'; + continue; + } + else if (!('a' <= c && c <= 'z') + && !('A' <= c && c <= 'Z') + && !('0' <= c && c <= '9')) { + *d++ = '%'; + *d++ = encodeHex(c >> 4); + c = encodeHex(c); + } + *d++ = c; + } + *d = '\0'; + return dst; +} diff --git a/examples/esp8266/Unlocker/Unlocker.ino b/examples/esp8266/Unlocker/Unlocker.ino index c61eb57..96ba197 100644 --- a/examples/esp8266/Unlocker/Unlocker.ino +++ b/examples/esp8266/Unlocker/Unlocker.ino @@ -69,7 +69,7 @@ * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad: + * Virtual keypad: * DSC Green ---- NPN collector --\ * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) * Ground --- NPN emitter --/ @@ -521,7 +521,7 @@ void setup() { // Starts the Keybus interface and optionally specifies how to print data. // begin() sets Serial by default and can accept a different stream: begin(Serial1), etc. dsc.begin(); - Serial.print(F("DSC Keybus Interface...")); + Serial.print(F("DSC Keybus Interface....")); // Loops until partition 1 is ready for key presses in status "Partition ready" (0x01), // "Stay/away zones open" (0x02), or "Zones open" (0x03) diff --git a/examples/esp8266/VirtualKeypad-Blynk/VirtualKeypad-Blynk.ino b/examples/esp8266/VirtualKeypad-Blynk/VirtualKeypad-Blynk.ino index 738dbd0..9a7e12d 100644 --- a/examples/esp8266/VirtualKeypad-Blynk/VirtualKeypad-Blynk.ino +++ b/examples/esp8266/VirtualKeypad-Blynk/VirtualKeypad-Blynk.ino @@ -1,12 +1,21 @@ /* - * VirtualKeypad-Blynk 1.3 (esp8266) + * VirtualKeypad-Blynk 1.4 (esp8266) * - * Provides a virtual keypad interface for the free Blynk (https://www.blynk.cc) app on iOS and Android, similar - * to a physical DSC LED keypad. Note that while the Blynk app has an LCD to display the partition status, the - * sketch currently does not emulate the menu navigation features of the DSC LCD keypads (PK5500, etc). + * Provides a virtual keypad interface for the free Blynk legacy (https://www.blynk.cc) app on iOS and Android, similar + * to a physical DSC LED keypad (the newer Blynk.Cloud app is not currently supported): + * + * iOS: https://apps.apple.com/us/app/blynk-0-1-legacy/id808760481 + * Android: https://play.google.com/store/apps/details?id=cc.blynk&hl=en&gl=US + * + * Installing Blynk as a local server (https://github.com/blynkkk/blynk-server) is recommended to keep control of the + * security system internal to your network. This also lets you use as many widgets as needed for free - local + * servers can setup users with any amount of Blynk Energy. + * + * Note that while the Blynk legacy app has an LCD to display the partition status, the sketch currently does + * not emulate the menu navigation features of the DSC LCD keypads (PK5500, etc). * * Usage: - * 1. Scan one of the following QR codes from within the Blynk app for an example keypad layout - as QR codes + * 1. Scan one of the following QR codes from within the Blynk legacy app for an example keypad layout - as QR codes * can contain a limited amount of objects, only the 8 and 16-zone template includes PGM outputs 1-8. Use * cloning within the Blynk app to add up to 64 zones and up to 14 PGM outputs. Some Android devices have * issues reading these QR codes and may need to be used with a different monitor/device. @@ -21,11 +30,6 @@ * 5. Add the auth token to the sketch below. * 6. Upload the sketch. * - * Installing Blynk as a local server (https://github.com/blynkkk/blynk-server) is recommended to keep control of the - * security system internal to your network. This also lets you use as many widgets as needed for free - local - * servers can setup users with any amount of Blynk Energy. Using the default Blynk cloud service with the above - * example layouts requires more of Blynk's Energy units than available on the free usage tier. - * * The Blynk layout can be customized with widgets using these virtual pin mappings: V0 - Keypad 0 ... V9 - Keypad 9 V10 - Keypad @@ -53,6 +57,7 @@ V61 - Zone 1 ... V124 - Zone 64 * * Release notes: + * 1.4 - Added DSC Classic series support * 1.3 - Display alarm memory, programming zone lights, and event buffer * Add PGM outputs 1-14 status * 1.2 - Updated esp8266 wiring diagram for 33k/10k resistors @@ -63,17 +68,24 @@ * * DSC Aux(-) --- esp8266 Ground * - * +--- dscClockPin (esp8266: D1, D2, D8) + * +--- dscClockPin // Default: D1, GPIO 5 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp8266: D1, D2, D8) + * +--- dscReadPin // Default: D2, GPIO 4 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: D8, GPIO 15 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -87,6 +99,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -96,18 +111,24 @@ // Settings const char* wifiSSID = ""; const char* wifiPassword = ""; +const char* accessCode = ""; // Classic series only, an access code is required to arm with the stay/away buttons. const char* blynkAuthToken = ""; // Token generated from within the Blynk app const char* blynkServer = ""; // Blynk local server address const int blynkPort = 8080; // Blynk local server port bool showLCDoutput = true; // Control if LCD programming output is displayed on VirtualPin20 // Configures the Keybus interface with the specified pins -#define dscClockPin D1 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscReadPin D2 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscWritePin D8 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 +#define dscWritePin D8 // GPIO 15 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif bool wifiConnected = true; bool partitionChanged, pausedZones, extendedBuffer; bool decimalOutput, inputDigits; @@ -210,13 +231,13 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi...")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); Blynk.begin(blynkAuthToken, wifiSSID, wifiPassword, blynkServer, blynkPort); while (WiFi.status() != WL_CONNECTED) yield(); Serial.print(F("connected: ")); Serial.println(WiFi.localIP()); - Serial.print(F("Blynk...")); + Serial.print(F("Blynk....")); while (!Blynk.connected()) { Blynk.run(); yield(); @@ -257,7 +278,7 @@ void loop() { dsc.statusChanged = false; // Reset the status tracking flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; @@ -401,10 +422,12 @@ void loop() { if (dsc.powerTrouble) { lcd.print(0, 0, "Power "); lcd.print(0, 1, "trouble "); + Blynk.notify("Power trouble"); } else { lcd.print(0, 0, "Power "); lcd.print(0, 1, "restored "); + Blynk.notify("Power restored"); } } @@ -413,10 +436,32 @@ void loop() { if (dsc.batteryTrouble) { lcd.print(0, 0, "Battery "); lcd.print(0, 1, "trouble "); + Blynk.notify("Battery trouble"); } else { lcd.print(0, 0, "Battery "); lcd.print(0, 1, "restored "); + Blynk.notify("Battery restored"); + } + } + + if (dsc.troubleChanged) { + dsc.troubleChanged = false; + if (dsc.trouble) Blynk.notify("Trouble status on"); + else Blynk.notify("Trouble status restored"); + } + + for (byte partition = 0; partition < dscPartitions; partition++) { + if (dsc.disabled[partition]) continue; + if (dsc.alarmChanged[partition]) { + dsc.alarmChanged[partition] = false; + if (dsc.alarm[partition]) { + char alarmPartition[19] = "Alarm: Partition "; + char partitionNumber[2]; + itoa(partition + 1, partitionNumber, 10); + strcat(alarmPartition, partitionNumber); + Blynk.notify(alarmPartition); + } } } } @@ -444,7 +489,7 @@ void setStatus(byte partition, bool forceUpdate) { case 0x05: lcd.print(0, 0, "Armed: "); lcd.print(0, 1, "Away "); if (pausedZones) resetZones(); break; - case 0x06: lcd.print(0, 0, "Armed: "); + case 0x06: lcd.print(0, 0, "Armed: Stay "); lcd.print(0, 1, "No entry delay "); if (pausedZones) resetZones(); break; case 0x07: lcd.print(0, 0, "Failed "); @@ -472,7 +517,7 @@ void setStatus(byte partition, bool forceUpdate) { lcd.print(0, 1, "in progress "); break; case 0x15: lcd.print(0, 0, "Arming with "); lcd.print(0, 1, "bypass zones "); break; - case 0x16: lcd.print(0, 0, "Armed: "); + case 0x16: lcd.print(0, 0, "Armed: Away "); lcd.print(0, 1, "No entry delay "); if (pausedZones) resetZones(); break; case 0x17: lcd.print(0, 0, "Power saving "); @@ -853,19 +898,27 @@ void setLights(byte partition, bool forceUpdate) { // Processes status data not natively handled within the library void processStatus() { + #ifndef dscClassicSeries switch (dsc.panelData[0]) { - case 0x05: - if ((dsc.panelData[3] == 0x9E || dsc.panelData[3] == 0xA5 || dsc.panelData[3] == 0xB7 || dsc.panelData[3] == 0xB8) && !pausedZones) { - pauseZones(); - } + case 0x05: //Enter (*) function key, enter (*) function key while armed, enter installer code, enter master code status messages for partitions 1-4 calls pauseZones + if ((dsc.panelData[3] == 0x9E || dsc.panelData[3] == 0xA5 || dsc.panelData[3] == 0xB7 || dsc.panelData[3] == 0xB8) && !pausedZones && dsc.writePartition == 1) pauseZones(); + if ((dsc.panelData[5] == 0x9E || dsc.panelData[5] == 0xA5 || dsc.panelData[5] == 0xB7 || dsc.panelData[5] == 0xB8) && !pausedZones && dsc.writePartition == 2) pauseZones(); + if ((dsc.panelData[7] == 0x9E || dsc.panelData[7] == 0xA5 || dsc.panelData[7] == 0xB7 || dsc.panelData[7] == 0xB8) && !pausedZones && dsc.writePartition == 3) pauseZones(); + if ((dsc.panelData[9] == 0x9E || dsc.panelData[9] == 0xA5 || dsc.panelData[9] == 0xB7 || dsc.panelData[9] == 0xB8) && !pausedZones && dsc.writePartition == 4) pauseZones(); break; - case 0x0A: - if ((dsc.panelData[3] == 0x9E || dsc.panelData[3] == 0xA5 || dsc.panelData[3] == 0xB7 || dsc.panelData[3] == 0xB8) && !pausedZones) { - pauseZones(); - } - if (pausedZones) { - processProgramZones(4, ledProgramZonesColor); - } + case 0x0A: //Call processProgramZones on partition 1 + if ((dsc.panelData[3] == 0x9E || dsc.panelData[3] == 0xA5 || dsc.panelData[3] == 0xB7 || dsc.panelData[3] == 0xB8) && !pausedZones && dsc.writePartition == 1) pauseZones(); + if (pausedZones) processProgramZones(4, ledProgramZonesColor); + break; + case 0x0F: //Call processProgramZones on partition 2 + if ((dsc.panelData[3] == 0x9E || dsc.panelData[3] == 0xA5 || dsc.panelData[3] == 0xB7 || dsc.panelData[3] == 0xB8) && !pausedZones && dsc.writePartition == 2) pauseZones(); + if (pausedZones) processProgramZones(4, ledProgramZonesColor); + break; + case 0x1B: //Enter (*) function key, enter (*) function key while armed, enter installer code, enter master code status messages for partitions 4-8 calls pauseZones + if ((dsc.panelData[3] == 0x9E || dsc.panelData[3] == 0xA5 || dsc.panelData[3] == 0xB7 || dsc.panelData[3] == 0xB8) && !pausedZones && dsc.writePartition == 5) pauseZones(); + if ((dsc.panelData[5] == 0x9E || dsc.panelData[5] == 0xA5 || dsc.panelData[5] == 0xB7 || dsc.panelData[5] == 0xB8) && !pausedZones && dsc.writePartition == 6) pauseZones(); + if ((dsc.panelData[7] == 0x9E || dsc.panelData[7] == 0xA5 || dsc.panelData[7] == 0xB7 || dsc.panelData[7] == 0xB8) && !pausedZones && dsc.writePartition == 7) pauseZones(); + if ((dsc.panelData[9] == 0x9E || dsc.panelData[9] == 0xA5 || dsc.panelData[9] == 0xB7 || dsc.panelData[9] == 0xB8) && !pausedZones && dsc.writePartition == 8) pauseZones(); break; case 0x5D: if ((dsc.panelData[2] & 0x04) == 0x04) { // Alarm memory zones 1-32 @@ -890,6 +943,7 @@ void processStatus() { break; case 0xEC: if (pausedZones) processEventBufferEC(); break; } + #endif } @@ -919,6 +973,7 @@ void processProgramZones(byte startByte, const char* ledColor) { void processLCDoutputData() { + #ifndef dscClassicSeries if (!showLCDoutput) return; // Do not display LCD output data if showLCDoutput is false char dataInfo[21] = "LCD Display: "; char dataBuffer[4]; @@ -937,10 +992,12 @@ void processLCDoutputData() { } } Blynk.virtualWrite(V20, dataInfo); + #endif } void processEventBufferAA() { + #ifndef dscClassicSeries if (extendedBuffer) return; // Skips 0xAA data when 0xEC extended event buffer data is available char eventInfo[45] = "Event: "; @@ -995,10 +1052,12 @@ void processEventBufferAA() { case 0x02: printPanelStatus2(6); break; case 0x03: printPanelStatus3(6); break; } + #endif } void processEventBufferEC() { + #ifndef dscClassicSeries if (!extendedBuffer) extendedBuffer = true; char eventInfo[45] = "Event: "; @@ -1070,6 +1129,7 @@ void processEventBufferEC() { case 0x18: printPanelStatus18(8); break; case 0x1B: printPanelStatus1B(8); break; } + #endif } diff --git a/examples/esp8266/VirtualKeypad-Web/VirtualKeypad-Web.ino b/examples/esp8266/VirtualKeypad-Web/VirtualKeypad-Web.ino index 6341443..7601edc 100644 --- a/examples/esp8266/VirtualKeypad-Web/VirtualKeypad-Web.ino +++ b/examples/esp8266/VirtualKeypad-Web/VirtualKeypad-Web.ino @@ -1,5 +1,5 @@ /* - * VirtualKeypad-Web 1.3 (esp8266) + * VirtualKeypad-Web 1.5 (esp8266) * * Provides a virtual keypad web interface using the esp8266 as a standalone web server, including * alarm memory, programming zone lights, and viewing the event buffer. To access the event buffer, @@ -7,8 +7,10 @@ * * Usage: * 1. Install the following libraries directly from each Github repository: - * ESPAsyncWebServer: https://github.com/me-no-dev/ESPAsyncWebServer * ESPAsyncTCP: https://github.com/me-no-dev/ESPAsyncTCP + * ESPAsyncWebServer: https://github.com/arjenhiemstra/ESPAsyncWebServer + * * This is a fork of the original ESPAsyncWebServer that fixes the web server crashing + * when used with recent versions of Safari on macOS and iOS. * * 2. Install ESP8266FS to enable uploading web server files to the esp8266: * https://arduino-esp8266.readthedocs.io/en/latest/filesystem.html#uploading-files-to-file-system @@ -21,8 +23,8 @@ * 4. Set the WiFi SSID and password in the sketch. * 5. If desired, update the DNS hostname in the sketch. By default, this is set to * "dsc" and the web interface will be accessible at: http://dsc.local - * 6. Set the esp8266 flash size to use 1M SPIFFS. - * Arduino IDE: Tools > Flash Size > 4M (1M SPIFFS) + * 6. Set the esp8266 flash size to use at least 1MB for the filesystem. + * Arduino IDE: Tools > Flash Size > 4MB (FS:1MB ...) * 7. Upload the sketch. * 8. Upload the SPIFFS data containing the web server files: * Arduino IDE: Tools > ESP8266 Sketch Data Upload @@ -30,6 +32,9 @@ * the serial output or http://dsc.local (for clients and networks that support mDNS). * * Release notes: + * 1.5 - Added DSC Classic series support + * Changed ESPAsyncWebServer to a newer fork to fix web server crashes with Safari + * 1.4 - Fix crash when pressing keys while Keybus is disconnected * 1.3 - Add event buffer display * Display zone lights in alarm memory and programming * Added AC power status, reset, quick exit @@ -45,17 +50,24 @@ * * DSC Aux(-) --- esp8266 Ground * - * +--- dscClockPin (esp8266: D1, D2, D8) + * +--- dscClockPin // Default: D1, GPIO 5 * DSC Yellow --- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * +--- dscReadPin (esp8266: D1, D2, D8) + * +--- dscReadPin // Default: D2, GPIO 4 * DSC Green ---- 33k ohm resistor ---| * +--- 10k ohm resistor --- Ground * - * Virtual keypad (optional): + * Classic series only, PGM configured for PC-16 output: + * DSC PGM ---+-- 1k ohm resistor --- DSC Aux(+) + * | + * | +--- dscPC16Pin // Default: D7, GPIO 13 + * +-- 33k ohm resistor ---| + * +--- 10k ohm resistor --- Ground + * + * Virtual keypad (optional): * DSC Green ---- NPN collector --\ - * |-- NPN base --- 1k ohm resistor --- dscWritePin (esp8266: D1, D2, D8) + * |-- NPN base --- 1k ohm resistor --- dscWritePin // Default: D8, GPIO 15 * Ground --- NPN emitter --/ * * Virtual keypad uses an NPN transistor to pull the data line low - most small signal NPN transistors should @@ -71,6 +83,9 @@ * This example code is in the public domain. */ +// DSC Classic series: uncomment for PC1500/PC1550 support (requires PC16-OUT configuration per README.md) +//#define dscClassicSeries + #include #include #include @@ -83,16 +98,22 @@ // Settings const char* wifiSSID = ""; const char* wifiPassword = ""; +const char* accessCode = ""; // Classic series only, an access code is required to arm with the stay/away buttons. const char* dnsHostname = "dsc"; // Sets the domain name - if set to "dsc", access via: http://dsc.local const byte dscPartition = 1; // Set the partition for the keypad // Configures the Keybus interface with the specified pins -#define dscClockPin D1 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscReadPin D2 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) -#define dscWritePin D8 // esp8266: D1, D2, D8 (GPIO 5, 4, 15) +#define dscClockPin D1 // GPIO 5 +#define dscReadPin D2 // GPIO 4 +#define dscPC16Pin D7 // DSC Classic Series only, GPIO 13 +#define dscWritePin D8 // GPIO 15 // Initialize components +#ifndef dscClassicSeries dscKeybusInterface dsc(dscClockPin, dscReadPin, dscWritePin); +#else +dscClassicInterface dsc(dscClockPin, dscReadPin, dscPC16Pin, dscWritePin, accessCode); +#endif AsyncWebServer server(80); AsyncWebSocket ws("/ws"); Chrono ws_ping_pong(Chrono::SECONDS); @@ -108,7 +129,7 @@ void setup() { Serial.println(); Serial.println(); - Serial.print(F("WiFi...")); + Serial.print(F("WiFi....")); WiFi.mode(WIFI_STA); WiFi.begin(wifiSSID, wifiPassword); while (WiFi.status() != WL_CONNECTED) { @@ -163,12 +184,42 @@ void loop() { dsc.statusChanged = false; // Resets the status flag // If the Keybus data buffer is exceeded, the sketch is too busy to process all Keybus commands. Call - // loop() more often, or increase dscBufferSize in the library: src/dscKeybusInterface.h + // loop() more often, or increase dscBufferSize in the library: src/dscKeybus.h or src/dscClassic.h if (dsc.bufferOverflow) { Serial.println(F("Keybus buffer overflow")); dsc.bufferOverflow = false; } + // Checks if the interface is connected to the Keybus + if (dsc.keybusChanged) { + dsc.keybusChanged = false; // Resets the Keybus data status flag + if (dsc.keybusConnected) { + Serial.println(F("Keybus connected")); + forceUpdate = true; + if (ws.count()) { + char outas[128]; + StaticJsonDocument<200> doc; + JsonObject root = doc.to(); + root["lcd_upper"] = "Keybus"; + root["lcd_lower"] = "connected"; + serializeJson(root, outas); + ws.textAll(outas); + } + } + else { + Serial.println(F("Keybus disconnected")); + if (ws.count()) { + char outas[128]; + StaticJsonDocument<200> doc; + JsonObject root = doc.to(); + root["lcd_upper"] = "Keybus"; + root["lcd_lower"] = "disconnected"; + serializeJson(root, outas); + ws.textAll(outas); + } + } + } + setLights(partition); setStatus(partition); @@ -286,7 +337,7 @@ void setStatus(byte partition) { case 0x05: root["lcd_upper"] = "Armed: "; root["lcd_lower"] = "Away "; if (pausedZones) resetZones(); break; - case 0x06: root["lcd_upper"] = "Armed: "; + case 0x06: root["lcd_upper"] = "Armed: Stay "; root["lcd_lower"] = "No entry delay "; if (pausedZones) resetZones(); break; case 0x07: root["lcd_upper"] = "Failed "; @@ -314,7 +365,7 @@ void setStatus(byte partition) { root["lcd_lower"] = "in progress "; break; case 0x15: root["lcd_upper"] = "Arming with "; root["lcd_lower"] = "bypass zones "; break; - case 0x16: root["lcd_upper"] = "Armed: "; + case 0x16: root["lcd_upper"] = "Armed: Away "; root["lcd_lower"] = "No entry delay "; if (pausedZones) resetZones(); break; case 0x17: root["lcd_upper"] = "Power saving "; @@ -470,6 +521,7 @@ void setLights(byte partition) { // Processes status data not natively handled within the library void processStatus() { + #ifndef dscClassicSeries switch (dsc.panelData[0]) { case 0x05: if ((dsc.panelData[3] == 0x9E || dsc.panelData[3] == 0xB8) && !pausedZones) { @@ -503,6 +555,7 @@ void processStatus() { break; case 0xEC: if (pausedZones) processEventBufferEC(); break; } + #endif } @@ -535,6 +588,7 @@ void processProgramZones(byte startByte) { void processEventBufferAA() { + #ifndef dscClassicSeries if (extendedBuffer) return; // Skips 0xAA data when 0xEC extended event buffer data is available char eventInfo[45] = "Event: "; @@ -596,10 +650,12 @@ void processEventBufferAA() { case 0x02: printPanelStatus2(6); break; case 0x03: printPanelStatus3(6); break; } + #endif } void processEventBufferEC() { + #ifndef dscClassicSeries if (!extendedBuffer) extendedBuffer = true; char eventInfo[45] = "Event: "; @@ -678,6 +734,7 @@ void processEventBufferEC() { case 0x18: printPanelStatus18(8); break; case 0x1B: printPanelStatus1B(8); break; } + #endif } @@ -1525,9 +1582,30 @@ void resetZones() { void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventType type, void * arg, uint8_t *data, size_t len) { + if (type == WS_EVT_CONNECT) { client->printf("{\"connected_id\": %u}", client->id()); - forceUpdate = true; + if (dsc.keybusConnected && ws.count()) { + if (ws.count()) { + char outas[128]; + StaticJsonDocument<200> doc; + JsonObject root = doc.to(); + root["lcd_upper"] = "Keybus"; + root["lcd_lower"] = "connected"; + serializeJson(root, outas); + ws.textAll(outas); + } + forceUpdate = true; + } + else if (!dsc.keybusConnected && ws.count()) { + char outas[128]; + StaticJsonDocument<200> doc; + JsonObject root = doc.to(); + root["lcd_upper"] = "Keybus"; + root["lcd_lower"] = "disconnected"; + serializeJson(root, outas); + ws.textAll(outas); + } client->ping(); ws_ping_pong.restart(); } @@ -1570,7 +1648,7 @@ void onWsEvent(AsyncWebSocket * server, AsyncWebSocketClient * client, AwsEventT char * const sep_at = strchr(tmp, '_'); if (sep_at != NULL) { *sep_at = '\0'; - dsc.write(sep_at + 1); + if (dsc.keybusConnected) dsc.write(sep_at + 1); } } } diff --git a/keywords.txt b/keywords.txt index dd82532..b6f8387 100644 --- a/keywords.txt +++ b/keywords.txt @@ -1,12 +1,18 @@ dscKeybusInterface KEYWORD1 +dscClassicInterface KEYWORD1 +dscKeypadInterface KEYWORD1 +dscClassicKeypadInterface KEYWORD1 dsc KEYWORD1 dscClockPin LITERAL1 dscReadPin LITERAL1 dscWritePin LITERAL1 dscRelayPin LITERAL1 +dscPC16Pin LITERAL1 dscZones LITERAL1 dscPartitions LITERAL1 +dscClassicSeries LITERAL1 +dscKeypad LITERAL1 hideKeypadDigits KEYWORD2 displayTrailingBits KEYWORD2 @@ -69,7 +75,45 @@ pgmOutputsStatusChanged KEYWORD2 lights KEYWORD2 status KEYWORD2 panelData KEYWORD2 +pc16Data KEYWORD2 panelVersion KEYWORD2 +lightReady KEYWORD2 +lightArmed KEYWORD2 +lightMemory KEYWORD2 +lightBypass KEYWORD2 +lightTrouble KEYWORD2 +lightProgram KEYWORD2 +lightFire KEYWORD2 +lightBacklight KEYWORD2 +blinkReady KEYWORD2 +blinkArmed KEYWORD2 +blinkMemory KEYWORD2 +blinkBypass KEYWORD2 +blinkTrouble KEYWORD2 +blinkProgram KEYWORD2 +blinkFire KEYWORD2 +blinkBacklight KEYWORD2 +lightZone1 KEYWORD2 +lightZone2 KEYWORD2 +lightZone3 KEYWORD2 +lightZone4 KEYWORD2 +lightZone5 KEYWORD2 +lightZone6 KEYWORD2 +lightZone7 KEYWORD2 +lightZone8 KEYWORD2 +blinkZone1 KEYWORD2 +blinkZone2 KEYWORD2 +blinkZone3 KEYWORD2 +blinkZone4 KEYWORD2 +blinkZone5 KEYWORD2 +blinkZone6 KEYWORD2 +blinkZone7 KEYWORD2 +blinkZone8 KEYWORD2 +keyAvailable KEYWORD2 +key KEYWORD2 +beep KEYWORD2 +tone KEYWORD2 +buzzer KEYWORD2 printPanelBinary KEYWORD2 printPanelCommand KEYWORD2 diff --git a/library.json b/library.json index b85bced..8b6991d 100644 --- a/library.json +++ b/library.json @@ -1,13 +1,13 @@ { "name": "dscKeybusInterface", - "keywords": "dsc, home-automation, home-security, homebridge, homekit, home-assistant, homeassistant, homey, openhab, google-home, blnynk, web, webserver, telegram, pushbullet, twilio, email, esp8266, esp32", - "description": "This library directly interfaces Arduino, esp8266, and esp32 microcontrollers to DSC PowerSeries security systems for integration with home automation (Home Assistant, Apple HomeKit, Homey), notifications on system events, and usage as a virtual keypad.", + "keywords": "dsc, home-automation, home-security, homebridge, homekit, home-assistant, homeassistant, homespan, homey, openhab, google-home, blynk, web, webserver, telegram, pushbullet, twilio, tinygsm, email, esp8266, esp32", + "description": "Directly interface Arduino, esp8266, and esp32 to DSC PowerSeries and Classic security systems for integration with home automation, remote control apps, notifications on alarm events, and emulating DSC panels to connect DSC keypads.", "repository": { "type": "git", "url": "https://github.com/taligentx/dscKeybusInterface.git" }, - "version": "2.0", + "version": "3.0", "frameworks": "arduino", "platforms": "atmelavr, espressif8266, espressif32" } diff --git a/library.properties b/library.properties index 43e47c2..38bf71f 100644 --- a/library.properties +++ b/library.properties @@ -1,10 +1,10 @@ name=DSC Keybus Interface -version=2.0 +version=3.0 author=Nikhil Choudhary maintainer=Nikhil Choudhary -sentence=Directly interface Arduino, esp8266, and esp32 microcontrollers to DSC PowerSeries security systems for integration with home automation, alarm notifications, and usage as a virtual keypad. -paragraph=Includes examples to monitor armed/alarm/zone/fire/trouble status, integrate with Homebridge (Apple HomeKit, Google Home) and Home Assistant via MQTT, send email and push notifications via Telegram and Pushbullet, and decode the Keybus protocol. +sentence=Directly interface Arduino, esp8266, and esp32 to DSC PowerSeries and Classic security systems for integration with home automation, remote control apps, notifications on alarm events, and emulating DSC panels to connect DSC keypads. +paragraph=Includes examples to integrate with Homebridge (Apple HomeKit, Google Home), Home Assistant and OpenHAB via MQTT, remote control via web interface/Blynk/Telegram bot, send email and push notifications via Pushbullet/Pushover/Pushsafer, send SMS via Twilio/TinyGSM, unlock panel installer codes, and decode the Keybus protocol. category=Device Control url=https://github.com/taligentx/dscKeybusInterface -architectures=* +architectures=avr,esp8266,esp32 includes=dscKeybusInterface.h diff --git a/src/dscClassic.cpp b/src/dscClassic.cpp new file mode 100644 index 0000000..c674ba9 --- /dev/null +++ b/src/dscClassic.cpp @@ -0,0 +1,1223 @@ +/* + DSC Keybus Interface + + https://github.com/taligentx/dscKeybusInterface + + This library is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#include "dscClassic.h" + +#if defined(ESP32) +portMUX_TYPE dscClassicInterface::timer1Mux = portMUX_INITIALIZER_UNLOCKED; +hw_timer_t * dscClassicInterface::timer1 = NULL; +#endif + +dscClassicInterface::dscClassicInterface(byte setClockPin, byte setReadPin, byte setPC16Pin, byte setWritePin, const char * setAccessCode) { + dscClockPin = setClockPin; + dscReadPin = setReadPin; + dscPC16Pin = setPC16Pin; + dscWritePin = setWritePin; + if (dscWritePin != 255) virtualKeypad = true; + writeReady = false; + writePartition = 1; + pauseStatus = false; + accessCodeStay = setAccessCode; + strcpy(accessCodeAway, accessCodeStay); + strcat(accessCodeAway, "*1"); + strcpy(accessCodeNight, "*9"); + strcat(accessCodeNight, accessCodeStay); +} + + +void dscClassicInterface::begin(Stream &_stream) { + pinMode(dscClockPin, INPUT); + pinMode(dscReadPin, INPUT); + pinMode(dscPC16Pin, INPUT); + if (virtualKeypad) pinMode(dscWritePin, OUTPUT); + stream = &_stream; + + // Platform-specific timers trigger a read of the data line 250us after the Keybus clock changes: + + // Arduino/AVR Timer1 calls ISR(TIMER1_OVF_vect) from dscClockInterrupt() and is disabled in the ISR for a one-shot timer + #if defined(__AVR__) + TCCR1A = 0; + TCCR1B = 0; + TIMSK1 |= (1 << TOIE1); + + // esp8266 timer1 calls dscDataInterrupt() from dscClockInterrupt() as a one-shot timer + #elif defined(ESP8266) + timer1_isr_init(); + timer1_attachInterrupt(dscDataInterrupt); + timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE); + + // esp32 timer1 calls dscDataInterrupt() from dscClockInterrupt() + #elif defined(ESP32) + timer1 = timerBegin(1, 80, true); + timerStop(timer1); + timerAttachInterrupt(timer1, &dscDataInterrupt, true); + timerAlarmWrite(timer1, 250, true); + timerAlarmEnable(timer1); + #endif + + // Generates an interrupt when the Keybus clock rises or falls - requires a hardware interrupt pin on Arduino/AVR + attachInterrupt(digitalPinToInterrupt(dscClockPin), dscClockInterrupt, CHANGE); +} + + +void dscClassicInterface::stop() { + + // Disables Arduino/AVR Timer1 interrupts + #if defined(__AVR__) + TIMSK1 = 0; + + // Disables esp8266 timer1 + #elif defined(ESP8266) + timer1_disable(); + timer1_detachInterrupt(); + + // Disables esp32 timer1 + #elif defined(ESP32) + timerAlarmDisable(timer1); + timerEnd(timer1); + #endif + + // Disables the Keybus clock pin interrupt + detachInterrupt(digitalPinToInterrupt(dscClockPin)); + + // Resets the panel capture data and counters + panelBufferLength = 0; + for (byte i = 0; i < dscReadSize; i++) { + isrPanelData[i] = 0; + isrPC16Data[i] = 0; + isrModuleData[i] = 0; + } + isrPanelBitTotal = 0; + isrPanelBitCount = 0; + isrPanelByteCount = 0; + + // Resets the keypad and module data counters + isrModuleBitTotal = 0; + isrModuleBitCount = 0; + isrModuleByteCount = 0; +} + + +bool dscClassicInterface::loop() { + + #if defined(ESP8266) || defined(ESP32) + yield(); + #endif + + // Checks if Keybus data is detected and sets a status flag if data is not detected for 3s + #if defined(ESP32) + portENTER_CRITICAL(&timer1Mux); + #else + noInterrupts(); + #endif + + if (millis() - keybusTime > 3000) keybusConnected = false; // keybusTime is set in dscDataInterrupt() when the clock resets + else keybusConnected = true; + + #if defined(ESP32) + portEXIT_CRITICAL(&timer1Mux); + #else + interrupts(); + #endif + + if (previousKeybus != keybusConnected) { + previousKeybus = keybusConnected; + keybusChanged = true; + if (!pauseStatus) statusChanged = true; + if (!keybusConnected) return true; + } + + // Writes keys when multiple keys are sent as a char array + if (writeKeysPending) writeKeys(writeKeysArray); + + // Skips processing if the panel data buffer is empty + if (panelBufferLength == 0) return false; + + // Copies data from the buffer to panelData[] + static byte panelBufferIndex = 1; + byte dataIndex = panelBufferIndex - 1; + for (byte i = 0; i < dscReadSize; i++) { + panelData[i] = panelBuffer[dataIndex][i]; + pc16Data[i] = pc16Buffer[dataIndex][i]; + } + panelBitCount = panelBufferBitCount[dataIndex]; + panelByteCount = panelBufferByteCount[dataIndex]; + panelBufferIndex++; + + // Resets counters when the buffer is cleared + #if defined(ESP32) + portENTER_CRITICAL(&timer1Mux); + #else + noInterrupts(); + #endif + + if (panelBufferIndex > panelBufferLength) { + panelBufferIndex = 1; + panelBufferLength = 0; + } + + #if defined(ESP32) + portEXIT_CRITICAL(&timer1Mux); + #else + interrupts(); + #endif + + // Waits at startup for valid data + static bool startupCycle = true; + if (startupCycle) { + if (panelByteCount != 2 || pc16Data[0] == 0xFF) return false; + else { + startupCycle = false; + writeReady = true; + } + } + + // Sets writeReady status + if (!writeKeyPending && !writeKeysPending) writeReady = true; + else writeReady = false; + + processPanelStatus(); + return true; +} + + +// Resets the state of all status components as changed for sketches to get the current status +void dscClassicInterface::resetStatus() { + statusChanged = true; + keybusChanged = true; + troubleChanged = true; + readyChanged[0] = true; + armedChanged[0] = true; + alarmChanged[0] = true; + fireChanged[0] = true; + openZonesStatusChanged = true; + alarmZonesStatusChanged = true; + openZonesChanged[0] = 0xFF; + alarmZonesChanged[0] = 0xFF; + pgmOutputsChanged[0] = true; +} + + +// Processes status +void dscClassicInterface::processPanelStatus() { + + // Keypad lights - maps Classic series keypad lights to PowerSeries keypad light order for sketch compatibility + if (bitRead(panelData[1], 7)) { + readyLight = true; + bitWrite(lights[0], 0, 1); + } + else { + readyLight = false; + bitWrite(lights[0], 0, 0); + } + if (bitRead(panelData[1], 6)) { + armedLight = true; + bitWrite(lights[0], 1, 1); + } + else { + armedLight = false; + bitWrite(lights[0], 1, 0); + } + if (bitRead(panelData[1], 5)) { + memoryLight = true; + bitWrite(lights[0], 2, 1); + } + else { + memoryLight = false; + bitWrite(lights[0], 2, 0); + } + if (bitRead(panelData[1], 4)) { + bypassLight = true; + bitWrite(lights[0], 3, 1); + } + else { + bypassLight = false; + bitWrite(lights[0], 3, 0); + } + if (bitRead(panelData[1], 3)) { + troubleLight = true; + bitWrite(lights[0], 4, 1); + } + else { + troubleLight = false; + bitWrite(lights[0], 4, 0); + } + if (bitRead(panelData[1], 2)) { + programLight = true; + bitWrite(lights[0], 5, 1); + } + else { + programLight = false; + bitWrite(lights[0], 5, 0); + } + if (bitRead(panelData[1], 1)) { + fireLight = true; + bitWrite(lights[0], 6, 1); + } + else { + fireLight = false; + bitWrite(lights[0], 6, 0); + } + if (bitRead(panelData[1], 0)) beep = true; + else beep = false; + + if (lights[0] != previousLights) { + previousLights = lights[0]; + if (!pauseStatus) statusChanged = true; + } + + // PC-16 status + if (bitRead(pc16Data[1], 7)) troubleBit = true; + else troubleBit = false; + if (bitRead(pc16Data[1], 6)) armedBypassBit = true; + else armedBypassBit = false; + if (bitRead(pc16Data[1], 5)) armedBit = true; + else armedBit = false; + if (bitRead(pc16Data[1], 0)) alarmBit = true; + else alarmBit = false; + + + // Checks for memory light blinking + static unsigned long memoryLightTimeOn = 0; + static unsigned long memoryLightTimeOff = 0; + if (memoryLight) { + memoryLightTimeOn = millis(); + if (millis() - memoryLightTimeOff < 600) { + memoryBlink = true; + lightBlink = true; + } + else { + memoryBlink = false; + if (!armedBlink && !bypassBlink && !troubleBlink) lightBlink = false; + } + } + else { + memoryLightTimeOff = millis(); + if (millis() - memoryLightTimeOn > 600) { + memoryBlink = false; + if (!armedBlink && !bypassBlink && !troubleBlink) lightBlink = false; + } + } + + // Checks for armed light blinking + static unsigned long armedLightTimeOn = 0; + static unsigned long armedLightTimeOff = 0; + if (armedLight) { + armedLightTimeOn = millis(); + if (millis() - armedLightTimeOff < 600) { + armedBlink = true; + lightBlink = true; + } + else { + armedBlink = false; + if (!memoryBlink && !bypassBlink && !troubleBlink) lightBlink = false; + } + } + else { + armedLightTimeOff = millis(); + if (millis() - armedLightTimeOn > 1200) { + armedBlink = false; + if (!memoryBlink && !bypassBlink && !troubleBlink) lightBlink = false; + } + } + + // Checks for bypass light blinking + static unsigned long bypassLightTimeOn = 0; + static unsigned long bypassLightTimeOff = 0; + if (bypassLight) { + bypassLightTimeOn = millis(); + if (millis() - bypassLightTimeOff < 600) { + bypassBlink = true; + lightBlink = true; + } + else { + bypassBlink = false; + if (!memoryBlink && !armedBlink && !troubleBlink) lightBlink = false; + } + } + else { + bypassLightTimeOff = millis(); + if (millis() - bypassLightTimeOn > 1200) { + bypassBlink = false; + if (!memoryBlink && !armedBlink && !troubleBlink) lightBlink = false; + } + } + + // Checks for trouble light blinking + static unsigned long troubleLightTimeOn = 0; + static unsigned long troubleLightTimeOff = 0; + if (troubleLight) { + troubleLightTimeOn = millis(); + if (millis() - troubleLightTimeOff < 600) { + troubleBlink = true; + lightBlink = true; + } + else { + troubleBlink = false; + if (!memoryBlink && !armedBlink && !bypassBlink) lightBlink = false; + } + } + else { + troubleLightTimeOff = millis(); + if (millis() - troubleLightTimeOn > 1200) { + troubleBlink = false; + if (!memoryBlink && !armedBlink && !bypassBlink) lightBlink = false; + } + } + + // Checks for beep status + static unsigned long beepTimeOn = 0; + static unsigned long beepTimeOff = 0; + if (beep) { + beepTimeOn = millis(); + } + else if (millis() - beepTimeOn > 500) { + beepTimeOff = millis(); + } + + // Armed status + static bool armedStayTriggered; + if (armedBit) { + armed[0] = true; + exitDelayArmed = true; + if (bypassLight || armedBypassBit) { + armedStay[0] = true; + armedStayTriggered = true; + armedAway[0] = false; + } + else if (armedStayTriggered) { + if (!beep && !alarmBit && millis() - beepTimeOff > 2000) { + armedStay[0] = false; + armedAway[0] = true; + } + } + else { + armedStay[0] = false; + armedAway[0] = true; + } + + if (armedBlink) { + noEntryDelay[0] = true; + exitState[0] = DSC_EXIT_NO_ENTRY_DELAY; + } + + // Reset ready status + processReadyStatus(false); + } + else { + armedStayTriggered = false; + processArmedStatus(false); + processAlarmStatus(false); + } + + if (armed[0] != previousArmed || armedStay[0] != previousArmedStay || armedAway[0] != previousArmedAway) { + previousArmed = armed[0]; + previousArmedStay = armedStay[0]; + previousArmedAway = armedAway[0]; + armedChanged[0] = true; + if (!pauseStatus) statusChanged = true; + } + + // Ready status + if (readyLight && !armedBit) { + processReadyStatus(true); + processArmedStatus(false); + processAlarmStatus(false); + exitDelayArmed = false; + previousAlarmTriggered = false; + starKeyDetected = false; + if (!armedBlink) noEntryDelay[0] = false; + + if (armedLight) { + processExitDelayStatus(true); + exitDelayTriggered = true; + if (exitState[0] != DSC_EXIT_NO_ENTRY_DELAY) { + if (bypassLight) exitState[0] = DSC_EXIT_STAY; + else exitState[0] = DSC_EXIT_AWAY; + if (exitState[0] != previousExitState) { + previousExitState = exitState[0]; + exitDelayChanged[0] = true; + exitStateChanged[0] = true; + if (!pauseStatus) statusChanged = true; + } + } + } + + // Reset armed status + else if (!exitDelayArmed && !armedBlink && millis() - armedLightTimeOn > 600) { + processExitDelayStatus(false); + exitState[0] = 0; + } + } + + else { + if (panelData[0]) processReadyStatus(false); + + if (exitDelayArmed && !armedBit) { + processReadyStatus(false); + exitDelayArmed = false; + } + if (exitDelay[0] && armedBit) { + processExitDelayStatus(false); + } + } + + // Zones status + byte zonesChanged; + if (!previousAlarmTriggered && !memoryBlink && !bypassBlink && !troubleBlink && !starKeyDetected) { + for (byte bit = 7; bit <= 7; bit--) { + if ((!bitRead(zonesTriggered, bit) && !alarmBit) || exitDelay[0]) { + if (bitRead(panelData[0], bit)) { + bitWrite(openZones[0], 7 - bit, 1); + } + else bitWrite(openZones[0], 7 - bit, 0); + } + } + zonesChanged = openZones[0] ^ previousOpenZones; + if (zonesChanged != 0) { + previousOpenZones = openZones[0]; + openZonesStatusChanged = true; + if (!pauseStatus) statusChanged = true; + + for (byte zoneBit = 0; zoneBit < 8; zoneBit++) { + if (bitRead(zonesChanged, zoneBit)) { + bitWrite(openZonesChanged[0], zoneBit, 1); + } + } + } + } + + // Alarm zones status + for (byte bit = 7; bit > 1; bit--) { + if (bitRead(pc16Data[0], bit)) { + bitWrite(alarmZones[0], 7 - bit, 1); + bitWrite(zonesTriggered, 7 - bit, 1); + } + else bitWrite(alarmZones[0], 7 - bit, 0); + } + zonesChanged = alarmZones[0] ^ previousAlarmZones; + if (zonesChanged != 0) { + previousAlarmZones = alarmZones[0]; + alarmZonesStatusChanged = true; + if (!pauseStatus) statusChanged = true; + + for (byte zoneBit = 0; zoneBit < 8; zoneBit++) { + if (bitRead(zonesChanged, zoneBit)) { + bitWrite(alarmZonesChanged[0], zoneBit, 1); + + // Handles zones closed during an alarm + if (alarmBit) { + if (bitRead(alarmZones[0], zoneBit)) { + bitWrite(openZones[0], zoneBit, 1); + bitWrite(openZonesChanged[0], zoneBit, 1); + openZonesStatusChanged = true; + } + else { + bitWrite(openZones[0], zoneBit, 0); + bitWrite(openZonesChanged[0], zoneBit, 1); + openZonesStatusChanged = true; + } + previousOpenZones = openZones[0]; + } + } + } + } + + // Alarm status - requires PGM output section 24 configured to option 08: Strobe Output + if ((panelData[1] & 0xFE) != 0) { + if (alarmBit && !memoryBlink) { + processReadyStatus(false); + processAlarmStatus(true); + alarmTriggered = true; + } + else if (!memoryBlink && !armedChanged[0]) { + processAlarmStatus(false); + if (alarmTriggered) { + alarmTriggered = false; + previousAlarmTriggered = true; + } + } + } + + // Trouble status + if (troubleBit) trouble = true; + else trouble = false; + if (trouble != previousTrouble) { + previousTrouble = trouble; + troubleChanged = true; + if (!pauseStatus) statusChanged = true; + } + + // Fire status + if (bitRead(pc16Data[0], 0)) fire[0] = true; + else fire[0] = false; + if (fire[0] != previousFire) { + previousFire = fire[0]; + fireChanged[0] = true; + if (!pauseStatus) statusChanged = true; + } + + // Keypad Fire alarm + if (bitRead(pc16Data[1], 1)) { + static unsigned long previousFireAlarm = 0; + if (millis() - previousFireAlarm > 1000) { + keypadFireAlarm = true; + previousFireAlarm = millis(); + if (!pauseStatus) statusChanged = true; + } + } + + // Keypad Aux alarm + if (bitRead(pc16Data[1], 2)) { + static unsigned long previousAuxAlarm = 0; + if (millis() - previousAuxAlarm > 1000) { + keypadAuxAlarm = true; + previousAuxAlarm = millis(); + if (!pauseStatus) statusChanged = true; + } + } + + // Keypad Panic alarm + if (bitRead(pc16Data[1], 3)) { + static unsigned long previousPanicAlarm = 0; + if (millis() - previousPanicAlarm > 1000) { + keypadPanicAlarm = true; + previousPanicAlarm = millis(); + if (!pauseStatus) statusChanged = true; + } + } + + // Status - sets the status to match PowerSeries status codes for sketch compatibility + if (memoryBlink && bypassBlink && troubleBlink) { + status[0] = 0xE4; // Programming + } + else { + if (readyChanged[0]) { + if (ready[0]) status[0] = 0x01; // Partition ready + else if (openZonesStatusChanged && openZones[0]) status[0] = 0x03; // Zones open + } + + if (armedChanged[0]) { + if (armed[0]) { + if (armedAway[0]) status[0] = 0x05; // Armed away + else if (armedStay[0]) status[0] = 0x04; // Armed stay + + if (noEntryDelay[0]) status[0] = 0x06; // Armed with no entry delay + } + else status[0] = 0x3E; // Disarmed + } + + if (alarmChanged[0]) { + if (alarm[0]) status[0] = 0x11; // Alarm + else if (!armedChanged[0]) status[0] = 0x3E; // Disarmed + } + + if (exitDelayChanged[0]) { + if (exitDelay[0]) status[0] = 0x08; // Exit delay in progress + else if (!armed[0]) status[0] = 0x3E; // Disarmed + } + + if (status[0] == 0x3E) { + if (ready[0]) status[0] = 0x01; + else if (openZones[0]) status[0] = 0x03; + } + } + + if (status[0] != previousStatus) { + previousStatus = status[0]; + if (!pauseStatus) statusChanged = true; + } +} + + +void dscClassicInterface::processReadyStatus(bool status) { + ready[0] = status; + if (ready[0] != previousReady) { + previousReady = ready[0]; + readyChanged[0] = true; + if (!pauseStatus) statusChanged = true; + } +} + + +void dscClassicInterface::processAlarmStatus(bool status) { + alarm[0] = status; + if (alarm[0] != previousAlarm) { + previousAlarm = alarm[0]; + alarmChanged[0] = true; + if (!pauseStatus) statusChanged = true; + } +} + + +void dscClassicInterface::processExitDelayStatus(bool status) { + exitDelay[0] = status; + if (exitDelay[0] != previousExitDelay) { + previousExitDelay = exitDelay[0]; + exitDelayChanged[0] = true; + if (!pauseStatus) statusChanged = true; + } +} + + +void dscClassicInterface::processArmedStatus(bool armedStatus) { + armedStay[0] = armedStatus; + armedAway[0] = armedStatus; + armed[0] = armedStatus; + + if (armed[0] != previousArmed) { + previousArmed = armed[0]; + armedChanged[0] = true; + if (!pauseStatus) statusChanged = true; + } +} + + +bool dscClassicInterface::handleModule() { + if (!moduleDataCaptured) return false; + moduleDataCaptured = false; + + if (moduleBitCount < 8) return false; + + return true; +} + +// Sets up writes for a single key +void dscClassicInterface::write(const char receivedKey) { + + // Blocks if a previous write is in progress + while(writeKeyPending || writeKeysPending) { + loop(); + #if defined(ESP8266) + yield(); + #endif + } + + if (strlen(accessCodeStay) < 4) { + setWriteKey(receivedKey); + } + else { + switch(receivedKey) { + case 's': + case 'S': write(accessCodeStay); break; + case 'w': + case 'W': write(accessCodeAway); break; + case 'n': + case 'N': write(accessCodeNight); break; + default: setWriteKey(receivedKey); + } + } +} + + +// Sets up writes for multiple keys sent as a char array +void dscClassicInterface::write(const char *receivedKeys, bool blockingWrite) { + + // Blocks if a previous write is in progress + while(writeKeyPending || writeKeysPending) { + loop(); + #if defined(ESP8266) + yield(); + #endif + } + + if (strlen(receivedKeys) == 1) { + write(receivedKeys[0]); + return; + } + + writeKeysArray = receivedKeys; + + if (writeKeysArray[0] != '\0') { + writeKeysPending = true; + writeReady = false; + } + + // Optionally blocks until the write is complete, necessary if the received keys char array is ephemeral + if (blockingWrite) { + while (writeKeysPending) { + writeKeys(writeKeysArray); + loop(); + #if defined(ESP8266) + yield(); + #endif + } + } + else writeKeys(writeKeysArray); +} + + +// Writes multiple keys from a char array +void dscClassicInterface::writeKeys(const char *writeKeysArray) { + static byte writeCounter = 0; + + if (!writeKeyPending && writeKeysPending && writeCounter < strlen(writeKeysArray)) { + if (writeKeysArray[writeCounter] != '\0') { + setWriteKey(writeKeysArray[writeCounter]); + writeCounter++; + if (writeKeysArray[writeCounter] == '\0') { + writeKeysPending = false; + writeCounter = 0; + } + } + } +} + + +// Specifies the key value to be written by dscClockInterrupt() and selects the write partition. This includes a 500ms +// delay after alarm keys to resolve errors when additional keys are sent immediately after alarm keys. +void dscClassicInterface::setWriteKey(const char receivedKey) { + static unsigned long previousTime; + + + // Sets the binary to write for virtual keypad keys + if (!writeKeyPending && (millis() - previousTime > 500 || millis() <= 500)) { + bool validKey = true; + + // Sets binary for virtual keypad keys + switch (receivedKey) { + case '0': writeKey = 0xD7; break; + case '1': writeKey = 0xBE; break; + case '2': writeKey = 0xDE; break; + case '3': writeKey = 0xEE; break; + case '4': writeKey = 0xBD; break; + case '5': writeKey = 0xDD; break; + case '6': writeKey = 0xED; break; + case '7': writeKey = 0xBB; break; + case '8': writeKey = 0xDB; break; + case '9': writeKey = 0xEB; break; + case '*': writeKey = 0xB7; break; + case '#': writeKey = 0xE7; break; + case 'F': + case 'f': writeKey = 0x3F; writeAlarm = true; break; // Keypad fire alarm + case 'A': + case 'a': writeKey = 0x5F; writeAlarm = true; break; // Keypad auxiliary alarm + case 'P': + case 'p': writeKey = 0x6F; writeAlarm = true; break; // Keypad panic alarm + default: { + validKey = false; + break; + } + } + + if (writeAlarm) previousTime = millis(); // Sets a marker to time writes after keypad alarm keys + if (validKey) { + writeKeyPending = true; // Sets a flag indicating that a write is pending, cleared by dscClockInterrupt() + writeReady = false; + } + } +} + + +/* + * printPanelMessage() decodes and prints panel data from + * panelData[] and pc16Data[] + */ +void dscClassicInterface::printPanelMessage() { + + stream->print(F("Lights: ")); + if (panelData[1]) { + if (bitRead(panelData[1], 7)) stream->print(F("Ready ")); + if (bitRead(panelData[1], 6)) stream->print(F("Armed ")); + if (bitRead(panelData[1], 5)) stream->print(F("Memory ")); + if (bitRead(panelData[1], 4)) stream->print(F("Bypass ")); + if (bitRead(panelData[1], 3)) stream->print(F("Trouble ")); + if (bitRead(panelData[1], 2)) stream->print(F("Program ")); + if (bitRead(panelData[1], 1)) stream->print(F("Fire ")); + if (bitRead(panelData[1], 0)) stream->print(F("Beep ")); + } + else stream->print(F("none ")); + + stream->print(F("| Status: ")); + if (pc16Data[1]) { + if (bitRead(pc16Data[1], 7)) stream->print(F("Trouble ")); + if (bitRead(pc16Data[1], 6)) stream->print(F("Bypassed zones ")); + if (bitRead(pc16Data[1], 5)) stream->print(F("Armed (side A) ")); + if (bitRead(pc16Data[1], 4)) stream->print(F("Armed (side B) ")); + if (bitRead(pc16Data[1], 3)) stream->print(F("Keypad Panic alarm ")); + if (bitRead(pc16Data[1], 2)) stream->print(F("Keypad Aux alarm ")); + if (bitRead(pc16Data[1], 1)) stream->print(F("Keypad Fire alarm ")); + if (bitRead(pc16Data[1], 0)) stream->print(F("Alarm ")); + } + else stream->print(F("none ")); + + stream->print(F("| Zones open: ")); + if (panelData[0] == 0) stream->print(F("none ")); + else { + for (byte bit = 7; bit <= 7; bit--) { + if (bitRead(panelData[0], bit)) { + stream->print(8 - bit); + stream->print(F(" ")); + } + } + } + + if (pc16Data[0] & 0xFE) { + stream->print(F("| Zone alarm: ")); + for (byte bit = 7; bit > 1; bit--) { + if (bitRead(pc16Data[0], bit)) { + stream->print(8 - bit); + stream->print(F(" ")); + } + } + } + + if (bitRead(pc16Data[0], 0)) { + stream->print(F("| Fire alarm")); + } +} + + + +// Processes keypad and module notifications and responses to panel queries +void dscClassicInterface::printModuleMessage() { + + stream->print(F("[Keypad] ")); + if (hideKeypadDigits) { + switch (moduleData[0]) { + case 0xBE: + case 0xDE: + case 0xEE: + case 0xBD: + case 0xDD: + case 0xED: + case 0xBB: + case 0xDB: + case 0xEB: + case 0xD7: Serial.print("[Digit]"); break; + case 0xB7: Serial.print("*"); break; + case 0xE7: Serial.print("#"); break; + case 0x3F: Serial.print(F("Fire alarm")); break; + case 0x5F: Serial.print(F("Aux alarm")); break; + case 0x6F: Serial.print(F("Panic alarm")); break; + } + } + + else { + switch (moduleData[0]) { + case 0xBE: Serial.print("1"); break; + case 0xDE: Serial.print("2"); break; + case 0xEE: Serial.print("3"); break; + case 0xBD: Serial.print("4"); break; + case 0xDD: Serial.print("5"); break; + case 0xED: Serial.print("6"); break; + case 0xBB: Serial.print("7"); break; + case 0xDB: Serial.print("8"); break; + case 0xEB: Serial.print("9"); break; + case 0xD7: Serial.print("0"); break; + case 0xB7: Serial.print("*"); break; + case 0xE7: Serial.print("#"); break; + case 0x3F: Serial.print(F("Fire alarm")); break; + case 0x5F: Serial.print(F("Aux alarm")); break; + case 0x6F: Serial.print(F("Panic alarm")); break; + } + } +} + + +// Prints the panel message as binary with an optional parameter to print spaces between bytes +void dscClassicInterface::printPanelBinary(bool printSpaces) { + for (byte panelByte = 0; panelByte < panelByteCount; panelByte++) { + for (byte mask = 0x80; mask; mask >>= 1) { + if (mask & panelData[panelByte]) stream->print("1"); + else stream->print("0"); + } + if (printSpaces && panelByte != panelByteCount - 1) stream->print(" "); + } + + if (printSpaces) stream->print(" "); + + for (byte panelByte = 0; panelByte < panelByteCount; panelByte++) { + for (byte mask = 0x80; mask; mask >>= 1) { + if (mask & pc16Data[panelByte]) stream->print("1"); + else stream->print("0"); + } + if (printSpaces && panelByte != panelByteCount - 1) stream->print(" "); + } +} + + +// Prints the panel message as binary with an optional parameter to print spaces between bytes +void dscClassicInterface::printModuleBinary(bool printSpaces) { + bool keypadDigit = false; + if (hideKeypadDigits) { + switch (moduleData[0]) { + case 0xBE: + case 0xDE: + case 0xEE: + case 0xBD: + case 0xDD: + case 0xED: + case 0xBB: + case 0xDB: + case 0xEB: keypadDigit = true; break; + default: keypadDigit = false; + } + } + + for (byte moduleByte = 0; moduleByte < moduleByteCount; moduleByte++) { + if (hideKeypadDigits && keypadDigit && moduleByte == 0) stream->print("........"); + else { + for (byte mask = 0x80; mask; mask >>= 1) { + if (mask & moduleData[moduleByte]) stream->print("1"); + else stream->print("0"); + } + } + if (printSpaces && moduleByte != panelByteCount - 1) stream->print(" "); + } +} + + +// Prints the panel command as hex +void dscClassicInterface::printPanelCommand() { + stream->print(F("Panel")); +} + + +#if defined(__AVR__) +bool dscClassicInterface::redundantPanelData(byte previousCmd[], volatile byte currentCmd[], byte checkedBytes) { +#elif defined(ESP8266) +bool ICACHE_RAM_ATTR dscClassicInterface::redundantPanelData(byte previousCmd[], volatile byte currentCmd[], byte checkedBytes) { +#elif defined(ESP32) +bool IRAM_ATTR dscClassicInterface::redundantPanelData(byte previousCmd[], volatile byte currentCmd[], byte checkedBytes) { +#endif + + bool redundantData = true; + for (byte i = 0; i < checkedBytes; i++) { + if (previousCmd[i] != currentCmd[i]) { + redundantData = false; + break; + } + } + if (redundantData) return true; + else { + for (byte i = 0; i < dscReadSize; i++) previousCmd[i] = currentCmd[i]; + return false; + } +} + + +// Called as an interrupt when the DSC clock changes to write data for virtual keypad and setup timers to read +// data after an interval. +#if defined(__AVR__) +void dscClassicInterface::dscClockInterrupt() { +#elif defined(ESP8266) +void ICACHE_RAM_ATTR dscClassicInterface::dscClockInterrupt() { +#elif defined(ESP32) +void IRAM_ATTR dscClassicInterface::dscClockInterrupt() { +#endif + + // Data sent from the panel and keypads/modules has latency after a clock change (observed up to 160us for + // keypad data). The following sets up a timer for each platform that will call dscDataInterrupt() in + // 250us to read the data line. + + // AVR Timer1 calls dscDataInterrupt() via ISR(TIMER1_OVF_vect) when the Timer1 counter overflows + #if defined(__AVR__) + TCNT1=61535; // Timer1 counter start value, overflows at 65535 in 250us + TCCR1B |= (1 << CS10); // Sets the prescaler to 1 + + // esp8266 timer1 calls dscDataInterrupt() in 250us + #elif defined(ESP8266) + timer1_write(1250); + + // esp32 timer1 calls dscDataInterrupt() in 250us + #elif defined(ESP32) + timerStart(timer1); + portENTER_CRITICAL(&timer1Mux); + #endif + + static unsigned long previousClockHighTime; + if (digitalRead(dscClockPin) == HIGH) { + if (virtualKeypad) digitalWrite(dscWritePin, LOW); // Restores the data line after a virtual keypad write + previousClockHighTime = micros(); + } + + else { + clockHighTime = micros() - previousClockHighTime; // Tracks the clock high time to find the reset between commands + + // Virtual keypad + if (virtualKeypad) { + static bool writeStart = false; + + if (writeKeyPending && millis() - writeCompleteTime > 50) { + writeKeyWait = false; + } + + // Writes a regular key + if (writeKeyPending && !writeKeyWait) { + + if (clockHighTime > 2000) { + if (!((writeKey >> 7) & 0x01)) digitalWrite(dscWritePin, HIGH); + writeStart = true; + } + else if (writeStart && isrPanelBitTotal <= 7) { + if (!((writeKey >> (7 - isrPanelBitCount)) & 0x01)) digitalWrite(dscWritePin, HIGH); + if (isrPanelBitTotal == 7) { + writeKeyPending = false; + writeKeyWait = true; + writeCompleteTime = millis(); + writeStart = false; + } + } + } + } + } + #if defined(ESP32) + portEXIT_CRITICAL(&timer1Mux); + #endif +} + + +// Interrupt function called by AVR Timer1, esp8266 timer1, and esp32 timer1 after 250us to read the data line +#if defined(__AVR__) +void dscClassicInterface::dscDataInterrupt() { +#elif defined(ESP8266) +void ICACHE_RAM_ATTR dscClassicInterface::dscDataInterrupt() { +#elif defined(ESP32) +void IRAM_ATTR dscClassicInterface::dscDataInterrupt() { + timerStop(timer1); + portENTER_CRITICAL(&timer1Mux); +#endif + + static bool skipData = false; + + // Panel sends data while the clock is high + if (digitalRead(dscClockPin) == HIGH) { + + // Stops processing Keybus data at the dscReadSize limit + if (isrPanelByteCount >= dscReadSize) skipData = true; + + else { + if (isrPanelBitCount < 8) { + // Data is captured in each byte by shifting left by 1 bit and writing to bit 0 + isrPanelData[isrPanelByteCount] <<= 1; + isrPC16Data[isrPanelByteCount] <<= 1; + + if (digitalRead(dscReadPin) == HIGH) isrPanelData[isrPanelByteCount] |= 1; + if (digitalRead(dscPC16Pin) == HIGH) isrPC16Data[isrPanelByteCount] |= 1; + } + + // Increments the bit counter if the byte is incomplete + if (isrPanelBitCount < 7) isrPanelBitCount++; + + // Byte is complete, set the counters for the next byte + else { + isrPanelBitCount = 0; + isrPanelByteCount++; + } + + isrPanelBitTotal++; + } + } + + // Keypads and modules send data while the clock is low + else { + static bool moduleDataDetected = false; + + // Saves data and resets counters after the clock cycle is complete (high for at least 1ms) + if (clockHighTime > 2000) { + keybusTime = millis(); + + // Skips incomplete data and redundant data + if (isrPanelBitTotal < 8) skipData = true; + else { + static byte previousPanelData[dscReadSize]; + static byte previousPC16Data[dscReadSize]; + + if (lightBlink && readyLight) skipData = false; + else if (redundantPanelData(previousPanelData, isrPanelData, isrPanelByteCount) && + redundantPanelData(previousPC16Data, isrPC16Data, isrPanelByteCount)) { + skipData = true; + } + } + + // Stores new panel data in the panel buffer + if (panelBufferLength == dscBufferSize) bufferOverflow = true; + else if (!skipData && panelBufferLength < dscBufferSize) { + for (byte i = 0; i < dscReadSize; i++) { + panelBuffer[panelBufferLength][i] = isrPanelData[i]; + pc16Buffer[panelBufferLength][i] = isrPC16Data[i]; + } + panelBufferBitCount[panelBufferLength] = isrPanelBitTotal; + panelBufferByteCount[panelBufferLength] = isrPanelByteCount; + panelBufferLength++; + } + + // Stores new keypad and module data - this data is not buffered + if (processModuleData) { + if (moduleDataDetected) { + moduleDataDetected = false; + moduleDataCaptured = true; // Sets a flag for handleModule() + for (byte i = 0; i < dscReadSize; i++) moduleData[i] = isrModuleData[i]; + moduleBitCount = isrModuleBitTotal; + moduleByteCount = isrModuleByteCount; + } + + // Resets the keypad and module capture data and counters + for (byte i = 0; i < dscReadSize; i++) isrModuleData[i] = 0; + isrModuleBitTotal = 0; + isrModuleBitCount = 0; + isrModuleByteCount = 0; + } + + // Resets the panel capture data and counters + for (byte i = 0; i < dscReadSize; i++) { + isrPanelData[i] = 0; + isrPC16Data[i] = 0; + } + isrPanelBitTotal = 0; + isrPanelBitCount = 0; + isrPanelByteCount = 0; + skipData = false; + } + + // Keypad and module data is not buffered and skipped if the panel data buffer is filling + if (processModuleData && isrModuleByteCount < dscReadSize && panelBufferLength <= 1) { + + // Data is captured in each byte by shifting left by 1 bit and writing to bit 0 + if (isrModuleBitCount < 8) { + isrModuleData[isrModuleByteCount] <<= 1; + if (digitalRead(dscReadPin) == HIGH) { + isrModuleData[isrModuleByteCount] |= 1; + } + else moduleDataDetected = true; // Keypads and modules send data by pulling the data line low + } + + // Byte is complete, set the counters for the next byte + if (isrModuleBitCount == 7) { + isrModuleBitCount = 0; + isrModuleByteCount++; + if (moduleDataDetected && isrModuleData[0] == 0xB7) { + starKeyDetected = true; + } + } + + // Increments the bit counter if the byte is incomplete + else if (isrModuleBitCount < 7) { + isrModuleBitCount++; + } + + isrModuleBitTotal++; + } + + } + #if defined(ESP32) + portEXIT_CRITICAL(&timer1Mux); + #endif +} diff --git a/src/dscClassic.h b/src/dscClassic.h new file mode 100644 index 0000000..e889556 --- /dev/null +++ b/src/dscClassic.h @@ -0,0 +1,211 @@ +/* + DSC Keybus Interface + + https://github.com/taligentx/dscKeybusInterface + + This library is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#ifndef dscClassic_h +#define dscClassic_h + +#include + +const byte dscPartitions = 1; // Maximum number of partitions - requires 19 bytes of memory per partition +const byte dscZones = 1; // Maximum number of zone groups, 8 zones per group - requires 6 bytes of memory per zone group +const byte dscReadSize = 2; // Maximum bytes of a Keybus command + +#if defined(__AVR__) +const byte dscBufferSize = 10; // Number of commands to buffer if the sketch is busy - requires dscReadSize + 2 bytes of memory per command +#elif defined(ESP8266) +const byte dscBufferSize = 50; +#elif defined(ESP32) +const DRAM_ATTR byte dscBufferSize = 50; +#endif + +// Exit delay target states +#define DSC_EXIT_STAY 1 +#define DSC_EXIT_AWAY 2 +#define DSC_EXIT_NO_ENTRY_DELAY 3 + + +class dscClassicInterface { + + public: + + // Initializes writes as disabled by default + dscClassicInterface(byte setClockPin, byte setReadPin, byte setPC16Pin, byte setWritePin = 255, const char * setAccessCode = ""); + + // Interface control + void begin(Stream &_stream = Serial); // Initializes the stream output to Serial by default + bool loop(); // Returns true if valid panel data is available + void stop(); // Disables the clock hardware interrupt and data timer interrupt + void resetStatus(); // Resets the state of all status components as changed for sketches to get the current status + + // Writes a single key - nonblocking unless a previous write is in progress + void write(const char receivedKey); + + // Writes multiple keys from a char array + // + // By default, this is nonblocking unless there is a previous write in progress - in this case, the sketch must keep the char + // array defined at least until the write is complete. + // + // If the char array is ephemeral, check if the write is complete by checking writeReady or set blockingWrite to true to + // block until the write is complete. + void write(const char * receivedKeys, bool blockingWrite = false); + + // Write control + static byte writePartition; // Set to a partition number for virtual keypad + bool writeReady; // True if the library is ready to write a key + + // Prints output to the stream interface set in begin() + void printPanelBinary(bool printSpaces = true); // Includes spaces between bytes by default + void printPanelCommand(); // Prints the panel command as hex + void printPanelMessage(); // Prints the decoded panel message + void printModuleBinary(bool printSpaces = true); // Includes spaces between bytes by default + void printModuleMessage(); // Prints the decoded keypad or module message + + // These can be configured in the sketch setup() before begin() + bool hideKeypadDigits; // Controls if keypad digits are hidden for publicly posted logs (default: false) + static bool processModuleData; // Controls if keypad and module data is processed and displayed (default: false) + + // Status tracking + bool statusChanged; // True after any status change + bool pauseStatus; // Prevent status from showing as changed, set in sketch to control when to update status + bool keybusConnected, keybusChanged; // True if data is detected on the Keybus + bool trouble, troubleChanged; + bool keypadFireAlarm, keypadAuxAlarm, keypadPanicAlarm; + bool ready[dscPartitions], readyChanged[dscPartitions]; + bool armed[dscPartitions], armedAway[dscPartitions], armedStay[dscPartitions]; + bool noEntryDelay[dscPartitions], armedChanged[dscPartitions]; + bool alarm[dscPartitions], alarmChanged[dscPartitions]; + bool exitDelay[dscPartitions], exitDelayChanged[dscPartitions]; + byte exitState[dscPartitions], exitStateChanged[dscPartitions]; + bool fire[dscPartitions], fireChanged[dscPartitions]; + bool openZonesStatusChanged; + byte openZones[dscZones], openZonesChanged[dscZones]; // Zone status is stored in an array using 1 bit per zone, up to 64 zones + bool alarmZonesStatusChanged; + byte alarmZones[dscZones], alarmZonesChanged[dscZones]; // Zone alarm status is stored in an array using 1 bit per zone, up to 64 zones + bool pgmOutputsStatusChanged; + byte pgmOutputs[1], pgmOutputsChanged[1]; + bool armedLight, memoryLight, bypassLight, troubleLight, programLight, fireLight, beep; + static volatile bool readyLight, lightBlink; + bool readyBlink, armedBlink, memoryBlink, bypassBlink, troubleBlink; + + /* panelData[], pc16Data[], and moduleData[] store panel and keypad/module data in an array. These can + * be accessed directly in the sketch to get data that is not already tracked in the library. See + * dscClassic.cpp for the currently known DSC commands and data. + * + * panelData[] example: + * Byte 0 Byte 2 Byte 3 Byte 4 Byte 5 + * 00000101 0 10000001 00000001 10010001 11000111 [0x05] Partition 1: Ready Backlight - Partition ready | Partition 2: disabled + * ^ Byte 1 (stop bit) + */ + static byte panelData[dscReadSize]; + static byte pc16Data[dscReadSize]; + static volatile byte moduleData[dscReadSize]; + + // status[] and lights[] store the current status message and LED state. These can be accessed directly in the + // sketch to get data that is not already tracked in the library. See printPanelMessages() and + // printPanelLights() in dscClassic.cpp to see how this data translates to the status message and LED status. + byte status[dscPartitions]; + byte lights[dscPartitions]; + + // Process keypad and module data, returns true if data is available + bool handleModule(); + + // True if dscBufferSize needs to be increased + static volatile bool bufferOverflow; + + // Timer interrupt function to capture data - declared as public for use by AVR Timer2 + static void dscDataInterrupt(); + + // Sketch cross-compatibility - these elements are not currently used for the Classic series + byte accessCode[dscPartitions]; + bool accessCodeChanged[dscPartitions]; + bool accessCodePrompt; + bool decimalInput; + bool powerTrouble, powerChanged; + bool batteryTrouble, batteryChanged; + bool disabled[dscPartitions], disabledChanged[dscPartitions]; + bool entryDelay[dscPartitions], entryDelayChanged[dscPartitions]; + byte panelVersion; + bool displayTrailingBits; + bool timestampChanged; + byte hour, minute, day, month; + int year; + bool setTime(unsigned int year, byte month, byte day, byte hour, byte minute, const char* accessCode, byte timePartition = 1); + + private: + + void processPanelStatus(); + void processReadyStatus(bool status); + void processArmed(bool status); + void processArmedStatus(bool status); + void processAlarmStatus(bool status); + void processExitDelayStatus(bool status); + void writeKeys(const char * writeKeysArray); + void setWriteKey(const char receivedKey); + static void dscClockInterrupt(); + static bool redundantPanelData(byte previousCmd[], volatile byte currentCmd[], byte checkedBytes = dscReadSize); + + #if defined(ESP32) + static hw_timer_t * timer1; + static portMUX_TYPE timer1Mux; + #endif + + Stream* stream; + const char * writeKeysArray; + const char * accessCodeStay; + char accessCodeAway[7]; + char accessCodeNight[7]; + bool writeKeysPending; + bool writeArm; + bool previousTrouble; + bool previousKeybus; + byte previousLights, previousStatus; + bool previousReady; + bool previousExitDelay, previousEntryDelay, exitDelayArmed, exitDelayTriggered; + byte previousExitState; + bool previousArmed, previousArmedStay, previousArmedAway; + bool previousAlarm; + bool alarmTriggered, previousAlarmTriggered; + byte zonesTriggered; + bool previousFire; + byte previousOpenZones, previousAlarmZones; + byte previousPgmOutput; + bool troubleBit, armedBypassBit, armedBit, alarmBit; + + static byte dscClockPin; + static byte dscReadPin; + static byte dscPC16Pin; + static byte dscWritePin; + static byte writeByte, writeBit; + static bool virtualKeypad; + static char writeKey; + static byte panelBitCount, panelByteCount; + static volatile bool writeKeyPending, writeKeyWait; + static volatile bool writeAlarm, starKeyDetected, starKeyCheck, starKeyWait; + static volatile bool moduleDataCaptured; + static volatile unsigned long clockHighTime, keybusTime, writeCompleteTime; + static volatile byte panelBufferLength; + static volatile byte panelBuffer[dscBufferSize][dscReadSize], pc16Buffer[dscBufferSize][dscReadSize]; + static volatile byte panelBufferBitCount[dscBufferSize], panelBufferByteCount[dscBufferSize]; + static volatile byte moduleBitCount, moduleByteCount; + static volatile byte moduleCmd; + static volatile byte isrPanelData[dscReadSize], isrPC16Data[dscReadSize], isrPanelBitTotal, isrPanelBitCount, isrPanelByteCount; + static volatile byte isrModuleData[dscReadSize], isrModuleBitTotal, isrModuleBitCount, isrModuleByteCount; +}; + +#endif // dscClassic_h diff --git a/src/dscClassicKeypad.cpp b/src/dscClassicKeypad.cpp new file mode 100644 index 0000000..bde8357 --- /dev/null +++ b/src/dscClassicKeypad.cpp @@ -0,0 +1,395 @@ +/* + DSC Keybus Interface + + https://github.com/taligentx/dscKeybusInterface + + This library is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#include "dscClassicKeypad.h" + +#if defined(ESP32) +portMUX_TYPE dscClassicKeypadInterface::timer1Mux = portMUX_INITIALIZER_UNLOCKED; +hw_timer_t * dscClassicKeypadInterface::timer1 = NULL; +#endif // ESP32 + + +dscClassicKeypadInterface::dscClassicKeypadInterface(byte setClockPin, byte setReadPin, byte setWritePin) { + dscClockPin = setClockPin; + dscReadPin = setReadPin; + dscWritePin = setWritePin; + commandReady = true; + keyData = 0xFF; + clockInterval = 50000; // Sets AVR timer 1 to trigger an overflow interrupt every ~1ms to generate a 500Hz clock signal + keyInterval = 150; + alarmKeyInterval = 1000; +} + + +void dscClassicKeypadInterface::begin(Stream &_stream) { + pinMode(dscClockPin, OUTPUT); + pinMode(dscReadPin, INPUT); + pinMode(dscWritePin, OUTPUT); + digitalWrite(dscClockPin, LOW); + digitalWrite(dscWritePin, LOW); + stream = &_stream; + + // Platform-specific timers setup the Keybus 1kHz clock signal + + // Arduino/AVR Timer1 calls ISR(TIMER1_OVF_vect) + #if defined(__AVR__) + TCCR1A = 0; + TCCR1B = 0; + TCNT1 = clockInterval; + TCCR1B |= (1 << CS10); + + // esp8266 timer1 calls dscClockInterrupt() + #elif defined(ESP8266) + timer1_isr_init(); + timer1_attachInterrupt(dscClockInterrupt); + timer1_write(5000); + + // esp32 timer1 calls dscClockInterrupt() + #elif defined(ESP32) + timer1 = timerBegin(1, 80, true); + timerStop(timer1); + timerAttachInterrupt(timer1, &dscClockInterrupt, true); + timerAlarmWrite(timer1, 1000, true); + timerAlarmEnable(timer1); + #endif + + intervalStart = millis(); + + unsigned long keybusTime = millis(); + while (millis() - keybusTime < 100) { // Waits for the keypad to be powered on + if (!digitalRead(dscReadPin)) keybusTime = millis(); + #if defined(ESP8266) || defined(ESP32) + yield(); + #endif + } +} + + +bool dscClassicKeypadInterface::loop() { + + // Sets up the next panel command once the previous command is complete + if (commandReady && millis() - intervalStart >= commandInterval) { + commandReady = false; + + // Sets lights + if (panelLights != previousLights) { + previousLights = panelLights; + classicCommand[1] = panelLights; + } + + // Sets zones + if (panelZones != previousZones) { + previousZones = panelZones; + classicCommand[0] = panelZones; + } + + // Key beep + if (keyBeep) { + if (!beepStart) { + beepStart = true; + beepInterval = millis(); + bitWrite(classicCommand[1], 0, 1); + } + else if (millis() - beepInterval > 100) { + beepStart = false; + keyBeep = false; + bitWrite(classicCommand[1], 0, 0); + } + } + + // Sets next panel command + for (byte i = 0; i < 2; i++) panelCommand[i] = classicCommand[i]; + panelCommandByteTotal = 2; + + clockCycleCount = 0; + clockCycleTotal = panelCommandByteTotal * 16; + + #if defined(__AVR__) + TIMSK1 |= (1 << TOIE1); // Enables AVR Timer 1 interrupt + #elif defined(ESP8266) + timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); + #elif defined(ESP32) + timerStart(timer1); + #endif + } + else if (!commandReady) intervalStart = millis(); + + // Sets panel lights + panelLight(lightReady, 7); + panelLight(lightArmed, 6); + panelLight(lightMemory, 5); + panelLight(lightBypass, 4); + panelLight(lightTrouble, 3); + panelLight(lightProgram, 2); + panelLight(lightFire, 1); + + // Sets zone lights + zoneLight(lightZone1, 7); + zoneLight(lightZone2, 6); + zoneLight(lightZone3, 5); + zoneLight(lightZone4, 4); + zoneLight(lightZone5, 3); + zoneLight(lightZone6, 2); + zoneLight(lightZone7, 1); + zoneLight(lightZone8, 0); + + // Skips key processing if the key buffer is empty + if (keyBufferLength == 0) return false; + + // Copies data from the buffer to keyData + static byte keyBufferIndex = 1; + byte dataIndex = keyBufferIndex - 1; + keyData = keyBuffer[dataIndex]; + keyBufferIndex++; + + // Resets counters when the buffer is cleared + #if defined(ESP32) + portENTER_CRITICAL(&timer1Mux); + #else + noInterrupts(); + #endif + + if (keyBufferIndex > keyBufferLength) { + keyBufferIndex = 1; + keyBufferLength = 0; + } + + #if defined(ESP32) + portEXIT_CRITICAL(&timer1Mux); + #else + interrupts(); + #endif + + if (keyData != 0xFF) { + keyAvailable = true; + keyBeep = true; + switch (keyData) { + case 0xD7: key = 0x00; break; // 0 + case 0xBE: key = 0x05; break; // 1 + case 0xDE: key = 0x0A; break; // 2 + case 0xEE: key = 0x0F; break; // 3 + case 0xBD: key = 0x11; break; // 4 + case 0xDD: key = 0x16; break; // 5 + case 0xED: key = 0x1B; break; // 6 + case 0xBB: key = 0x1C; break; // 7 + case 0xDB: key = 0x22; break; // 8 + case 0xEB: key = 0x27; break; // 9 + case 0xB7: key = 0x28; break; // * + case 0xE7: key = 0x2D; break; // # + case 0x3F: key = 0x0B; break; // Fire alarm + case 0x5F: key = 0x0D; break; // Aux alarm + case 0x6F: key = 0x0E; break; // Panic alarm + default: keyAvailable = false; keyBeep = false; break; // Skips other DSC key values and invalid data + } + keyData = 0xFF; + } + + return true; +} + + +void dscClassicKeypadInterface::panelLight(Light lightPanel, byte zoneBit) { + if (lightPanel == on) { + bitWrite(panelLights, zoneBit, 1); + bitWrite(panelBlink, zoneBit, 0); + } + else if (lightPanel == blink) bitWrite(panelBlink, zoneBit, 1); + else { + bitWrite(panelLights, zoneBit, 0); + bitWrite(panelBlink, zoneBit, 0); + } +} + + +void dscClassicKeypadInterface::zoneLight(Light lightZone, byte zoneBit) { + if (lightZone == on ) { + bitWrite(panelZones, zoneBit, 1); + bitWrite(panelZonesBlink, zoneBit, 0); + } + else if (lightZone == blink) bitWrite(panelZonesBlink, zoneBit, 1); + else { + bitWrite(panelZones, zoneBit, 0); + bitWrite(panelZonesBlink, zoneBit, 0); + } +} + + + +void dscClassicKeypadInterface::beep(byte beeps) { + (void) beeps; +} + + +void dscClassicKeypadInterface::tone(byte beep, bool tone, byte interval) { + (void) beep; + (void) tone; + (void) interval; +} + + +void dscClassicKeypadInterface::buzzer(byte seconds) { + (void) seconds; +} + + +#if defined(__AVR__) +void dscClassicKeypadInterface::dscClockInterrupt() { +#elif defined(ESP8266) +void ICACHE_RAM_ATTR dscClassicKeypadInterface::dscClockInterrupt() { +#elif defined(ESP32) +void IRAM_ATTR dscClassicKeypadInterface::dscClockInterrupt() { +#endif + + // Toggles the clock pin for the length of a panel command + if (clockCycleCount < clockCycleTotal) { + static bool clockHigh = true; + if (clockHigh) { + clockHigh = false; + digitalWrite(dscClockPin, HIGH); + digitalWrite(dscWritePin, LOW); + } + else { + clockHigh = true; + digitalWrite(dscClockPin, LOW); + if (isrModuleByteCount < dscReadSize) { + + // Data is captured in each byte by shifting left by 1 bit and writing to bit 0 + if (isrModuleBitCount < 8) { + isrModuleData[isrModuleByteCount] <<= 1; + if (digitalRead(dscReadPin) == HIGH) { + isrModuleData[isrModuleByteCount] |= 1; + } + else { + moduleDataDetected = true; // Keypads and modules send data by pulling the data line low + } + } + + // Increments the bit counter if the byte is incomplete + if (isrModuleBitCount < 7) { + isrModuleBitCount++; + } + + // Byte is complete, set the counters for the next byte + else { + isrModuleBitCount = 0; + isrModuleByteCount++; + } + + isrModuleBitTotal++; + } + + // Write panel data + if (isrPanelBitCount == 7) { + if (!bitRead(panelCommand[panelCommandByteCount], 0)) digitalWrite(dscWritePin, HIGH); + isrPanelBitCount = 0; + isrPanelBitTotal++; + panelCommandByteCount++; + } + + // Panel command bytes bits 0-6 + else if (panelCommandByteCount < panelCommandByteTotal) { + byte bitCount = 0; + for (byte i = 7; i > 0; i--) { + if (isrPanelBitCount == bitCount && !bitRead(panelCommand[panelCommandByteCount], i)) digitalWrite(dscWritePin, HIGH); + bitCount++; + } + isrPanelBitCount++; + isrPanelBitTotal++; + } + } + clockCycleCount++; + } + + // Panel command complete + else { + digitalWrite(dscClockPin, LOW); + + // Checks for module data + if (moduleDataDetected) { + moduleDataDetected = false; + for (byte i = 0; i < dscReadSize; i++) moduleData[i] = isrModuleData[i]; + + if (isrModuleData[0] != 0xFF) { + + // Checks that alarm keys are pressed continuously for the alarmKeyInterval before setting the key + if (isrModuleData[0] == 0x3F || isrModuleData[0] == 0x5F || isrModuleData[0] == 0x6F) { + if (!alarmKeyDetected) { + alarmKeyDetected = true; + alarmKeyTime = millis(); + } + else if (millis() - alarmKeyTime > alarmKeyInterval) { + keyBuffer[keyBufferLength] = isrModuleData[0]; + keyBufferLength++; + alarmKeyDetected = false; + } + else { + keyBuffer[keyBufferLength] = 0xFF; + keyBufferLength++; + } + } + + // Checks for regular keys and debounces for keyInterval + else { + alarmKeyDetected = false; + alarmKeyTime = millis(); + + // Skips the debounce interval if a different key is pressed + if (keyBuffer[keyBufferLength] != isrModuleData[0]) { + keyBuffer[keyBufferLength] = isrModuleData[0]; + keyBufferLength++; + repeatInterval = millis(); + } + + // Sets the key + else if (millis() - repeatInterval > keyInterval) { + keyBuffer[keyBufferLength] = isrModuleData[0]; + keyBufferLength++; + repeatInterval = millis(); + } + } + } + } + else { + alarmKeyDetected = false; + alarmKeyTime = millis(); + } + + // Resets counters + for (byte i = 0; i < dscReadSize; i++) isrModuleData[i] = 0; + isrModuleBitTotal = 0; + isrModuleBitCount = 0; + isrModuleByteCount = 0; + panelCommandByteCount = 0; + isrPanelBitTotal = 0; + isrPanelBitCount = 0; + commandReady = true; + + #if defined(__AVR__) + TIMSK1 = 0; // Disables AVR Timer 1 interrupt + #elif defined(ESP8266) + timer1_disable(); + #elif defined(ESP32) + timerStop(timer1); + #endif + } + + #if defined(__AVR__) + TCNT1 = clockInterval; + #endif +} diff --git a/src/dscClassicKeypad.h b/src/dscClassicKeypad.h new file mode 100644 index 0000000..47e2d8c --- /dev/null +++ b/src/dscClassicKeypad.h @@ -0,0 +1,103 @@ +/* + DSC Keybus Interface + + https://github.com/taligentx/dscKeybusInterface + + This library is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#ifndef dscClassicKeypad_h +#define dscClassicKeypad_h + +#include + +#if defined(__AVR__) +const byte dscBufferSize = 10; // Number of keys to buffer if the sketch is busy +#elif defined(ESP8266) || defined (ESP32) +const byte dscBufferSize = 50; +#endif +const byte dscReadSize = 2; // Maximum bytes of a Keybus command + +enum Light {off, on, blink}; // Custom values for keypad lights status + +class dscClassicKeypadInterface { + + public: + dscClassicKeypadInterface(byte setClockPin, byte setReadPin, byte setWritePin); + + // Interface control + void begin(Stream &_stream = Serial); // Initializes the stream output to Serial by default + bool loop(); // Returns true if valid panel data is available + void beep(byte beeps = 0); // Keypad beep, 1-128 beeps + void tone(byte beep = 0, bool tone = false, byte interval = 0); // Keypad tone pattern, 1-7 beeps at 1-15s interval, with optional constant tone + void buzzer(byte seconds = 0); // Keypad buzzer, 1-255 seconds + + // Keypad key + byte key, keyAvailable; + + // Keypad lights + Light lightReady = on, lightArmed, lightMemory, lightBypass, lightTrouble, lightProgram, lightFire, lightBacklight = on; + Light lightZone1, lightZone2, lightZone3, lightZone4, lightZone5, lightZone6, lightZone7, lightZone8; + + // Panel Keybus commands + byte classicCommand[2] = {0x00, 0x80}; + + /* + * moduleData[] stores keypad data in an array: command [0], stop bit by itself [1], followed by the + * remaining data. These can be accessed directly in the sketch to get data that is not already tracked + * in the library. See dscKeybusPrintData.cpp for the currently known DSC commands and data. + */ + static volatile byte moduleData[dscReadSize]; + + // Key data buffer overflow, true if dscBufferSize needs to be increased + static volatile bool bufferOverflow; + + // Timer interrupt function to capture data - declared as public for use by AVR Timer1 + static void dscClockInterrupt(); + + private: + + void zoneLight(Light lightZone, byte zoneBit); + void panelLight(Light lightPanel, byte zoneBit); + + Stream* stream; + byte panelLights = 0x80, previousLights = 0x80; + byte panelBlink, previousBlink; + byte panelZones, previousZones; + byte panelZonesBlink, previousZonesBlink; + bool startupCycle = true; + bool setBeep, setTone, setBuzzer; + byte commandInterval = 26; // Sets the milliseconds between panel commands + bool keyBeep, beepStart; + + #if defined(ESP32) + static hw_timer_t * timer1; + static portMUX_TYPE timer1Mux; + #endif + + static int clockInterval; + static byte dscClockPin, dscReadPin, dscWritePin; + static volatile byte keyData; + static volatile byte keyBufferLength; + static volatile byte keyBuffer[dscBufferSize]; + static volatile bool commandReady, moduleDataDetected; + static volatile bool alarmKeyDetected, alarmKeyResponsePending; + static volatile byte clockCycleCount, clockCycleTotal; + static volatile byte panelCommand[dscReadSize], panelCommandByteCount, panelCommandByteTotal; + static volatile byte isrPanelBitTotal, isrPanelBitCount; + static volatile byte isrModuleData[dscReadSize], isrModuleBitTotal, isrModuleBitCount, isrModuleByteCount; + static volatile unsigned long intervalStart, beepInterval, repeatInterval, keyInterval, alarmKeyTime, alarmKeyInterval; +}; + +#endif // dscKeypad_h diff --git a/src/dscKeybus.h b/src/dscKeybus.h new file mode 100644 index 0000000..da10972 --- /dev/null +++ b/src/dscKeybus.h @@ -0,0 +1,343 @@ +/* + DSC Keybus Interface + + https://github.com/taligentx/dscKeybusInterface + + This library is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#ifndef dscKeybus_h +#define dscKeybus_h + +#include + +#if defined(__AVR__) +const byte dscPartitions = 4; // Maximum number of partitions - requires 19 bytes of memory per partition +const byte dscZones = 4; // Maximum number of zone groups, 8 zones per group - requires 6 bytes of memory per zone group +const byte dscBufferSize = 10; // Number of commands to buffer if the sketch is busy - requires dscReadSize + 2 bytes of memory per command +const byte dscReadSize = 16; // Maximum bytes of a Keybus command +#elif defined(ESP8266) +const byte dscPartitions = 8; +const byte dscZones = 8; +const byte dscBufferSize = 50; +const byte dscReadSize = 16; +#elif defined(ESP32) +const byte dscPartitions = 8; +const byte dscZones = 8; +const DRAM_ATTR byte dscBufferSize = 50; +const DRAM_ATTR byte dscReadSize = 16; +#endif + +// Exit delay target states +#define DSC_EXIT_STAY 1 +#define DSC_EXIT_AWAY 2 +#define DSC_EXIT_NO_ENTRY_DELAY 3 + + +class dscKeybusInterface { + + public: + + // Initializes writes as disabled by default + dscKeybusInterface(byte setClockPin, byte setReadPin, byte setWritePin = 255); + + // Interface control + void begin(Stream &_stream = Serial); // Initializes the stream output to Serial by default + bool loop(); // Returns true if valid panel data is available + void stop(); // Disables the clock hardware interrupt and data timer interrupt + void resetStatus(); // Resets the state of all status components as changed for sketches to get the current status + + // Writes a single key - nonblocking unless a previous write is in progress + void write(const char receivedKey); + + // Writes multiple keys from a char array + // + // By default, this is nonblocking unless there is a previous write in progress - in this case, the sketch must keep the char + // array defined at least until the write is complete. + // + // If the char array is ephemeral, check if the write is complete by checking writeReady or set blockingWrite to true to + // block until the write is complete. + void write(const char * receivedKeys, bool blockingWrite = false); + + // Write control + static byte writePartition; // Set to a partition number for virtual keypad + bool writeReady; // True if the library is ready to write a key + + // Prints output to the stream interface set in begin() + void printPanelBinary(bool printSpaces = true); // Includes spaces between bytes by default + void printPanelCommand(); // Prints the panel command as hex + void printPanelMessage(); // Prints the decoded panel message + void printModuleBinary(bool printSpaces = true); // Includes spaces between bytes by default + void printModuleMessage(); // Prints the decoded keypad or module message + + // These can be configured in the sketch setup() before begin() + bool hideKeypadDigits; // Controls if keypad digits are hidden for publicly posted logs (default: false) + static bool processModuleData; // Controls if keypad and module data is processed and displayed (default: false) + bool displayTrailingBits; // Controls if bits read as the clock is reset are displayed, appears to be spurious data (default: false) + + // Panel time + bool timestampChanged; // True after the panel sends a timestamped message + byte hour, minute, day, month; + int year; + + // Sets panel time, the year can be sent as either 2 or 4 digits, returns true if the panel is ready to set the time + bool setTime(unsigned int year, byte month, byte day, byte hour, byte minute, const char* accessCode, byte timePartition = 1); + + // Status tracking + bool statusChanged; // True after any status change + bool pauseStatus; // Prevent status from showing as changed, set in sketch to control when to update status + bool keybusConnected, keybusChanged; // True if data is detected on the Keybus + byte accessCode[dscPartitions]; + bool accessCodeChanged[dscPartitions]; + bool accessCodePrompt; // True if the panel is requesting an access code + bool decimalInput; // True if the panel is requesting 3 digit input (for 0x6E readout) + bool trouble, troubleChanged; + bool powerTrouble, powerChanged; + bool batteryTrouble, batteryChanged; + bool keypadFireAlarm, keypadAuxAlarm, keypadPanicAlarm; + bool ready[dscPartitions], readyChanged[dscPartitions]; + bool disabled[dscPartitions], disabledChanged[dscPartitions]; + bool armed[dscPartitions], armedAway[dscPartitions], armedStay[dscPartitions]; + bool noEntryDelay[dscPartitions], armedChanged[dscPartitions]; + bool alarm[dscPartitions], alarmChanged[dscPartitions]; + bool exitDelay[dscPartitions], exitDelayChanged[dscPartitions]; + byte exitState[dscPartitions], exitStateChanged[dscPartitions]; + bool entryDelay[dscPartitions], entryDelayChanged[dscPartitions]; + bool fire[dscPartitions], fireChanged[dscPartitions]; + bool openZonesStatusChanged; + byte openZones[dscZones], openZonesChanged[dscZones]; // Zone status is stored in an array using 1 bit per zone, up to 64 zones + bool alarmZonesStatusChanged; + byte alarmZones[dscZones], alarmZonesChanged[dscZones]; // Zone alarm status is stored in an array using 1 bit per zone, up to 64 zones + bool pgmOutputsStatusChanged; + byte pgmOutputs[2], pgmOutputsChanged[2]; + byte panelVersion; + + /* panelData[] and moduleData[] store panel and keypad/module data in an array: command [0], stop bit by itself [1], + * followed by the remaining data. These can be accessed directly in the sketch to get data that is not already + * tracked in the library. See dscKeybusPrintData.cpp for the currently known DSC commands and data. + * + * panelData[] example: + * Byte 0 Byte 2 Byte 3 Byte 4 Byte 5 + * 00000101 0 10000001 00000001 10010001 11000111 [0x05] Partition 1: Ready Backlight - Partition ready | Partition 2: disabled + * ^ Byte 1 (stop bit) + */ + static byte panelData[dscReadSize]; + static volatile byte moduleData[dscReadSize]; + + // status[] and lights[] store the current status message and LED state for each partition. These can be accessed + // directly in the sketch to get data that is not already tracked in the library. See printPanelMessages() and + // printPanelLights() in dscKeybusPrintData.cpp to see how this data translates to the status message and LED status. + byte status[dscPartitions]; + byte lights[dscPartitions]; + + // Process keypad and module data, returns true if data is available + bool handleModule(); + + // True if dscBufferSize needs to be increased + static volatile bool bufferOverflow; + + // Timer interrupt function to capture data - declared as public for use by AVR Timer1 + static void dscDataInterrupt(); + + // Deprecated + bool processRedundantData; // Controls if repeated periodic commands are processed and displayed (default: false) + + private: + + void processPanelStatus(); + void processPanelStatus0(byte partition, byte panelByte); + void processPanelStatus1(byte partition, byte panelByte); + void processPanelStatus2(byte partition, byte panelByte); + void processPanelStatus4(byte partition, byte panelByte); + void processPanelStatus5(byte partition, byte panelByte); + void processPanel_0x16(); + void processPanel_0x27(); + void processPanel_0x2D(); + void processPanel_0x34(); + void processPanel_0x3E(); + void processPanel_0x87(); + void processPanel_0xA5(); + void processPanel_0xE6(); + void processPanel_0xE6_0x09(); + void processPanel_0xE6_0x0B(); + void processPanel_0xE6_0x0D(); + void processPanel_0xE6_0x0F(); + void processPanel_0xE6_0x1A(); + void processPanel_0xEB(); + void processReadyStatus(byte partitionIndex, bool status); + void processAlarmStatus(byte partitionIndex, bool status); + void processExitDelayStatus(byte partitionIndex, bool status); + void processEntryDelayStatus(byte partitionIndex, bool status); + void processNoEntryDelayStatus(byte partitionIndex, bool status); + void processZoneStatus(byte zonesByte, byte panelByte); + void processTime(byte panelByte); + void processAlarmZones(byte panelByte, byte startByte, byte zoneCountOffset, byte writeValue); + void processAlarmZonesStatus(byte zonesByte, byte zoneCount, byte writeValue); + void processArmed(byte partitionIndex, bool armedStatus); + void processPanelAccessCode(byte partitionIndex, byte dscCode, bool accessCodeIncrease = true); + + void printPanelPartitionStatus(byte startPartition, byte startByte, byte endByte); + void printPanelStatus0(byte panelByte); + void printPanelStatus1(byte panelByte); + void printPanelStatus2(byte panelByte); + void printPanelStatus3(byte panelByte); + void printPanelStatus4(byte panelByte); + void printPanelStatus5(byte panelByte); + void printPanelStatus14(byte panelByte); + void printPanelStatus16(byte panelByte); + void printPanelStatus17(byte panelByte); + void printPanelStatus18(byte panelByte); + void printPanelStatus1B(byte panelByte); + + void printPanelMessages(byte panelByte); + void printPanelLights(byte panelByte, bool printMessage = true); + void printPanelTime(byte panelByte); + void printPanelBeeps(byte panelByte); + void printPanelTone(byte panelByte); + void printPanelBuzzer(byte panelByte); + bool printPanelZones(byte inputByte, byte startZone); + void printPanelAccessCode(byte dscCode, bool accessCodeIncrease = true); + void printPanelBitNumbers(byte panelByte, byte startNumber, byte startBit = 0, byte stopBit = 7, bool printNone = true); + void printNumberSpace(byte number); + void printNumberOffset(byte panelByte, int numberOffset); + void printUnknownData(); + void printPartition(); + void printStatusLights(); + void printStatusLightsFlashing(); + void printZoneLights(bool lowerRange = true); + void printPanel_0x05(); + void printPanel_0x0A_0F(); + void printPanel_0x11(); + void printPanel_0x16(); + void printPanel_0x1B(); + void printPanel_0x1C(); + void printPanel_0x22_28_33_39(); + void printPanel_0x27(); + void printPanel_0x2D(); + void printPanel_0x34(); + void printPanel_0x3E(); + void printPanel_0x41(); + void printPanel_0x4C(); + void printPanel_0x57(); + void printPanel_0x58(); + void printPanel_0x5D_63(); + void printPanel_0x64(); + void printPanel_0x69(); + void printPanel_0x6E(); + void printPanel_0x70(); + void printPanel_0x75(); + void printPanel_0x7A(); + void printPanel_0x7F(); + void printPanel_0x82(); + void printPanel_0x87(); + void printPanel_0x8D(); + void printPanel_0x94(); + void printPanel_0x9E(); + void printPanel_0xA5(); + void printPanel_0xAA(); + void printPanel_0xB1(); + void printPanel_0xBB(); + void printPanel_0xC3(); + void printPanel_0xCE(); + void printPanel_0xD5(); + void printPanel_0xE6(); + void printPanel_0xE6_0x01_06_20_21(); + void printPanel_0xE6_0x08_0A_0C_0E(); + void printPanel_0xE6_0x09(); + void printPanel_0xE6_0x0B(); + void printPanel_0xE6_0x0D(); + void printPanel_0xE6_0x0F(); + void printPanel_0xE6_0x17(); + void printPanel_0xE6_0x18(); + void printPanel_0xE6_0x19(); + void printPanel_0xE6_0x1A(); + void printPanel_0xE6_0x1D(); + void printPanel_0xE6_0x1F(); + void printPanel_0xE6_0x2B(); + void printPanel_0xE6_0x2C(); + void printPanel_0xE6_0x41(); + void printPanel_0xEB(); + void printPanel_0xEC(); + + void printModule_0xBB(); + void printModule_0xDD(); + void printModule_0xEE(); + void printModule_Status(); + void printModule_0x11(); + void printModule_0x41(); + void printModule_0x4C(); + void printModule_0x57(); + void printModule_0x58(); + void printModule_0x70(); + void printModule_0x94(); + void printModule_0xD5(); + bool printModule_Keys(); + void printModule_KeyCodes(byte keyByte); + void printModule_Expander(); + bool printModuleSlots(byte startCount, byte startByte, byte endByte, byte startMask, byte endMask, byte bitShift, byte matchValue, bool reverse = false); + void printModuleProgramming(byte panelByte2, byte panelByte3); + + bool validCRC(); + void writeKeys(const char * writeKeysArray); + void setWriteKey(const char receivedKey); + static void dscClockInterrupt(); + static bool redundantPanelData(byte previousCmd[], volatile byte currentCmd[], byte checkedBytes = dscReadSize); + + #if defined(ESP32) + static hw_timer_t * timer1; + static portMUX_TYPE timer1Mux; + #endif + + Stream* stream; + const char* writeKeysArray; + bool writeKeysPending; + bool writeAccessCode[dscPartitions]; + bool queryResponse; + bool previousTrouble; + bool previousKeybus; + bool previousPower; + bool previousDisabled[dscPartitions]; + byte previousAccessCode[dscPartitions]; + byte previousLights[dscPartitions], previousStatus[dscPartitions]; + bool previousReady[dscPartitions]; + bool previousExitDelay[dscPartitions], previousEntryDelay[dscPartitions]; + byte previousExitState[dscPartitions]; + bool previousArmed[dscPartitions], previousArmedStay[dscPartitions], previousNoEntryDelay[dscPartitions]; + bool previousAlarm[dscPartitions]; + bool previousFire[dscPartitions]; + byte previousOpenZones[dscZones], previousAlarmZones[dscZones]; + byte previousPgmOutputs[2]; + bool keybusVersion1; + + static byte dscClockPin; + static byte dscReadPin; + static byte dscWritePin; + static byte writeByte, writeBit; + static bool virtualKeypad; + static char writeKey; + static byte panelBitCount, panelByteCount; + static volatile bool writeKeyPending; + static volatile bool writeAlarm, starKeyCheck, starKeyWait[dscPartitions]; + static volatile bool moduleDataDetected, moduleDataCaptured; + static volatile unsigned long clockHighTime, keybusTime; + static volatile byte panelBufferLength; + static volatile byte panelBuffer[dscBufferSize][dscReadSize]; + static volatile byte panelBufferBitCount[dscBufferSize], panelBufferByteCount[dscBufferSize]; + static volatile byte moduleBitCount, moduleByteCount; + static volatile byte currentCmd, statusCmd, moduleCmd, moduleSubCmd; + static volatile byte isrPanelData[dscReadSize], isrPanelBitTotal, isrPanelBitCount, isrPanelByteCount; + static volatile byte isrModuleData[dscReadSize]; +}; + +#endif // dscKeybus_h diff --git a/src/dscKeybusInterface.cpp b/src/dscKeybusInterface.cpp index 631ea5f..e694851 100644 --- a/src/dscKeybusInterface.cpp +++ b/src/dscKeybusInterface.cpp @@ -17,52 +17,13 @@ along with this program. If not, see . */ -#include "dscKeybusInterface.h" - -byte dscKeybusInterface::dscClockPin; -byte dscKeybusInterface::dscReadPin; -byte dscKeybusInterface::dscWritePin; -char dscKeybusInterface::writeKey; -byte dscKeybusInterface::writePartition; -byte dscKeybusInterface::writeByte; -byte dscKeybusInterface::writeBit; -bool dscKeybusInterface::virtualKeypad; -bool dscKeybusInterface::processModuleData; -byte dscKeybusInterface::panelData[dscReadSize]; -byte dscKeybusInterface::panelByteCount; -byte dscKeybusInterface::panelBitCount; -volatile bool dscKeybusInterface::writeKeyPending; -volatile byte dscKeybusInterface::moduleData[dscReadSize]; -volatile bool dscKeybusInterface::moduleDataCaptured; -volatile byte dscKeybusInterface::moduleByteCount; -volatile byte dscKeybusInterface::moduleBitCount; -volatile bool dscKeybusInterface::writeAlarm; -volatile bool dscKeybusInterface::starKeyCheck; -volatile bool dscKeybusInterface::starKeyWait[dscPartitions]; -volatile bool dscKeybusInterface::bufferOverflow; -volatile byte dscKeybusInterface::panelBufferLength; -volatile byte dscKeybusInterface::panelBuffer[dscBufferSize][dscReadSize]; -volatile byte dscKeybusInterface::panelBufferBitCount[dscBufferSize]; -volatile byte dscKeybusInterface::panelBufferByteCount[dscBufferSize]; -volatile byte dscKeybusInterface::isrPanelData[dscReadSize]; -volatile byte dscKeybusInterface::isrPanelByteCount; -volatile byte dscKeybusInterface::isrPanelBitCount; -volatile byte dscKeybusInterface::isrPanelBitTotal; -volatile byte dscKeybusInterface::isrModuleData[dscReadSize]; -volatile byte dscKeybusInterface::isrModuleByteCount; -volatile byte dscKeybusInterface::isrModuleBitCount; -volatile byte dscKeybusInterface::isrModuleBitTotal; -volatile byte dscKeybusInterface::currentCmd; -volatile byte dscKeybusInterface::statusCmd; -volatile byte dscKeybusInterface::moduleCmd; -volatile byte dscKeybusInterface::moduleSubCmd; -volatile unsigned long dscKeybusInterface::clockHighTime; -volatile unsigned long dscKeybusInterface::keybusTime; +#include "dscKeybus.h" + #if defined(ESP32) -hw_timer_t *timer0 = NULL; -portMUX_TYPE timer0Mux = portMUX_INITIALIZER_UNLOCKED; -#endif +portMUX_TYPE dscKeybusInterface::timer1Mux = portMUX_INITIALIZER_UNLOCKED; +hw_timer_t * dscKeybusInterface::timer1 = NULL; +#endif // ESP32 dscKeybusInterface::dscKeybusInterface(byte setClockPin, byte setReadPin, byte setWritePin) { @@ -99,13 +60,13 @@ void dscKeybusInterface::begin(Stream &_stream) { timer1_attachInterrupt(dscDataInterrupt); timer1_enable(TIM_DIV16, TIM_EDGE, TIM_SINGLE); - // esp32 timer0 calls dscDataInterrupt() from dscClockInterrupt() + // esp32 timer1 calls dscDataInterrupt() from dscClockInterrupt() #elif defined(ESP32) - timer0 = timerBegin(0, 80, true); - timerStop(timer0); - timerAttachInterrupt(timer0, &dscDataInterrupt, true); - timerAlarmWrite(timer0, 250, true); - timerAlarmEnable(timer0); + timer1 = timerBegin(1, 80, true); + timerStop(timer1); + timerAttachInterrupt(timer1, &dscDataInterrupt, true); + timerAlarmWrite(timer1, 250, true); + timerAlarmEnable(timer1); #endif // Generates an interrupt when the Keybus clock rises or falls - requires a hardware interrupt pin on Arduino/AVR @@ -124,10 +85,10 @@ void dscKeybusInterface::stop() { timer1_disable(); timer1_detachInterrupt(); - // Disables esp32 timer0 + // Disables esp32 timer1 #elif defined(ESP32) - timerAlarmDisable(timer0); - timerEnd(timer0); + timerAlarmDisable(timer1); + timerEnd(timer1); #endif // Disables the Keybus clock pin interrupt @@ -140,11 +101,8 @@ void dscKeybusInterface::stop() { isrPanelBitCount = 0; isrPanelByteCount = 0; - // Resets the keypad and module capture data and counters + // Resets the keypad and module capture data for (byte i = 0; i < dscReadSize; i++) isrModuleData[i] = 0; - isrModuleBitTotal = 0; - isrModuleBitCount = 0; - isrModuleByteCount = 0; } @@ -156,7 +114,7 @@ bool dscKeybusInterface::loop() { // Checks if Keybus data is detected and sets a status flag if data is not detected for 3s #if defined(ESP32) - portENTER_CRITICAL(&timer0Mux); + portENTER_CRITICAL(&timer1Mux); #else noInterrupts(); #endif @@ -165,7 +123,7 @@ bool dscKeybusInterface::loop() { else keybusConnected = true; #if defined(ESP32) - portEXIT_CRITICAL(&timer0Mux); + portEXIT_CRITICAL(&timer1Mux); #else interrupts(); #endif @@ -193,7 +151,7 @@ bool dscKeybusInterface::loop() { // Resets counters when the buffer is cleared #if defined(ESP32) - portENTER_CRITICAL(&timer0Mux); + portENTER_CRITICAL(&timer1Mux); #else noInterrupts(); #endif @@ -204,7 +162,7 @@ bool dscKeybusInterface::loop() { } #if defined(ESP32) - portEXIT_CRITICAL(&timer0Mux); + portEXIT_CRITICAL(&timer1Mux); #else interrupts(); #endif @@ -251,171 +209,17 @@ bool dscKeybusInterface::loop() { // Processes valid panel data switch (panelData[0]) { - case 0x05: - case 0x1B: processPanelStatus(); break; - case 0x16: processPanel_0x16(); break; - case 0x27: processPanel_0x27(); break; - case 0x2D: processPanel_0x2D(); break; - case 0x34: processPanel_0x34(); break; - case 0x3E: processPanel_0x3E(); break; - case 0x87: processPanel_0x87(); break; - case 0xA5: processPanel_0xA5(); break; - case 0xE6: if (dscPartitions > 2) processPanel_0xE6(); break; - case 0xEB: if (dscPartitions > 2) processPanel_0xEB(); break; - } - - return true; -} - - -// Deprecated, replaced by loop() -bool dscKeybusInterface::handlePanel() { - - // Checks if Keybus data is detected and sets a status flag if data is not detected for 3s - #if defined(ESP32) - portENTER_CRITICAL(&timer0Mux); - #else - noInterrupts(); - #endif - - if (millis() - keybusTime > 3000) keybusConnected = false; // keybusTime is set in dscDataInterrupt() when the clock resets - else keybusConnected = true; - - #if defined(ESP32) - portEXIT_CRITICAL(&timer0Mux); - #else - interrupts(); - #endif - - if (previousKeybus != keybusConnected) { - previousKeybus = keybusConnected; - keybusChanged = true; - if (!pauseStatus) statusChanged = true; - if (!keybusConnected) return true; - } - - // Writes keys when multiple keys are sent as a char array - if (writeKeysPending) writeKeys(writeKeysArray); - - // Skips processing if the panel data buffer is empty - if (panelBufferLength == 0) return false; - - // Copies data from the buffer to panelData[] - static byte panelBufferIndex = 1; - byte dataIndex = panelBufferIndex - 1; - for (byte i = 0; i < dscReadSize; i++) panelData[i] = panelBuffer[dataIndex][i]; - panelBitCount = panelBufferBitCount[dataIndex]; - panelByteCount = panelBufferByteCount[dataIndex]; - panelBufferIndex++; - - // Resets counters when the buffer is cleared - #if defined(ESP32) - portENTER_CRITICAL(&timer0Mux); - #else - noInterrupts(); - #endif - - if (panelBufferIndex > panelBufferLength) { - panelBufferIndex = 1; - panelBufferLength = 0; - } - - #if defined(ESP32) - portEXIT_CRITICAL(&timer0Mux); - #else - interrupts(); - #endif - - // Waits at startup for the 0x05 status command or a command with valid CRC data to eliminate spurious data. - static bool firstClockCycle = true; - if (firstClockCycle) { - if ((validCRC() || panelData[0] == 0x05) && panelData[0] != 0) firstClockCycle = false; - else return false; - } - - // Skips redundant data sent constantly while in installer programming - static byte previousCmd0A[dscReadSize]; - static byte previousCmdE6_20[dscReadSize]; - switch (panelData[0]) { - case 0x0A: // Status in programming - if (redundantPanelData(previousCmd0A, panelData)) return false; - break; - - case 0xE6: - if (panelData[2] == 0x20 && redundantPanelData(previousCmdE6_20, panelData)) return false; // Status in programming, zone lights 33-64 - break; - } - if (dscPartitions > 4) { - static byte previousCmdE6_03[dscReadSize]; - if (panelData[0] == 0xE6 && panelData[2] == 0x03 && redundantPanelData(previousCmdE6_03, panelData, 8)) return false; // Status in alarm/programming, partitions 5-8 - } - - // Skips redundant data from periodic commands sent at regular intervals, by default this data is processed - if (!processRedundantData) { - static byte previousCmd11[dscReadSize]; - static byte previousCmd16[dscReadSize]; - static byte previousCmd27[dscReadSize]; - static byte previousCmd2D[dscReadSize]; - static byte previousCmd34[dscReadSize]; - static byte previousCmd3E[dscReadSize]; - static byte previousCmd5D[dscReadSize]; - static byte previousCmd63[dscReadSize]; - static byte previousCmdB1[dscReadSize]; - static byte previousCmdC3[dscReadSize]; - switch (panelData[0]) { - case 0x11: // Keypad slot query - if (redundantPanelData(previousCmd11, panelData)) return false; - break; - - case 0x16: // Zone wiring - if (redundantPanelData(previousCmd16, panelData)) return false; - break; - - case 0x27: // Status with zone 1-8 info - if (redundantPanelData(previousCmd27, panelData)) return false; - break; - - case 0x2D: // Status with zone 9-16 info - if (redundantPanelData(previousCmd2D, panelData)) return false; - break; - - case 0x34: // Status with zone 17-24 info - if (redundantPanelData(previousCmd34, panelData)) return false; - break; - - case 0x3E: // Status with zone 25-32 info - if (redundantPanelData(previousCmd3E, panelData)) return false; - break; - - case 0x5D: // Flash panel lights: status and zones 1-32 - if (redundantPanelData(previousCmd5D, panelData)) return false; - break; - - case 0x63: // Flash panel lights: status and zones 33-64 - if (redundantPanelData(previousCmd63, panelData)) return false; - break; - - case 0xB1: // Enabled zones 1-32 - if (redundantPanelData(previousCmdB1, panelData)) return false; - break; - - case 0xC3: // Unknown command - if (redundantPanelData(previousCmdC3, panelData)) return false; - break; - } - } - - // Processes valid panel data - switch (panelData[0]) { - case 0x05: - case 0x1B: processPanelStatus(); break; - case 0x27: processPanel_0x27(); break; - case 0x2D: processPanel_0x2D(); break; - case 0x34: processPanel_0x34(); break; - case 0x3E: processPanel_0x3E(); break; - case 0xA5: processPanel_0xA5(); break; - case 0xE6: if (dscPartitions > 2) processPanel_0xE6(); break; - case 0xEB: if (dscPartitions > 2) processPanel_0xEB(); break; + case 0x05: // Panel status: partitions 1-4 + case 0x1B: processPanelStatus(); break; // Panel status: partitions 5-8 + case 0x16: processPanel_0x16(); break; // Panel configuration + case 0x27: processPanel_0x27(); break; // Panel status and zones 1-8 status + case 0x2D: processPanel_0x2D(); break; // Panel status and zones 9-16 status + case 0x34: processPanel_0x34(); break; // Panel status and zones 17-24 status + case 0x3E: processPanel_0x3E(); break; // Panel status and zones 25-32 status + case 0x87: processPanel_0x87(); break; // PGM outputs + case 0xA5: processPanel_0xA5(); break; // Date, time, system status messages - partitions 1-2 + case 0xE6: if (dscPartitions > 2) processPanel_0xE6(); break; // Extended status command split into multiple subcommands to handle up to 8 partitions/64 zones + case 0xEB: if (dscPartitions > 2) processPanel_0xEB(); break; // Date, time, system status messages - partitions 1-8 } return true; @@ -545,34 +349,23 @@ void dscKeybusInterface::setWriteKey(const char receivedKey) { case '9': writeKey = 0x27; break; case '*': writeKey = 0x28; if (status[writePartition - 1] < 0x9E) starKeyCheck = true; break; case '#': writeKey = 0x2D; break; - case 'F': - case 'f': writeKey = 0x77; writeAlarm = true; break; // Keypad fire alarm - case 'b': - case 'B': writeKey = 0x82; break; // Enter event buffer - case '>': writeKey = 0x87; break; // Event buffer right arrow - case '<': writeKey = 0x88; break; // Event buffer left arrow - case 'l': - case 'L': writeKey = 0xA5; break; // LCD keypad data request - case 's': - case 'S': writeKey = 0xAF; writeArm[writePartition - 1] = true; break; // Arm stay - case 'w': - case 'W': writeKey = 0xB1; writeArm[writePartition - 1] = true; break; // Arm away - case 'n': - case 'N': writeKey = 0xB6; writeArm[writePartition - 1] = true; break; // Arm with no entry delay (night arm) - case 'A': - case 'a': writeKey = 0xBB; writeAlarm = true; break; // Keypad auxiliary alarm - case 'c': - case 'C': writeKey = 0xBB; break; // Door chime - case 'r': - case 'R': writeKey = 0xDA; break; // Reset - case 'P': - case 'p': writeKey = 0xDD; writeAlarm = true; break; // Keypad panic alarm - case 'x': - case 'X': writeKey = 0xE1; break; // Exit - case '[': writeKey = 0xD5; break; // Command output 1 - case ']': writeKey = 0xDA; break; // Command output 2 - case '{': writeKey = 0x70; break; // Command output 3 - case '}': writeKey = 0xEC; break; // Command output 4 + case 'f': case 'F': writeKey = 0xBB; writeAlarm = true; break; // Keypad fire alarm + case 'b': case 'B': writeKey = 0x82; break; // Enter event buffer + case '>': writeKey = 0x87; break; // Event buffer right arrow + case '<': writeKey = 0x88; break; // Event buffer left arrow + case 'l': case 'L': writeKey = 0xA5; break; // LCD keypad data request + case 's': case 'S': writeKey = 0xAF; writeAccessCode[writePartition - 1] = true; break; // Arm stay + case 'w': case 'W': writeKey = 0xB1; writeAccessCode[writePartition - 1] = true; break; // Arm away + case 'n': case 'N': writeKey = 0xB6; writeAccessCode[writePartition - 1] = true; break; // Arm with no entry delay (night arm) + case 'a': case 'A': writeKey = 0xDD; writeAlarm = true; break; // Keypad auxiliary alarm + case 'c': case 'C': writeKey = 0xBB; break; // Door chime + case 'r': case 'R': writeKey = 0xDA; break; // Reset + case 'p': case 'P': writeKey = 0xEE; writeAlarm = true; break; // Keypad panic alarm + case 'x': case 'X': writeKey = 0xE1; break; // Exit + case '[': writeKey = 0xD5; writeAccessCode[writePartition - 1] = true; break; // Command output 1 + case ']': writeKey = 0xDA; writeAccessCode[writePartition - 1] = true; break; // Command output 2 + case '{': writeKey = 0x70; writeAccessCode[writePartition - 1] = true; break; // Command output 3 + case '}': writeKey = 0xEC; writeAccessCode[writePartition - 1] = true; break; // Command output 4 default: { validKey = false; break; @@ -679,23 +472,83 @@ void IRAM_ATTR dscKeybusInterface::dscClockInterrupt() { #elif defined(ESP8266) timer1_write(1250); - // esp32 timer0 calls dscDataInterrupt() in 250us + // esp32 timer1 calls dscDataInterrupt() in 250us #elif defined(ESP32) - timerStart(timer0); - portENTER_CRITICAL(&timer0Mux); + timerStart(timer1); + portENTER_CRITICAL(&timer1Mux); #endif static unsigned long previousClockHighTime; + static bool skipData = false; + + // Panel sends data while the clock is high if (digitalRead(dscClockPin) == HIGH) { if (virtualKeypad) digitalWrite(dscWritePin, LOW); // Restores the data line after a virtual keypad write previousClockHighTime = micros(); } + // Keypads and modules send data while the clock is low else { clockHighTime = micros() - previousClockHighTime; // Tracks the clock high time to find the reset between commands + // Saves data and resets counters after the clock cycle is complete (high for at least 1ms) + if (clockHighTime > 1000) { + keybusTime = millis(); + + // Skips incomplete and redundant data from status commands - these are sent constantly on the keybus at a high + // rate, so they are always skipped. Checking is required in the ISR to prevent flooding the buffer. + if (isrPanelBitTotal < 8) skipData = true; + else switch (isrPanelData[0]) { + static byte previousCmd05[dscReadSize]; + static byte previousCmd1B[dscReadSize]; + + case 0x05: // Status: partitions 1-4 + if (redundantPanelData(previousCmd05, isrPanelData, isrPanelByteCount)) skipData = true; + break; + + case 0x1B: // Status: partitions 5-8 + if (redundantPanelData(previousCmd1B, isrPanelData, isrPanelByteCount)) skipData = true; + break; + } + + // Stores new panel data in the panel buffer + currentCmd = isrPanelData[0]; + if (panelBufferLength == dscBufferSize) bufferOverflow = true; + else if (!skipData && panelBufferLength < dscBufferSize) { + for (byte i = 0; i < dscReadSize; i++) panelBuffer[panelBufferLength][i] = isrPanelData[i]; + panelBufferBitCount[panelBufferLength] = isrPanelBitTotal; + panelBufferByteCount[panelBufferLength] = isrPanelByteCount; + panelBufferLength++; + } + + if (processModuleData) { + + // Stores new keypad and module data - this data is not buffered + if (moduleDataDetected) { + moduleCmd = isrPanelData[0]; + moduleSubCmd = isrPanelData[2]; + moduleDataDetected = false; + moduleDataCaptured = true; // Sets a flag for handleModule() + for (byte i = 0; i < dscReadSize; i++) moduleData[i] = isrModuleData[i]; + moduleBitCount = isrPanelBitTotal; + moduleByteCount = isrPanelByteCount; + } + + // Resets the keypad and module capture data + for (byte i = 0; i < dscReadSize; i++) isrModuleData[i] = 0; + } + + // Resets the panel capture data and counters + for (byte i = 0; i < dscReadSize; i++) isrPanelData[i] = 0; + isrPanelBitTotal = 0; + isrPanelBitCount = 0; + isrPanelByteCount = 0; + skipData = false; + } + // Virtual keypad if (virtualKeypad) { + static bool writeStart = false; static bool writeRepeat = false; static bool writeCmd = false; @@ -708,7 +561,7 @@ void IRAM_ATTR dscKeybusInterface::dscClockInterrupt() { if ((writeAlarm && writeKeyPending) || writeRepeat) { // Writes the first bit by shifting the alarm key data right 7 bits and checking bit 0 - if (isrPanelBitTotal == 1) { + if (isrPanelBitTotal == 0) { if (!((writeKey >> 7) & 0x01)) { digitalWrite(dscWritePin, HIGH); } @@ -716,11 +569,11 @@ void IRAM_ATTR dscKeybusInterface::dscClockInterrupt() { } // Writes the remaining alarm key data - else if (writeStart && isrPanelBitTotal > 1 && isrPanelBitTotal <= 8) { - if (!((writeKey >> (8 - isrPanelBitTotal)) & 0x01)) digitalWrite(dscWritePin, HIGH); + else if (writeStart && isrPanelBitTotal <= 7) { + if (!((writeKey >> (7 - isrPanelBitTotal)) & 0x01)) digitalWrite(dscWritePin, HIGH); // Resets counters when the write is complete - if (isrPanelBitTotal == 8) { + if (isrPanelBitTotal == 7) { writeKeyPending = false; writeStart = false; writeAlarm = false; @@ -756,41 +609,27 @@ void IRAM_ATTR dscKeybusInterface::dscClockInterrupt() { } } #if defined(ESP32) - portEXIT_CRITICAL(&timer0Mux); + portEXIT_CRITICAL(&timer1Mux); #endif } -// Interrupt function called after 250us by dscClockInterrupt() using AVR Timer1, disables the timer and calls -// dscDataInterrupt() to read the data line -#if defined(__AVR__) -ISR(TIMER1_OVF_vect) { - TCCR1B = 0; // Disables Timer1 - dscKeybusInterface::dscDataInterrupt(); -} -#endif - - -// Interrupt function called by AVR Timer1, esp8266 timer1, and esp32 timer0 after 250us to read the data line +// Interrupt function called by AVR Timer1, esp8266 timer1, and esp32 timer1 after 250us to read the data line #if defined(__AVR__) void dscKeybusInterface::dscDataInterrupt() { #elif defined(ESP8266) void ICACHE_RAM_ATTR dscKeybusInterface::dscDataInterrupt() { #elif defined(ESP32) void IRAM_ATTR dscKeybusInterface::dscDataInterrupt() { - timerStop(timer0); - portENTER_CRITICAL(&timer0Mux); + timerStop(timer1); + portENTER_CRITICAL(&timer1Mux); #endif - static bool skipData = false; - // Panel sends data while the clock is high if (digitalRead(dscClockPin) == HIGH) { - // Stops processing Keybus data at the dscReadSize limit - if (isrPanelByteCount >= dscReadSize) skipData = true; - - else { + // Reads panel data and sets data counters + if (isrPanelByteCount < dscReadSize) { // Limits Keybus data bytes to dscReadSize if (isrPanelBitCount < 8) { // Data is captured in each byte by shifting left by 1 bit and writing to bit 0 isrPanelData[isrPanelByteCount] <<= 1; @@ -799,17 +638,18 @@ void IRAM_ATTR dscKeybusInterface::dscDataInterrupt() { } } - if (isrPanelBitTotal == 8) { - - // Tests for a status command, used in dscClockInterrupt() to ensure keys are only written during a status command + // Tests for a status command, used in dscClockInterrupt() to ensure keys are only written during a status command + if (isrPanelBitTotal == 7) { switch (isrPanelData[0]) { case 0x05: case 0x0A: statusCmd = 0x05; break; case 0x1B: statusCmd = 0x1B; break; default: statusCmd = 0; break; } + } - // Stores the stop bit by itself in byte 1 - this aligns the Keybus bytes with panelData[] bytes + // Stores the stop bit by itself in byte 1 - this aligns the Keybus bytes with panelData[] bytes + if (isrPanelBitTotal == 8) { isrPanelBitCount = 0; isrPanelByteCount++; } @@ -831,99 +671,28 @@ void IRAM_ATTR dscKeybusInterface::dscDataInterrupt() { // Keypads and modules send data while the clock is low else { - static bool moduleDataDetected = false; // Keypad and module data is not buffered and skipped if the panel data buffer is filling - if (processModuleData && isrModuleByteCount < dscReadSize && panelBufferLength <= 1) { + if (processModuleData && isrPanelByteCount < dscReadSize && panelBufferLength <= 1) { // Data is captured in each byte by shifting left by 1 bit and writing to bit 0 - if (isrModuleBitCount < 8) { - isrModuleData[isrModuleByteCount] <<= 1; + if (isrPanelBitCount < 8) { + isrModuleData[isrPanelByteCount] <<= 1; if (digitalRead(dscReadPin) == HIGH) { - isrModuleData[isrModuleByteCount] |= 1; + isrModuleData[isrPanelByteCount] |= 1; + } + else { + moduleDataDetected = true; // Keypads and modules send data by pulling the data line low } - else moduleDataDetected = true; // Keypads and modules send data by pulling the data line low } // Stores the stop bit by itself in byte 1 - this aligns the Keybus bytes with moduleData[] bytes - if (isrModuleBitTotal == 7) { + if (isrPanelBitTotal == 8) { isrModuleData[1] = 1; // Sets the stop bit manually to 1 in byte 1 - isrModuleBitCount = 0; - isrModuleByteCount += 2; } - - // Increments the bit counter if the byte is incomplete - else if (isrModuleBitCount < 7) { - isrModuleBitCount++; - } - - // Byte is complete, set the counters for the next byte - else { - isrModuleBitCount = 0; - isrModuleByteCount++; - } - - isrModuleBitTotal++; - } - - // Saves data and resets counters after the clock cycle is complete (high for at least 1ms) - if (clockHighTime > 1000) { - keybusTime = millis(); - - // Skips incomplete and redundant data from status commands - these are sent constantly on the keybus at a high - // rate, so they are always skipped. Checking is required in the ISR to prevent flooding the buffer. - if (isrPanelBitTotal < 8) skipData = true; - else switch (isrPanelData[0]) { - static byte previousCmd05[dscReadSize]; - static byte previousCmd1B[dscReadSize]; - case 0x05: // Status: partitions 1-4 - if (redundantPanelData(previousCmd05, isrPanelData, isrPanelByteCount)) skipData = true; - break; - - case 0x1B: // Status: partitions 5-8 - if (redundantPanelData(previousCmd1B, isrPanelData, isrPanelByteCount)) skipData = true; - break; - } - - // Stores new panel data in the panel buffer - currentCmd = isrPanelData[0]; - if (panelBufferLength == dscBufferSize) bufferOverflow = true; - else if (!skipData && panelBufferLength < dscBufferSize) { - for (byte i = 0; i < dscReadSize; i++) panelBuffer[panelBufferLength][i] = isrPanelData[i]; - panelBufferBitCount[panelBufferLength] = isrPanelBitTotal; - panelBufferByteCount[panelBufferLength] = isrPanelByteCount; - panelBufferLength++; - } - - if (processModuleData) { - - // Stores new keypad and module data - this data is not buffered - if (moduleDataDetected) { - moduleCmd = isrPanelData[0]; - moduleSubCmd = isrPanelData[2]; - moduleDataDetected = false; - moduleDataCaptured = true; // Sets a flag for handleModule() - for (byte i = 0; i < dscReadSize; i++) moduleData[i] = isrModuleData[i]; - moduleBitCount = isrModuleBitTotal; - moduleByteCount = isrModuleByteCount; - } - - // Resets the keypad and module capture data and counters - for (byte i = 0; i < dscReadSize; i++) isrModuleData[i] = 0; - isrModuleBitTotal = 0; - isrModuleBitCount = 0; - isrModuleByteCount = 0; - } - - // Resets the panel capture data and counters - for (byte i = 0; i < dscReadSize; i++) isrPanelData[i] = 0; - isrPanelBitTotal = 0; - isrPanelBitCount = 0; - isrPanelByteCount = 0; - skipData = false; } } #if defined(ESP32) - portEXIT_CRITICAL(&timer0Mux); + portEXIT_CRITICAL(&timer1Mux); #endif } diff --git a/src/dscKeybusInterface.h b/src/dscKeybusInterface.h index a246319..0715e2c 100644 --- a/src/dscKeybusInterface.h +++ b/src/dscKeybusInterface.h @@ -20,318 +20,193 @@ #ifndef dscKeybusInterface_h #define dscKeybusInterface_h -#include +// DSC Classic Series +#if defined dscClassicSeries +#include "dscClassic.h" + +byte dscClassicInterface::dscClockPin; +byte dscClassicInterface::dscReadPin; +byte dscClassicInterface::dscPC16Pin; +byte dscClassicInterface::dscWritePin; +char dscClassicInterface::writeKey; +byte dscClassicInterface::writePartition; +byte dscClassicInterface::writeByte; +byte dscClassicInterface::writeBit; +bool dscClassicInterface::virtualKeypad; +bool dscClassicInterface::processModuleData; +byte dscClassicInterface::panelData[dscReadSize]; +byte dscClassicInterface::pc16Data[dscReadSize]; +byte dscClassicInterface::panelByteCount; +byte dscClassicInterface::panelBitCount; +volatile bool dscClassicInterface::writeKeyPending; +volatile bool dscClassicInterface::writeKeyWait; +volatile byte dscClassicInterface::moduleData[dscReadSize]; +volatile bool dscClassicInterface::moduleDataCaptured; +volatile byte dscClassicInterface::moduleByteCount; +volatile byte dscClassicInterface::moduleBitCount; +volatile bool dscClassicInterface::writeAlarm; +volatile bool dscClassicInterface::starKeyDetected; +volatile bool dscClassicInterface::starKeyCheck; +volatile bool dscClassicInterface::starKeyWait; +volatile bool dscClassicInterface::bufferOverflow; +volatile byte dscClassicInterface::panelBufferLength; +volatile byte dscClassicInterface::panelBuffer[dscBufferSize][dscReadSize]; +volatile byte dscClassicInterface::pc16Buffer[dscBufferSize][dscReadSize]; +volatile byte dscClassicInterface::panelBufferBitCount[dscBufferSize]; +volatile byte dscClassicInterface::panelBufferByteCount[dscBufferSize]; +volatile byte dscClassicInterface::isrPanelData[dscReadSize]; +volatile byte dscClassicInterface::isrPC16Data[dscReadSize]; +volatile byte dscClassicInterface::isrPanelByteCount; +volatile byte dscClassicInterface::isrPanelBitCount; +volatile byte dscClassicInterface::isrPanelBitTotal; +volatile byte dscClassicInterface::isrModuleData[dscReadSize]; +volatile byte dscClassicInterface::isrModuleByteCount; +volatile byte dscClassicInterface::isrModuleBitCount; +volatile byte dscClassicInterface::isrModuleBitTotal; +volatile byte dscClassicInterface::moduleCmd; +volatile bool dscClassicInterface::readyLight; +volatile bool dscClassicInterface::lightBlink; +volatile unsigned long dscClassicInterface::clockHighTime; +volatile unsigned long dscClassicInterface::keybusTime; +volatile unsigned long dscClassicInterface::writeCompleteTime; + +// Interrupt function called after 250us by dscClockInterrupt() using AVR Timer1, disables the timer and calls +// dscDataInterrupt() to read the data line #if defined(__AVR__) -const byte dscPartitions = 4; // Maximum number of partitions - requires 19 bytes of memory per partition -const byte dscZones = 4; // Maximum number of zone groups, 8 zones per group - requires 6 bytes of memory per zone group -const byte dscBufferSize = 10; // Number of commands to buffer if the sketch is busy - requires dscReadSize + 2 bytes of memory per command -const byte dscReadSize = 16; // Maximum bytes of a Keybus command -#elif defined(ESP8266) -const byte dscPartitions = 8; -const byte dscZones = 8; -const byte dscBufferSize = 50; -const byte dscReadSize = 16; -#elif defined(ESP32) -const byte dscPartitions = 8; -const byte dscZones = 8; -const DRAM_ATTR byte dscBufferSize = 50; -const DRAM_ATTR byte dscReadSize = 16; -#endif +ISR(TIMER1_OVF_vect) { + TCCR1B = 0; // Disables Timer1 + dscClassicInterface::dscDataInterrupt(); +} +#endif // __AVR__ + + +// DSC Keypad Interface +#elif defined dscKeypad +#include "dscKeypad.h" + +byte dscKeypadInterface::dscClockPin; +byte dscKeypadInterface::dscReadPin; +byte dscKeypadInterface::dscWritePin; +int dscKeypadInterface::clockInterval; +volatile byte dscKeypadInterface::keyData; +volatile byte dscKeypadInterface::keyBufferLength; +volatile byte dscKeypadInterface::keyBuffer[dscBufferSize]; +volatile bool dscKeypadInterface::bufferOverflow; +volatile bool dscKeypadInterface::commandReady; +volatile bool dscKeypadInterface::moduleDataDetected; +volatile bool dscKeypadInterface::alarmKeyDetected; +volatile bool dscKeypadInterface::alarmKeyResponsePending; +volatile byte dscKeypadInterface::clockCycleCount; +volatile byte dscKeypadInterface::clockCycleTotal; +volatile byte dscKeypadInterface::panelCommand[dscReadSize]; +volatile byte dscKeypadInterface::isrPanelBitTotal; +volatile byte dscKeypadInterface::isrPanelBitCount; +volatile byte dscKeypadInterface::panelCommandByteCount; +volatile byte dscKeypadInterface::isrModuleData[dscReadSize]; +volatile byte dscKeypadInterface::isrModuleBitTotal; +volatile byte dscKeypadInterface::isrModuleBitCount; +volatile byte dscKeypadInterface::isrModuleByteCount; +volatile byte dscKeypadInterface::panelCommandByteTotal; +volatile byte dscKeypadInterface::moduleData[dscReadSize]; -// Exit delay target states -#define DSC_EXIT_STAY 1 -#define DSC_EXIT_AWAY 2 -#define DSC_EXIT_NO_ENTRY_DELAY 3 - - -class dscKeybusInterface { - - public: - - // Initializes writes as disabled by default - dscKeybusInterface(byte setClockPin, byte setReadPin, byte setWritePin = 255); - - // Interface control - void begin(Stream &_stream = Serial); // Initializes the stream output to Serial by default - bool loop(); // Returns true if valid panel data is available - void stop(); // Disables the clock hardware interrupt and data timer interrupt - void resetStatus(); // Resets the state of all status components as changed for sketches to get the current status - - // Writes a single key - nonblocking unless a previous write is in progress - void write(const char receivedKey); - - // Writes multiple keys from a char array - // - // By default, this is nonblocking unless there is a previous write in progress - in this case, the sketch must keep the char - // array defined at least until the write is complete. - // - // If the char array is ephemeral, check if the write is complete by checking writeReady or set blockingWrite to true to - // block until the write is complete. - void write(const char * receivedKeys, bool blockingWrite = false); - - // Write control - static byte writePartition; // Set to a partition number for virtual keypad - bool writeReady; // True if the library is ready to write a key - - // Prints output to the stream interface set in begin() - void printPanelBinary(bool printSpaces = true); // Includes spaces between bytes by default - void printPanelCommand(); // Prints the panel command as hex - void printPanelMessage(); // Prints the decoded panel message - void printModuleBinary(bool printSpaces = true); // Includes spaces between bytes by default - void printModuleMessage(); // Prints the decoded keypad or module message - - - // These can be configured in the sketch setup() before begin() - bool hideKeypadDigits; // Controls if keypad digits are hidden for publicly posted logs (default: false) - static bool processModuleData; // Controls if keypad and module data is processed and displayed (default: false) - bool displayTrailingBits; // Controls if bits read as the clock is reset are displayed, appears to be spurious data (default: false) - - // Panel time - bool timestampChanged; // True after the panel sends a timestamped message - byte hour, minute, day, month; - int year; - - // Sets panel time, the year can be sent as either 2 or 4 digits, returns true if the panel is ready to set the time - bool setTime(unsigned int year, byte month, byte day, byte hour, byte minute, const char* accessCode, byte timePartition = 1); - - // Status tracking - bool statusChanged; // True after any status change - bool pauseStatus; // Prevent status from showing as changed, set in sketch to control when to update status - bool keybusConnected, keybusChanged; // True if data is detected on the Keybus - byte accessCode[dscPartitions]; - bool accessCodeChanged[dscPartitions]; - bool accessCodePrompt; // True if the panel is requesting an access code - bool decimalInput; // True if the panel is requesting 3 digit input (for 0x6E readout) - bool trouble, troubleChanged; - bool powerTrouble, powerChanged; - bool batteryTrouble, batteryChanged; - bool keypadFireAlarm, keypadAuxAlarm, keypadPanicAlarm; - bool ready[dscPartitions], readyChanged[dscPartitions]; - bool disabled[dscPartitions], disabledChanged[dscPartitions]; - bool armed[dscPartitions], armedAway[dscPartitions], armedStay[dscPartitions]; - bool noEntryDelay[dscPartitions], armedChanged[dscPartitions]; - bool alarm[dscPartitions], alarmChanged[dscPartitions]; - bool exitDelay[dscPartitions], exitDelayChanged[dscPartitions]; - byte exitState[dscPartitions], exitStateChanged[dscPartitions]; - bool entryDelay[dscPartitions], entryDelayChanged[dscPartitions]; - bool fire[dscPartitions], fireChanged[dscPartitions]; - bool openZonesStatusChanged; - byte openZones[dscZones], openZonesChanged[dscZones]; // Zone status is stored in an array using 1 bit per zone, up to 64 zones - bool alarmZonesStatusChanged; - byte alarmZones[dscZones], alarmZonesChanged[dscZones]; // Zone alarm status is stored in an array using 1 bit per zone, up to 64 zones - bool pgmOutputsStatusChanged; - byte pgmOutputs[2], pgmOutputsChanged[2]; - byte panelVersion; - - /* panelData[] and moduleData[] store panel and keypad/module data in an array: command [0], stop bit by itself [1], - * followed by the remaining data. These can be accessed directly in the sketch to get data that is not already - * tracked in the library. See dscKeybusPrintData.cpp for the currently known DSC commands and data. - * - * panelData[] example: - * Byte 0 Byte 2 Byte 3 Byte 4 Byte 5 - * 00000101 0 10000001 00000001 10010001 11000111 [0x05] Partition 1: Ready Backlight - Partition ready | Partition 2: disabled - * ^ Byte 1 (stop bit) - */ - static byte panelData[dscReadSize]; - static volatile byte moduleData[dscReadSize]; - - // status[] and lights[] store the current status message and LED state for each partition. These can be accessed - // directly in the sketch to get data that is not already tracked in the library. See printPanelMessages() and - // printPanelLights() in dscKeybusPrintData.cpp to see how this data translates to the status message and LED status. - byte status[dscPartitions]; - byte lights[dscPartitions]; - - // Process keypad and module data, returns true if data is available - bool handleModule(); - - // True if dscBufferSize needs to be increased - static volatile bool bufferOverflow; - - // Timer interrupt function to capture data - declared as public for use by AVR Timer1 - static void dscDataInterrupt(); - - // Deprecated - bool handlePanel(); // Returns true if valid panel data is available. Relabeled to loop() - bool processRedundantData; // Controls if repeated periodic commands are processed and displayed (default: false) - - private: - - void processPanelStatus(); - void processPanelStatus0(byte partition, byte panelByte); - void processPanelStatus2(byte partition, byte panelByte); - void processPanelStatus4(byte partition, byte panelByte); - void processPanelStatus5(byte partition, byte panelByte); - void processPanel_0x16(); - void processPanel_0x27(); - void processPanel_0x2D(); - void processPanel_0x34(); - void processPanel_0x3E(); - void processPanel_0x87(); - void processPanel_0xA5(); - void processPanel_0xE6(); - void processPanel_0xE6_0x09(); - void processPanel_0xE6_0x0B(); - void processPanel_0xE6_0x0D(); - void processPanel_0xE6_0x0F(); - void processPanel_0xE6_0x1A(); - void processPanel_0xEB(); - void processReadyStatus(byte partitionIndex, bool status); - void processAlarmStatus(byte partitionIndex, bool status); - void processExitDelayStatus(byte partitionIndex, bool status); - void processEntryDelayStatus(byte partitionIndex, bool status); - void processZoneStatus(byte zonesByte, byte panelByte); - void processTime(byte panelByte); - void processAlarmZones(byte panelByte, byte startByte, byte zoneCountOffset, byte writeValue); - void processAlarmZonesStatus(byte zonesByte, byte zoneCount, byte writeValue); - void processArmed(byte partitionIndex, bool armedStatus); - void processPanelAccessCode(byte partitionIndex, byte dscCode, bool accessCodeIncrease = true); - - void printPanelPartitionStatus(byte startPartition, byte startByte, byte endByte); - void printPanelStatus0(byte panelByte); - void printPanelStatus1(byte panelByte); - void printPanelStatus2(byte panelByte); - void printPanelStatus3(byte panelByte); - void printPanelStatus4(byte panelByte); - void printPanelStatus5(byte panelByte); - void printPanelStatus14(byte panelByte); - void printPanelStatus16(byte panelByte); - void printPanelStatus17(byte panelByte); - void printPanelStatus18(byte panelByte); - void printPanelStatus1B(byte panelByte); - - void printPanelMessages(byte panelByte); - void printPanelLights(byte panelByte, bool printMessage = true); - void printPanelTime(byte panelByte); - void printPanelBeeps(byte panelByte); - void printPanelTone(byte panelByte); - void printPanelBuzzer(byte panelByte); - bool printPanelZones(byte inputByte, byte startZone); - void printPanelAccessCode(byte dscCode, bool accessCodeIncrease = true); - void printPanelBitNumbers(byte panelByte, byte startNumber, byte startBit = 0, byte stopBit = 7, bool printNone = true); - void printNumberSpace(byte number); - void printNumberOffset(byte panelByte, int numberOffset); - void printUnknownData(); - void printPartition(); - void printStatusLights(); - void printStatusLightsFlashing(); - void printZoneLights(bool lowerRange = true); - void printPanel_0x05(); - void printPanel_0x0A_0F(); - void printPanel_0x11(); - void printPanel_0x16(); - void printPanel_0x1B(); - void printPanel_0x1C(); - void printPanel_0x22_28_33_39(); - void printPanel_0x27(); - void printPanel_0x2D(); - void printPanel_0x34(); - void printPanel_0x3E(); - void printPanel_0x41(); - void printPanel_0x4C(); - void printPanel_0x57(); - void printPanel_0x58(); - void printPanel_0x5D_63(); - void printPanel_0x64(); - void printPanel_0x69(); - void printPanel_0x6E(); - void printPanel_0x70(); - void printPanel_0x75(); - void printPanel_0x7A(); - void printPanel_0x7F(); - void printPanel_0x82(); - void printPanel_0x87(); - void printPanel_0x8D(); - void printPanel_0x94(); - void printPanel_0x9E(); - void printPanel_0xA5(); - void printPanel_0xAA(); - void printPanel_0xB1(); - void printPanel_0xBB(); - void printPanel_0xC3(); - void printPanel_0xCE(); - void printPanel_0xD5(); - void printPanel_0xE6(); - void printPanel_0xE6_0x01_06_20_21(); - void printPanel_0xE6_0x08_0A_0C_0E(); - void printPanel_0xE6_0x09(); - void printPanel_0xE6_0x0B(); - void printPanel_0xE6_0x0D(); - void printPanel_0xE6_0x0F(); - void printPanel_0xE6_0x17(); - void printPanel_0xE6_0x18(); - void printPanel_0xE6_0x19(); - void printPanel_0xE6_0x1A(); - void printPanel_0xE6_0x1D(); - void printPanel_0xE6_0x1F(); - void printPanel_0xE6_0x2B(); - void printPanel_0xE6_0x2C(); - void printPanel_0xE6_0x41(); - void printPanel_0xEB(); - void printPanel_0xEC(); - - void printModule_0x77(); - void printModule_0xBB(); - void printModule_0xDD(); - void printModule_Status(); - void printModule_0x11(); - void printModule_0x41(); - void printModule_0x4C(); - void printModule_0x57(); - void printModule_0x58(); - void printModule_0x70(); - void printModule_0x94(); - void printModule_0xD5(); - bool printModule_Keys(); - void printModule_KeyCodes(byte keyByte); - void printModule_Expander(); - bool printModuleSlots(byte startCount, byte startByte, byte endByte, byte startMask, byte endMask, byte bitShift, byte matchValue, bool reverse = false); - - bool validCRC(); - void writeKeys(const char * writeKeysArray); - void setWriteKey(const char receivedKey); - static void dscClockInterrupt(); - static bool redundantPanelData(byte previousCmd[], volatile byte currentCmd[], byte checkedBytes = dscReadSize); - - Stream* stream; - const char* writeKeysArray; - bool writeKeysPending; - bool writeArm[dscPartitions]; - bool queryResponse; - bool previousTrouble; - bool previousKeybus; - bool previousPower; - bool previousDisabled[dscPartitions]; - byte previousAccessCode[dscPartitions]; - byte previousLights[dscPartitions], previousStatus[dscPartitions]; - bool previousReady[dscPartitions]; - bool previousExitDelay[dscPartitions], previousEntryDelay[dscPartitions]; - byte previousExitState[dscPartitions]; - bool previousArmed[dscPartitions], previousArmedStay[dscPartitions]; - bool previousAlarm[dscPartitions]; - bool previousFire[dscPartitions]; - byte previousOpenZones[dscZones], previousAlarmZones[dscZones]; - byte previousPgmOutputs[2]; - bool keybusVersion1; - - static byte dscClockPin; - static byte dscReadPin; - static byte dscWritePin; - static byte writeByte, writeBit; - static bool virtualKeypad; - static char writeKey; - static byte panelBitCount, panelByteCount; - static volatile bool writeKeyPending; - static volatile bool writeAlarm, starKeyCheck, starKeyWait[dscPartitions]; - static volatile bool moduleDataCaptured; - static volatile unsigned long clockHighTime, keybusTime; - static volatile byte panelBufferLength; - static volatile byte panelBuffer[dscBufferSize][dscReadSize]; - static volatile byte panelBufferBitCount[dscBufferSize], panelBufferByteCount[dscBufferSize]; - static volatile byte moduleBitCount, moduleByteCount; - static volatile byte currentCmd, statusCmd, moduleCmd, moduleSubCmd; - static volatile byte isrPanelData[dscReadSize], isrPanelBitTotal, isrPanelBitCount, isrPanelByteCount; - static volatile byte isrModuleData[dscReadSize], isrModuleBitTotal, isrModuleBitCount, isrModuleByteCount; -}; +#if defined(__AVR__) +ISR(TIMER1_OVF_vect) { + dscKeypadInterface::dscClockInterrupt(); +} +#endif // __AVR__ + +// DSC Classic Keypad Interface +#elif defined dscClassicKeypad +#include "dscClassicKeypad.h" + +byte dscClassicKeypadInterface::dscClockPin; +byte dscClassicKeypadInterface::dscReadPin; +byte dscClassicKeypadInterface::dscWritePin; +int dscClassicKeypadInterface::clockInterval; +volatile byte dscClassicKeypadInterface::keyData; +volatile byte dscClassicKeypadInterface::keyBufferLength; +volatile byte dscClassicKeypadInterface::keyBuffer[dscBufferSize]; +volatile bool dscClassicKeypadInterface::bufferOverflow; +volatile bool dscClassicKeypadInterface::commandReady; +volatile bool dscClassicKeypadInterface::moduleDataDetected; +volatile bool dscClassicKeypadInterface::alarmKeyDetected; +volatile bool dscClassicKeypadInterface::alarmKeyResponsePending; +volatile byte dscClassicKeypadInterface::clockCycleCount; +volatile byte dscClassicKeypadInterface::clockCycleTotal; +volatile byte dscClassicKeypadInterface::panelCommand[dscReadSize]; +volatile byte dscClassicKeypadInterface::isrPanelBitTotal; +volatile byte dscClassicKeypadInterface::isrPanelBitCount; +volatile byte dscClassicKeypadInterface::panelCommandByteCount; +volatile byte dscClassicKeypadInterface::isrModuleData[dscReadSize]; +volatile byte dscClassicKeypadInterface::isrModuleBitTotal; +volatile byte dscClassicKeypadInterface::isrModuleBitCount; +volatile byte dscClassicKeypadInterface::isrModuleByteCount; +volatile byte dscClassicKeypadInterface::panelCommandByteTotal; +volatile byte dscClassicKeypadInterface::moduleData[dscReadSize]; +volatile unsigned long dscClassicKeypadInterface::intervalStart; +volatile unsigned long dscClassicKeypadInterface::beepInterval; +volatile unsigned long dscClassicKeypadInterface::repeatInterval; +volatile unsigned long dscClassicKeypadInterface::keyInterval; +volatile unsigned long dscClassicKeypadInterface::alarmKeyTime; +volatile unsigned long dscClassicKeypadInterface::alarmKeyInterval; +#if defined(__AVR__) +ISR(TIMER1_OVF_vect) { + dscClassicKeypadInterface::dscClockInterrupt(); +} +#endif // __AVR__ + + +// DSC PowerSeries +#else +#include "dscKeybus.h" + +byte dscKeybusInterface::dscClockPin; +byte dscKeybusInterface::dscReadPin; +byte dscKeybusInterface::dscWritePin; +char dscKeybusInterface::writeKey; +byte dscKeybusInterface::writePartition; +byte dscKeybusInterface::writeByte; +byte dscKeybusInterface::writeBit; +bool dscKeybusInterface::virtualKeypad; +bool dscKeybusInterface::processModuleData; +byte dscKeybusInterface::panelData[dscReadSize]; +byte dscKeybusInterface::panelByteCount; +byte dscKeybusInterface::panelBitCount; +volatile bool dscKeybusInterface::writeKeyPending; +volatile byte dscKeybusInterface::moduleData[dscReadSize]; +volatile bool dscKeybusInterface::moduleDataCaptured; +volatile bool dscKeybusInterface::moduleDataDetected; +volatile byte dscKeybusInterface::moduleByteCount; +volatile byte dscKeybusInterface::moduleBitCount; +volatile bool dscKeybusInterface::writeAlarm; +volatile bool dscKeybusInterface::starKeyCheck; +volatile bool dscKeybusInterface::starKeyWait[dscPartitions]; +volatile bool dscKeybusInterface::bufferOverflow; +volatile byte dscKeybusInterface::panelBufferLength; +volatile byte dscKeybusInterface::panelBuffer[dscBufferSize][dscReadSize]; +volatile byte dscKeybusInterface::panelBufferBitCount[dscBufferSize]; +volatile byte dscKeybusInterface::panelBufferByteCount[dscBufferSize]; +volatile byte dscKeybusInterface::isrPanelData[dscReadSize]; +volatile byte dscKeybusInterface::isrPanelByteCount; +volatile byte dscKeybusInterface::isrPanelBitCount; +volatile byte dscKeybusInterface::isrPanelBitTotal; +volatile byte dscKeybusInterface::isrModuleData[dscReadSize]; +volatile byte dscKeybusInterface::currentCmd; +volatile byte dscKeybusInterface::statusCmd; +volatile byte dscKeybusInterface::moduleCmd; +volatile byte dscKeybusInterface::moduleSubCmd; +volatile unsigned long dscKeybusInterface::clockHighTime; +volatile unsigned long dscKeybusInterface::keybusTime; + +// Interrupt function called after 250us by dscClockInterrupt() using AVR Timer1, disables the timer and calls +// dscDataInterrupt() to read the data line +#if defined(__AVR__) +ISR(TIMER1_OVF_vect) { + TCCR1B = 0; // Disables Timer1 + dscKeybusInterface::dscDataInterrupt(); +} +#endif // __AVR__ +#endif // dscClassicSeries, dscKeypadInterface #endif // dscKeybusInterface_h diff --git a/src/dscKeybusPrintData.cpp b/src/dscKeybusPrintData.cpp index 9585154..f634b6c 100644 --- a/src/dscKeybusPrintData.cpp +++ b/src/dscKeybusPrintData.cpp @@ -29,7 +29,7 @@ along with this program. If not, see . */ - #include "dscKeybusInterface.h" + #include "dscKeybus.h" /* @@ -92,7 +92,7 @@ void dscKeybusInterface::printPanelMessage() { case 0x57: printPanel_0x57(); return; // Wireless key query | Structure: complete | Content: *incomplete case 0x58: printPanel_0x58(); return; // Module status query | Structure: complete | Content: *incomplete case 0x5D: - case 0x63: printPanel_0x5D_63(); return; // Flash panel lights: status and zones 1-32, partition 2 | Structure: complete | Content: complete + case 0x63: printPanel_0x5D_63(); return; // Flash panel lights: status and zones 1-32, partitions 1-2 | Structure: complete | Content: complete case 0x64: printPanel_0x64(); return; // Beep, partition 1 | Structure: complete | Content: complete case 0x69: printPanel_0x69(); return; // Beep, partition 2 | Structure: complete | Content: complete case 0x6E: printPanel_0x6E(); return; // LCD keypad display | Structure: complete | Content: complete @@ -126,9 +126,9 @@ void dscKeybusInterface::printPanelMessage() { // Processes keypad and module notifications and responses to panel queries void dscKeybusInterface::printModuleMessage() { switch (moduleData[0]) { - case 0x77: printModule_0x77(); return; // Keypad fire alarm | Structure: complete | Content: complete - case 0xBB: printModule_0xBB(); return; // Keypad auxiliary alarm | Structure: complete | Content: complete - case 0xDD: printModule_0xDD(); return; // Keypad panic alarm | Structure: complete | Content: complete + case 0xBB: printModule_0xBB(); return; // Keypad fire alarm | Structure: complete | Content: complete + case 0xDD: printModule_0xDD(); return; // Keypad auxiliary alarm | Structure: complete | Content: complete + case 0xEE: printModule_0xEE(); return; // Keypad panic alarm | Structure: complete | Content: complete } stream->print(F("[Module/0x")); @@ -223,7 +223,7 @@ void dscKeybusInterface::printPanelMessages(byte panelByte) { case 0x03: stream->print(F("Zones open")); break; case 0x04: stream->print(F("Armed: Stay")); break; case 0x05: stream->print(F("Armed: Away")); break; - case 0x06: stream->print(F("Armed: No entry delay")); break; + case 0x06: stream->print(F("Armed: Stay with no entry delay")); break; case 0x07: stream->print(F("Failed to arm")); break; case 0x08: stream->print(F("Exit delay in progress")); break; case 0x09: stream->print(F("Arming: No entry delay")); break; @@ -236,7 +236,7 @@ void dscKeybusInterface::printPanelMessages(byte panelByte) { case 0x12: stream->print(F("Battery check in progress")); break; case 0x14: stream->print(F("Auto-arm in progress")); break; case 0x15: stream->print(F("Arming with bypassed zones")); break; - case 0x16: stream->print(F("Armed: No entry delay")); break; + case 0x16: stream->print(F("Armed: Away with no entry delay")); break; case 0x19: stream->print(F("Disarmed: Alarm memory")); break; case 0x22: stream->print(F("Disarmed: Recent closing")); break; case 0x2F: stream->print(F("Keypad LCD test")); break; @@ -985,12 +985,14 @@ void dscKeybusInterface::printPanelStatus5(byte panelByte) { * from multiple sets of status messages, split into printPanelStatus4...printPanelStatus1B. */ void dscKeybusInterface::printPanelStatus14(byte panelByte) { + #if !defined(__AVR__) // Excludes Arduino/AVR to conserve storage space switch (panelData[panelByte]) { case 0xC0: stream->print(F("TLink com fault")); return; case 0xC2: stream->print(F("Tlink network fault")); return; case 0xC4: stream->print(F("TLink receiver trouble")); return; case 0xC5: stream->print(F("TLink receiver restored")); return; } + #endif printUnknownData(); } @@ -1920,7 +1922,12 @@ void dscKeybusInterface::printPanel_0x87() { * Byte 0 1 2 3 4 5 6 7 8 9 */ void dscKeybusInterface::printPanel_0x8D() { + #if !defined(__AVR__) + stream->print(F("Module programming entry: ")); + printModuleProgramming(panelData[2], panelData[3]); + #else stream->print(F("Module programming entry")); + #endif } @@ -1947,7 +1954,12 @@ void dscKeybusInterface::printPanel_0x8D() { * Byte 0 1 2 3 4 5 6 7 8 9 10 */ void dscKeybusInterface::printPanel_0x94() { + #if !defined(__AVR__) + stream->print(F("Module programming request: ")); + printModuleProgramming(panelData[2], panelData[3]); + #else stream->print(F("Module programming request")); + #endif } @@ -2143,8 +2155,8 @@ void dscKeybusInterface::printPanel_0xBB() { * Content decoding: *incomplete * * Byte 2: bit 0-2 unknown - * Byte 2: bit 3 active when dialer attempt begin - * Byte 2: bit 4 dialer enabled (always true on old-gen?) + * Byte 2: bit 3 TLM available or communications disabled (no trouble) + * Byte 2: bit 4 TLM trouble or dialing attempt (with/without trouble) * Byte 2: bit 5 keypad lockout active * Byte 2: bit 6-7 unknown * Byte 3: Unknown, always observed as 11111111 @@ -2159,16 +2171,12 @@ void dscKeybusInterface::printPanel_0xBB() { */ void dscKeybusInterface::printPanel_0xC3() { if (panelData[3] == 0xFF) { + stream->print(F("TLM: ")); + if (panelData[2] & 0x10) stream->print(F("trouble/attempt")); + else stream->print(F("available/disabled")); - if (panelData[2] & 0x01 || panelData[2] & 0x02 || panelData[2] & 0x04 || panelData[2] & 0x40 || panelData[2] & 0x80) printUnknownData(); - else { - stream->print(F("Dialer: ")); - if (panelData[2] & 0x10) stream->print(F("enabled")); - else stream->print(F("disabled")); - - if (panelData[2] & 0x08) stream->print(F(" | Dialer call attempt")); - if (panelData[2] & 0x20) stream->print(F(" | Keypad lockout")); - } + if (panelData[2] & 0x08) stream->print(F(" | Dialer call attempt")); + if (panelData[2] & 0x20) stream->print(F(" | Keypad lockout")); } else printUnknownData(); } @@ -2832,11 +2840,11 @@ void dscKeybusInterface::printPanel_0xEC() { * Structure decoding: complete * Content decoding: complete * - * Byte 0: 01110111 + * Byte 0: 10111011 * - * 01110111 1 11111111 11111111 11111111 11111111 11111111 11111111 [Keypad] Fire alarm + * 10111011 1 11111111 11111111 11111111 11111111 11111111 11111111 [Keypad] Fire alarm */ -void dscKeybusInterface::printModule_0x77() { +void dscKeybusInterface::printModule_0xBB() { stream->print(F("[Keypad] Fire alarm")); } @@ -2846,11 +2854,11 @@ void dscKeybusInterface::printModule_0x77() { * Structure decoding: complete * Content decoding: complete * - * Byte 0: 10111011 + * Byte 0: 11011101 * - * 10111011 1 11111111 11111111 11111111 11111111 11111111 11111111 [Keypad] Aux alarm + * 11011101 1 11111111 11111111 11111111 11111111 11111111 11111111 [Keypad] Aux alarm */ -void dscKeybusInterface::printModule_0xBB() { +void dscKeybusInterface::printModule_0xDD() { stream->print(F("[Keypad] Auxiliary alarm")); } @@ -2860,11 +2868,11 @@ void dscKeybusInterface::printModule_0xBB() { * Structure decoding: complete * Content decoding: complete * - * Byte 0: 11011101 + * Byte 0: 11101110 * - * 11011101 1 11111111 11111111 11111111 11111111 11111111 11111111 [Keypad] Panic alarm + * 11101110 1 11111111 11111111 11111111 11111111 11111111 11111111 [Keypad] Panic alarm */ -void dscKeybusInterface::printModule_0xDD() { +void dscKeybusInterface::printModule_0xEE() { stream->print(F("[Keypad] Panic alarm")); } @@ -3733,6 +3741,24 @@ bool dscKeybusInterface::printModuleSlots(byte outputNumber, byte startByte, byt } +// Print 0x8D and 0x94 section and command subsection data used for programming modules +void dscKeybusInterface::printModuleProgramming(byte panelByte2, byte panelByte3) { + switch (panelByte2) { + case 0x11: stream->print(F("RF5132")); break; //section 804 verified on pc1832 and pc5020 + case 0x14: stream->print(F("RF5400")); break; //section 801 not verified + case 0x15: stream->print(F("RF5936")); break; //section 802 not verified + case 0x16: stream->print(F("LINKS2X50")); break; //section 803 not verified + case 0x17: stream->print(F("PC5108L")); break; //section 806 not verified + case 0x19: stream->print(F("RF5100")); break; //section 805 not verified + case 0x31: stream->print(F("*5 user")); break; //*5 access codes verified on pc1832 and pc5020 + default: stream->print("Unknown data"); + } + stream->print(" | "); + if (panelByte3 < 16) stream->print("0"); + stream->print(panelByte3, HEX); +} + + /* * Panel lights and status message for commands: 0x05, 0x1B, 0x27, 0x2D, 0x34, 0x3E */ diff --git a/src/dscKeybusProcessData.cpp b/src/dscKeybusProcessData.cpp index 610c220..395c749 100644 --- a/src/dscKeybusProcessData.cpp +++ b/src/dscKeybusProcessData.cpp @@ -27,7 +27,7 @@ along with this program. If not, see . */ -#include "dscKeybusInterface.h" +#include "dscKeybus.h" // Resets the state of all status components as changed for sketches to get the current status @@ -236,7 +236,7 @@ void dscKeybusInterface::processPanelStatus() { armedAway[partitionIndex] = true; } - writeArm[partitionIndex] = false; + writeAccessCode[partitionIndex] = false; armed[partitionIndex] = true; if (armed[partitionIndex] != previousArmed[partitionIndex] || armedStay[partitionIndex] != previousArmedStay[partitionIndex]) { @@ -255,8 +255,7 @@ void dscKeybusInterface::processPanelStatus() { // Exit delay in progress case 0x08: { - writeArm[partitionIndex] = false; - accessCodePrompt = false; + writeAccessCode[partitionIndex] = false; processExitDelayStatus(partitionIndex, true); @@ -306,19 +305,18 @@ void dscKeybusInterface::processPanelStatus() { // Partition armed with no entry delay case 0x06: case 0x16: { - noEntryDelay[partitionIndex] = true; + armed[partitionIndex] = true; // Sets an armed mode if not already set, used if interface is initialized while the panel is armed - if (!armedStay[partitionIndex] && !armedAway[partitionIndex]) armedStay[partitionIndex] = true; - - armed[partitionIndex] = true; - if (armed[partitionIndex] != previousArmed[partitionIndex]) { - previousArmed[partitionIndex] = armed[partitionIndex]; - previousArmedStay[partitionIndex] = armedStay[partitionIndex]; - armedChanged[partitionIndex] = true; - if (!pauseStatus) statusChanged = true; + if (!armedStay[partitionIndex] && !armedAway[partitionIndex]) { + if (panelData[messageByte] == 0x06) { + armedStay[partitionIndex] = true; + previousArmedStay[partitionIndex] = armedStay[partitionIndex]; + } + else armedAway[partitionIndex] = true; } + processNoEntryDelayStatus(partitionIndex, true); processReadyStatus(partitionIndex, false); break; } @@ -358,7 +356,8 @@ void dscKeybusInterface::processPanelStatus() { // Enter access code case 0x9F: { - if (writeArm[partitionIndex]) { // Ensures access codes are only sent when an arm command is sent through this interface + if (writeAccessCode[partitionIndex]) { // Ensures access codes are only sent when an arm or command output key is sent through this interface + writeAccessCode[partitionIndex] = false; accessCodePrompt = true; if (!pauseStatus) statusChanged = true; } @@ -407,12 +406,6 @@ void dscKeybusInterface::processPanel_0x27() { } armed[partitionIndex] = true; - if (armed[partitionIndex] != previousArmed[partitionIndex] || armedStay[partitionIndex] != previousArmedStay[partitionIndex]) { - previousArmed[partitionIndex] = armed[partitionIndex]; - previousArmedStay[partitionIndex] = armedStay[partitionIndex]; - armedChanged[partitionIndex] = true; - if (!pauseStatus) statusChanged = true; - } processExitDelayStatus(partitionIndex, false); exitState[partitionIndex] = 0; @@ -477,6 +470,11 @@ void dscKeybusInterface::processPanel_0x3E() { void dscKeybusInterface::processPanel_0x87() { if (!validCRC()) return; + // Resets flag to write access code if needed when writing command output keys + for (byte partitionIndex = 0; partitionIndex < dscPartitions; partitionIndex++) { + writeAccessCode[partitionIndex] = false; + } + pgmOutputs[0] = panelData[3] & 0x03; pgmOutputs[0] |= panelData[2] << 2; pgmOutputs[1] = panelData[2] >> 6; @@ -513,6 +511,7 @@ void dscKeybusInterface::processPanel_0xA5() { byte partition = panelData[3] >> 6; switch (panelData[5] & 0x03) { case 0x00: processPanelStatus0(partition, 6); break; + case 0x01: processPanelStatus1(partition, 6); break; case 0x02: processPanelStatus2(partition, 6); break; } } @@ -597,6 +596,7 @@ void dscKeybusInterface::processPanel_0xEB() { switch (panelData[7] & 0x07) { case 0x00: processPanelStatus0(partition, 8); break; + case 0x01: processPanelStatus1(partition, 8); break; case 0x02: processPanelStatus2(partition, 8); break; case 0x04: processPanelStatus4(partition, 8); break; case 0x05: processPanelStatus5(partition, 8); break; @@ -686,6 +686,13 @@ void dscKeybusInterface::processPanelStatus0(byte partition, byte panelByte) { processArmed(partitionIndex, false); processAlarmStatus(partitionIndex, false); processEntryDelayStatus(partitionIndex, false); + + // Disarmed by access codes 1-34, 40-42 + if (panelData[panelByte] >= 0xC0 && panelData[panelByte] <= 0xE4) { + byte dscCode = panelData[panelByte] - 0xBF; + processPanelAccessCode(partitionIndex, dscCode); + } + return; } @@ -725,12 +732,20 @@ void dscKeybusInterface::processPanelStatus0(byte partition, byte panelByte) { processPanelAccessCode(partitionIndex, dscCode); return; } +} - // Disarmed by access codes 1-34, 40-42 - if (panelData[panelByte] >= 0xC0 && panelData[panelByte] <= 0xE4) { - byte dscCode = panelData[panelByte] - 0xBF; - processPanelAccessCode(partitionIndex, dscCode); - return; + +void dscKeybusInterface::processPanelStatus1(byte partition, byte panelByte) { + if (partition == 0 || partition > dscPartitions) return; + byte partitionIndex = partition - 1; + + switch (panelData[panelByte]) { + + // Armed with no entry delay + case 0xD2: { + processNoEntryDelayStatus(partitionIndex, false); + return; + } } } @@ -764,7 +779,7 @@ void dscKeybusInterface::processPanelStatus2(byte partition, byte panelByte) { return; } - if (panelData[panelByte] == 0xA5) { + if (panelData[0] == 0xA5) { switch (panelData[panelByte]) { // Activate stay/away zones @@ -779,7 +794,7 @@ void dscKeybusInterface::processPanelStatus2(byte partition, byte panelByte) { // Armed with no entry delay case 0x9C: { - noEntryDelay[partitionIndex] = true; + processNoEntryDelayStatus(partitionIndex, true); processReadyStatus(partitionIndex, false); return; } @@ -886,6 +901,16 @@ void dscKeybusInterface::processEntryDelayStatus(byte partitionIndex, bool statu } +void dscKeybusInterface::processNoEntryDelayStatus(byte partitionIndex, bool status) { + noEntryDelay[partitionIndex] = status; + if (noEntryDelay[partitionIndex] != previousNoEntryDelay[partitionIndex]) { + previousNoEntryDelay[partitionIndex] = noEntryDelay[partitionIndex]; + armedChanged[partitionIndex] = true; + if (!pauseStatus) statusChanged = true; + } +} + + void dscKeybusInterface::processZoneStatus(byte zonesByte, byte panelByte) { openZones[zonesByte] = panelData[panelByte]; byte zonesChanged = openZones[zonesByte] ^ previousOpenZones[zonesByte]; diff --git a/src/dscKeypad.cpp b/src/dscKeypad.cpp new file mode 100644 index 0000000..cd946a0 --- /dev/null +++ b/src/dscKeypad.cpp @@ -0,0 +1,550 @@ +/* + DSC Keybus Interface + + https://github.com/taligentx/dscKeybusInterface + + This library is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#include "dscKeypad.h" +#if defined dscKeypad_h + + +#if defined(ESP32) +portMUX_TYPE dscKeypadInterface::timer1Mux = portMUX_INITIALIZER_UNLOCKED; +hw_timer_t * dscKeypadInterface::timer1 = NULL; +#endif // ESP32 + + +dscKeypadInterface::dscKeypadInterface(byte setClockPin, byte setReadPin, byte setWritePin) { + dscClockPin = setClockPin; + dscReadPin = setReadPin; + dscWritePin = setWritePin; + commandReady = true; + keyData = 0xFF; + clockInterval = 57800; // Sets AVR timer 1 to trigger an overflow interrupt every ~500us to generate a 1kHz clock signal +} + + +void dscKeypadInterface::begin(Stream &_stream) { + pinMode(dscClockPin, OUTPUT); + pinMode(dscReadPin, INPUT); + pinMode(dscWritePin, OUTPUT); + digitalWrite(dscClockPin, LOW); + digitalWrite(dscWritePin, LOW); + stream = &_stream; + + // Platform-specific timers setup the Keybus 1kHz clock signal + + // Arduino/AVR Timer1 calls ISR(TIMER1_OVF_vect) + #if defined(__AVR__) + TCCR1A = 0; + TCCR1B = 0; + TCNT1 = clockInterval; + TCCR1B |= (1 << CS10); + + // esp8266 timer1 calls dscClockInterrupt() + #elif defined(ESP8266) + timer1_isr_init(); + timer1_attachInterrupt(dscClockInterrupt); + timer1_write(2500); + + // esp32 timer1 calls dscClockInterrupt() + #elif defined(ESP32) + timer1 = timerBegin(1, 80, true); + timerStop(timer1); + timerAttachInterrupt(timer1, &dscClockInterrupt, true); + timerAlarmWrite(timer1, 500, true); + timerAlarmEnable(timer1); + #endif + + intervalStart = millis(); + + unsigned long keybusTime = millis(); + while (millis() - keybusTime < 4000) { // Waits for the keypad to be powered on + if (!digitalRead(dscReadPin)) keybusTime = millis(); + #if defined(ESP8266) || defined(ESP32) + yield(); + #endif + } +} + + +bool dscKeypadInterface::loop() { + + // Sets up the next panel command once the previous command is complete + if (commandReady && millis() - intervalStart >= commandInterval) { + commandReady = false; + + // Sets the startup command sequence + if (startupCycle) { + static byte startupCommand = 0x16; + switch (startupCommand) { + case 0x16: { + for (byte i = 0; i < 5; i++) panelCommand[i] = panelCommand16[i]; + panelCommandByteTotal = 5; + startupCommand = 0x5D; + break; + } + case 0x5D: { + delay(200); + for (byte i = 0; i < 7; i++) panelCommand[i] = panelCommand5D[i]; + panelCommandByteTotal = 7; + startupCommand = 0x4C; + break; + } + case 0x4C: { + for (byte i = 0; i < 12; i++) panelCommand[i] = panelCommand4C[i]; + panelCommandByteTotal = 12; + startupCommand = 0xB1; + break; + } + case 0xB1: { + for (byte i = 0; i < 10; i++) panelCommand[i] = panelCommandB1[i]; + panelCommandByteTotal = 10; + startupCommand = 0xA5; + break; + } + case 0xA5: { + for (byte i = 0; i < 8; i++) panelCommand[i] = panelCommandA5[i]; + panelCommandByteTotal = 8; + startupCommand = 0x05; + break; + } + case 0x05: { + for (byte i = 0; i < 5; i++) panelCommand[i] = panelCommand05[i]; + panelCommandByteTotal = 5; + startupCommand = 0xD5; + break; + } + case 0xD5: { + for (byte i = 0; i < 9; i++) panelCommand[i] = panelCommandD5[i]; + panelCommandByteTotal = 9; + startupCommand = 0x27; + break; + } + case 0x27: { + for (byte i = 0; i < 7; i++) panelCommand[i] = panelCommand27[i]; + panelCommandByteTotal = 7; + startupCycle = false; + break; + } + } + } + + // Sets the next panel command to 0x1C alarm key verification if an alarm key is pressed + else if (alarmKeyDetected) { + alarmKeyDetected = false; + alarmKeyResponsePending = true; + panelCommand[0] = 0x1C; + panelCommandByteTotal = 1; + } + + // Sets the next panel command + else if (!alarmKeyResponsePending) { + + // Sets lights + if (panelLights != previousLights) { + previousLights = panelLights; + panelCommand05[1] = panelLights; + panelCommand27[1] = panelLights; + } + + // Sets next panel command to 0xD5 keypad zone query on keypad zone notification + if (panelCommand[0] == 0x05 && !bitRead(moduleData[5], 2)) { + for (byte i = 0; i < 9; i++) panelCommand[i] = panelCommandD5[i]; + panelCommandByteTotal = 9; + } + + // Sets next panel command to 0x27 zones 1-8 status if a zone changed + else if (panelZones != previousZones) { + previousZones = panelZones; + panelCommand27[5] = panelZones; + + int dataSum = 0; + for (byte panelByte = 0; panelByte < 6; panelByte++) dataSum += panelCommand27[panelByte]; + panelCommand27[6] = dataSum % 256; + + for (byte i = 0; i < 7; i++) panelCommand[i] = panelCommand27[i]; + panelCommandByteTotal = 7; + } + + else if (panelBlink != previousBlink || panelZonesBlink != previousZonesBlink) { + previousBlink = panelBlink; + previousZonesBlink = panelZonesBlink; + panelCommand5D[1] = panelBlink; + panelCommand5D[2] = panelZonesBlink; + + int dataSum = 0; + for (byte panelByte = 0; panelByte < 6; panelByte++) dataSum += panelCommand5D[panelByte]; + panelCommand5D[6] = dataSum % 256; + + for (byte i = 0; i < 7; i++) panelCommand[i] = panelCommand5D[i]; + panelCommandByteTotal = 7; + } + + // Sets next panel command to 0x64 beep if beep() is called + else if (setBeep) { + setBeep = false; + for (byte i = 0; i < 3; i++) panelCommand[i] = panelCommand64[i]; + panelCommandByteTotal = 3; + } + + else if (setTone) { + setTone = false; + for (byte i = 0; i < 3; i++) panelCommand[i] = panelCommand75[i]; + panelCommandByteTotal = 3; + } + + else if (setBuzzer) { + setBuzzer = false; + for (byte i = 0; i < 3; i++) panelCommand[i] = panelCommand7F[i]; + panelCommandByteTotal = 3; + } + + // Sets next panel command to 0x05 status command + else { + for (byte i = 0; i < 5; i++) panelCommand[i] = panelCommand05[i]; + panelCommandByteTotal = 5; + } + } + clockCycleCount = 0; + clockCycleTotal = (panelCommandByteTotal * 16) + 4; + + #if defined(__AVR__) + TIMSK1 |= (1 << TOIE1); // Enables AVR Timer 1 interrupt + #elif defined(ESP8266) + timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); + #elif defined(ESP32) + timerStart(timer1); + #endif + } + else if (!commandReady) intervalStart = millis(); + + // Sets panel lights + panelLight(lightReady, 0); + panelLight(lightArmed, 1); + panelLight(lightMemory, 2); + panelLight(lightBypass, 3); + panelLight(lightTrouble, 4); + panelLight(lightProgram, 5); + panelLight(lightFire, 6); + panelLight(lightBacklight, 7); + + // Sets zone lights + zoneLight(lightZone1, 0); + zoneLight(lightZone2, 1); + zoneLight(lightZone3, 2); + zoneLight(lightZone4, 3); + zoneLight(lightZone5, 4); + zoneLight(lightZone6, 5); + zoneLight(lightZone7, 6); + zoneLight(lightZone8, 7); + + // Skips key processing if the key buffer is empty + if (keyBufferLength == 0) return false; + + // Copies data from the buffer to keyData + static byte keyBufferIndex = 1; + byte dataIndex = keyBufferIndex - 1; + keyData = keyBuffer[dataIndex]; + keyBufferIndex++; + + // Resets counters when the buffer is cleared + #if defined(ESP32) + portENTER_CRITICAL(&timer1Mux); + #else + noInterrupts(); + #endif + + if (keyBufferIndex > keyBufferLength) { + keyBufferIndex = 1; + keyBufferLength = 0; + } + + #if defined(ESP32) + portEXIT_CRITICAL(&timer1Mux); + #else + interrupts(); + #endif + + if (keyData != 0xFF) { + keyAvailable = true; + switch (keyData) { + case 0x00: key = 0x00; break; // 0 + case 0x05: key = 0x05; break; // 1 + case 0x0A: key = 0x0A; break; // 2 + case 0x0F: key = 0x0F; break; // 3 + case 0x11: key = 0x11; break; // 4 + case 0x16: key = 0x16; break; // 5 + case 0x1B: key = 0x1B; break; // 6 + case 0x1C: key = 0x1C; break; // 7 + case 0x22: key = 0x22; break; // 8 + case 0x27: key = 0x27; break; // 9 + case 0x28: key = 0x28; break; // * + case 0x2D: key = 0x2D; break; // # + case 0x82: key = 0x82; break; // Enter + case 0x87: key = 0x87; break; // Right arrow + case 0x88: key = 0x88; break; // Left arrow + case 0xAF: key = 0xAF; break; // Arm: Stay + case 0xB1: key = 0xB1; break; // Arm: Away + case 0xBB: key = 0xBB; break; // Door chime + case 0xDA: key = 0xDA; break; // Reset + case 0xE1: key = 0xE1; break; // Quick exit + case 0xF7: key = 0xF7; break; // LCD keypad navigation + case 0x0B: key = 0x0B; break; // Fire alarm + case 0x0D: key = 0x0D; break; // Aux alarm + case 0x0E: key = 0x0E; break; // Panic alarm + default: keyAvailable = false; break; // Skips other DSC key values and invalid data + } + keyData = 0xFF; + } + + return true; +} + + +void dscKeypadInterface::panelLight(Light lightPanel, byte zoneBit) { + if (lightPanel == on) { + bitWrite(panelLights, zoneBit, 1); + bitWrite(panelBlink, zoneBit, 0); + } + else if (lightPanel == blink) bitWrite(panelBlink, zoneBit, 1); + else { + bitWrite(panelLights, zoneBit, 0); + bitWrite(panelBlink, zoneBit, 0); + } +} + + +void dscKeypadInterface::zoneLight(Light lightZone, byte zoneBit) { + if (lightZone == on ) { + bitWrite(panelZones, zoneBit, 1); + bitWrite(panelZonesBlink, zoneBit, 0); + } + else if (lightZone == blink) bitWrite(panelZonesBlink, zoneBit, 1); + else { + bitWrite(panelZones, zoneBit, 0); + bitWrite(panelZonesBlink, zoneBit, 0); + } +} + + + +void dscKeypadInterface::beep(byte beeps) { + if (!beeps) { + setBeep = false; + return; + } + + if (beeps >= 128) beeps = 255; + else beeps *= 2; + panelCommand64[1] = beeps; + + int dataSum = 0; + for (byte panelByte = 0; panelByte < 2; panelByte++) dataSum += panelCommand64[panelByte]; + panelCommand64[2] = dataSum % 256; + + setBeep = true; +} + + +void dscKeypadInterface::tone(byte beep, bool tone, byte interval) { + panelCommand75[1] = 0; + + if (tone >= 1) panelCommand75[1] |= 0x80; + + if (beep > 7) beep = 7; + if (beep >= 1) { + panelCommand75[1] |= beep << 4; + } + + if (interval > 15) interval = 15; + panelCommand75[1] |= interval; + + int dataSum = 0; + for (byte panelByte = 0; panelByte < 2; panelByte++) dataSum += panelCommand75[panelByte]; + panelCommand75[2] = dataSum % 256; + + setTone = true; +} + + +void dscKeypadInterface::buzzer(byte seconds) { + if (!seconds) { + setBuzzer = false; + return; + } + + panelCommand7F[1] = seconds; + + int dataSum = 0; + for (byte panelByte = 0; panelByte < 2; panelByte++) dataSum += panelCommand7F[panelByte]; + panelCommand7F[2] = dataSum % 256; + + setBuzzer = true; +} + + +#if defined(__AVR__) +void dscKeypadInterface::dscClockInterrupt() { +#elif defined(ESP8266) +void ICACHE_RAM_ATTR dscKeypadInterface::dscClockInterrupt() { +#elif defined(ESP32) +void IRAM_ATTR dscKeypadInterface::dscClockInterrupt() { +#endif + + // Toggles the clock pin for the length of a panel command + if (clockCycleCount < clockCycleTotal) { + static bool clockHigh = true; + if (clockHigh) { + clockHigh = false; + digitalWrite(dscClockPin, HIGH); + digitalWrite(dscWritePin, LOW); + } + else { + clockHigh = true; + digitalWrite(dscClockPin, LOW); + if (isrModuleByteCount < dscReadSize) { + + // Data is captured in each byte by shifting left by 1 bit and writing to bit 0 + if (isrModuleBitCount < 8) { + isrModuleData[isrModuleByteCount] <<= 1; + if (digitalRead(dscReadPin) == HIGH) { + isrModuleData[isrModuleByteCount] |= 1; + } + else { + moduleDataDetected = true; // Keypads and modules send data by pulling the data line low + } + } + + // Stores the stop bit by itself in byte 1 - this aligns the Keybus bytes with moduleData[] bytes + if (isrModuleBitTotal == 8) { + isrModuleData[1] = 1; // Sets the stop bit manually to 1 in byte 1 + isrModuleBitCount = 0; + isrModuleByteCount++; + } + + // Increments the bit counter if the byte is incomplete + else if (isrModuleBitCount < 7) { + isrModuleBitCount++; + } + + // Byte is complete, set the counters for the next byte + else { + isrModuleBitCount = 0; + isrModuleByteCount++; + } + + isrModuleBitTotal++; + } + + // Write panel data + + // Panel command byte 0 complete + if (isrPanelBitTotal == 8) { + digitalWrite(dscWritePin, HIGH); // Stop bit + + // Checks for an alarm key sent during 0x1C alarm key verification command to save in the key buffer + if (panelCommand[0] == 0x1C) { + alarmKeyResponsePending = false; + + if (isrModuleData[0] != 0xFF) { + if (keyBufferLength >= dscBufferSize) bufferOverflow = true; + else { + + // Converts the DSC alarm key value to handle a conflict with the door chime key (0xBB) + switch (isrModuleData[0]) { + case 0xBB: keyBuffer[keyBufferLength] = 0x0B; keyBufferLength++; break; // Fire alarm + case 0xDD: keyBuffer[keyBufferLength] = 0x0D; keyBufferLength++; break; // Aux alarm + case 0xEE: keyBuffer[keyBufferLength] = 0x0E; keyBufferLength++; break; // Panic alarm + default: break; + } + } + } + } + isrPanelBitTotal++; + } + + // Panel command bytes bit 7 + else if (isrPanelBitCount == 7) { + if (!bitRead(panelCommand[panelCommandByteCount], 0)) digitalWrite(dscWritePin, HIGH); + isrPanelBitCount = 0; + isrPanelBitTotal++; + panelCommandByteCount++; + } + + // Panel command bytes bits 0-6 + else if (panelCommandByteCount < panelCommandByteTotal) { + byte bitCount = 0; + for (byte i = 7; i > 0; i--) { + if (isrPanelBitCount == bitCount && !bitRead(panelCommand[panelCommandByteCount], i)) digitalWrite(dscWritePin, HIGH); + bitCount++; + } + isrPanelBitCount++; + isrPanelBitTotal++; + } + } + clockCycleCount++; + } + + // Panel command complete + else { + digitalWrite(dscClockPin, LOW); + + // Checks for module data + if (moduleDataDetected) { + moduleDataDetected = false; + for (byte i = 0; i < dscReadSize; i++) moduleData[i] = isrModuleData[i]; + + // Checks for an alarm key press and sets a flag to send panel command 0x1C alarm key verification + if (isrModuleData[0] != 0xFF && panelCommand[0] != 0x1C) { + alarmKeyDetected = true; + } + + // Checks for a partition 1 key to save in the key buffer + if (isrModuleData[2] != 0xFF && panelCommand[0] == 0x05) { + if (keyBufferLength >= dscBufferSize) bufferOverflow = true; + else { + keyBuffer[keyBufferLength] = isrModuleData[2]; + keyBufferLength++; + } + } + } + + // Resets counters + for (byte i = 0; i < dscReadSize; i++) isrModuleData[i] = 0; + isrModuleBitTotal = 0; + isrModuleBitCount = 0; + isrModuleByteCount = 0; + panelCommandByteCount = 0; + isrPanelBitTotal = 0; + isrPanelBitCount = 0; + commandReady = true; + + #if defined(__AVR__) + TIMSK1 = 0; // Disables AVR Timer 1 interrupt + #elif defined(ESP8266) + timer1_disable(); + #elif defined(ESP32) + timerStop(timer1); + #endif + } + + #if defined(__AVR__) + TCNT1 = clockInterval; + #endif +} + +#endif // dscKeypad_h diff --git a/src/dscKeypad.h b/src/dscKeypad.h new file mode 100644 index 0000000..6f51b1f --- /dev/null +++ b/src/dscKeypad.h @@ -0,0 +1,112 @@ +/* + DSC Keybus Interface + + https://github.com/taligentx/dscKeybusInterface + + This library is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + */ + +#ifndef dscKeypad_h +#define dscKeypad_h + +#include + +#if defined(__AVR__) +const byte dscBufferSize = 10; // Number of keys to buffer if the sketch is busy +#elif defined(ESP8266) || defined (ESP32) +const byte dscBufferSize = 50; +#endif +const byte dscReadSize = 16; // Maximum bytes of a Keybus command + +enum Light {off, on, blink}; // Custom values for keypad lights status + +class dscKeypadInterface { + + public: + dscKeypadInterface(byte setClockPin, byte setReadPin, byte setWritePin); + + // Interface control + void begin(Stream &_stream = Serial); // Initializes the stream output to Serial by default + bool loop(); // Returns true if valid panel data is available + void beep(byte beeps = 0); // Keypad beep, 1-128 beeps + void tone(byte beep = 0, bool tone = false, byte interval = 0); // Keypad tone pattern, 1-7 beeps at 1-15s interval, with optional constant tone + void buzzer(byte seconds = 0); // Keypad buzzer, 1-255 seconds + + // Keypad key + byte key, keyAvailable; + + // Keypad lights + Light lightReady = on, lightArmed, lightMemory, lightBypass, lightTrouble, lightProgram, lightFire, lightBacklight = on; + Light lightZone1, lightZone2, lightZone3, lightZone4, lightZone5, lightZone6, lightZone7, lightZone8; + + // Panel Keybus commands + byte panelCommand05[5] = {0x05, 0x81, 0x01, 0x10, 0xC7}; // Partition 1: Ready Backlight - Partition ready | Partition 2: disabled + byte panelCommand16[5] = {0x16, 0x0E, 0x23, 0xF1, 0x38}; // Panel version: v2.3 | Zone wiring: NC | Code length: 4 digits | *8 programming: no + byte panelCommand27[7] = {0x27, 0x81, 0x01, 0x10, 0xC7, 0x00, 0x80}; // Partition 1: Ready Backlight - Partition ready | Partition 2: disabled | Zones 1-8 open: none + byte panelCommand4C[12] = {0x4C, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}; // Module tamper query + byte panelCommand5D[7] = {0x5D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5D}; // Partition 1 | Status lights flashing: none | Zones 1-32 flashing: none + byte panelCommand64[3] = {0x64, 0x0, 0x64}; // Beep pattern, 1-128 beeps + byte panelCommand75[3] = {0x75, 0x0, 0x75}; // Tone pattern, beeps at interval with optional constant tone + byte panelCommand7F[3] = {0x7F, 0x0, 0x7F}; // Buzzer, 1-255 seconds + byte panelCommandA5[8] = {0xA5, 0x18, 0x0E, 0xED, 0x80, 0x00, 0x00, 0x38}; // Date, time, system status messages - partitions 1-2 + byte panelCommandB1[10] = {0xB1, 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xAD}; // Enabled zones 1-32 + byte panelCommandD5[9] = {0xD5, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}; // Keypad zone query + + /* + * moduleData[] stores keypad data in an array: command [0], stop bit by itself [1], followed by the + * remaining data. These can be accessed directly in the sketch to get data that is not already tracked + * in the library. See dscKeybusPrintData.cpp for the currently known DSC commands and data. + */ + static volatile byte moduleData[dscReadSize]; + + // Key data buffer overflow, true if dscBufferSize needs to be increased + static volatile bool bufferOverflow; + + // Timer interrupt function to capture data - declared as public for use by AVR Timer1 + static void dscClockInterrupt(); + + private: + + void zoneLight(Light lightZone, byte zoneBit); + void panelLight(Light lightPanel, byte zoneBit); + + Stream* stream; + byte panelLights = 0x81, previousLights = 0x81; + byte panelBlink, previousBlink; + byte panelZones, previousZones; + byte panelZonesBlink, previousZonesBlink; + bool startupCycle = true; + bool setBeep, setTone, setBuzzer; + byte commandInterval = 5; // Sets the milliseconds between panel commands + unsigned long intervalStart; + + #if defined(ESP32) + static hw_timer_t * timer1; + static portMUX_TYPE timer1Mux; + #endif + + static int clockInterval; + static byte dscClockPin, dscReadPin, dscWritePin; + static volatile byte keyData; + static volatile byte keyBufferLength; + static volatile byte keyBuffer[dscBufferSize]; + static volatile bool commandReady, moduleDataDetected; + static volatile bool alarmKeyDetected, alarmKeyResponsePending; + static volatile byte clockCycleCount, clockCycleTotal; + static volatile byte panelCommand[dscReadSize], panelCommandByteCount, panelCommandByteTotal; + static volatile byte isrPanelBitTotal, isrPanelBitCount; + static volatile byte isrModuleData[dscReadSize], isrModuleBitTotal, isrModuleBitCount, isrModuleByteCount; +}; + +#endif // dscKeypad_h