Files
ESPEasy/src/_P055_Chiming.ino
T

558 lines
15 KiB
Arduino

#include "_Plugin_Helper.h"
#ifdef USES_P055
// #######################################################################################################
// #################################### Plugin 055: Chiming Mechanism ####################################
// #######################################################################################################
/** Changelog:
* 2025-01-04 tonhuisman: Change check for a timesource to look for more than just NTP, but anything more reliable than 'Restore from RTC'
* Minor code cleanup
*/
// ESPEasy plugin to strike up to 4 physical bells and gongs with chiming sequences.
// You also can use an antique door bell as a single strikes (not ringing) notification.
// Optional you can use it as hourly chiming clock
// written by Jochen Krapf (jk@nerd2nerd.org)
// List of commands:
// (1) CHIME,<tokens> Play token direct
// (2) CHIMESAVE,<name>,<tokens> Save tokens with given name in FFS
// (3) CHIMEPLAY,<name> Play saved tokens given name out of FFS
// List of tokens:
// (a) '1'...'9', 'A'...'F' Bell number - 1=1st bell, 2=2nd bell, 4=3rd bell, 8=4th bell, numbers can be added to strike
// simultaniouly, F=all bells
// (b) '!' Double strike prev. token
// (c) '-' or ' ' Normal Pause
// (d) '=' Long Pause (3 times normal)
// (e) '.' Short Pause (1/3 of normal)
// (f) '|' Shortest Pause
// (g) '#' Comment - rest of the tokens will be ignored
// Note: If no pause is specified, a normal pause will be inserted "111" -> "1-1-1"
// Usage as Hourly Chime Clock:
// save twelve comma separated tokens with name "hours", enable checkbox "Hourly Chiming Clock Strike" in plugin web interface and enable
// NTP (advanced settings)
//
// examples:
// Historical coded with 1 bell: "1,11,111,1111,11111,111111,1111111,11111111,111111111,1111111111,11111111111,111111111111"
// Binary coded with 2 bells (2nd bell=1): "1112,1121,1122,1211,1212,1221,1222,2111,2112,2121,2122,2211"
// Binary coded with 1 bell (short pause=1):
// "1_1_1_11,1_1_11_1,1_1_111,1_11_1_1,1_11_11,1_111_1,1_1111,11_1_1_1,11_1_11,11_11_1,11_111,111_1_1"
// Binary coded with 1 bell (double strike=1):"1111!,111!1,111!1!,11!11,11!11!,11!1!1,11!1!1!,1!111,1!111!,1!11!1,1!11!1!,1!1!11"
//
// CHIMESAVE,hours,1111!,111!1,111!1!,11!11,11!11!,11!1!1,11!1!1!,1!111,1!111!,1!11!1,1!11!1!,1!1!11
// Usage as Alarm Clock:
// save tokens with name "<HH><MM>" and enable NTP (advanced settings)
//
// examples:
// CHIMESAVE,0815,1111! Daily Alarm at 8:15am
// CHIMESAVE,2015,11121 Daily Alarm at 8:15pm
// CHIMESAVE,2015 Delete Alarm at 8:15pm
// Electronics:
// Use a power-FET or an ULN2003 to switch on the bells coil with 12 or 24 volts
// #include <*.h> - no external lib required
# include "src/WebServer/Markup_Buttons.h"
# define PLUGIN_055
# define PLUGIN_ID_055 55
# define PLUGIN_NAME_055 "Notify - Chiming"
# define PLUGIN_055_FIFO_SIZE 64 // must be power of 2
# define PLUGIN_055_FIFO_MASK (PLUGIN_055_FIFO_SIZE - 1)
class CPlugin_055_Data {
public:
long millisStateEnd = 0;
long millisChimeTime = 60;
long millisPauseTime = 400;
int pin[4] = { 0 };
uint8_t lowActive = false;
uint8_t chimeClock = true;
char FIFO[PLUGIN_055_FIFO_SIZE] = { 0 };
uint8_t FIFO_IndexR = 0;
uint8_t FIFO_IndexW = 0;
};
static CPlugin_055_Data *Plugin_055_Data = nullptr;
boolean Plugin_055(uint8_t function, struct EventStruct *event, String& string)
{
boolean success = false;
switch (function)
{
case PLUGIN_DEVICE_ADD:
{
auto& dev = Device[++deviceCount];
dev.Number = PLUGIN_ID_055;
dev.Type = DEVICE_TYPE_TRIPLE;
dev.VType = Sensor_VType::SENSOR_TYPE_NONE;
dev.InverseLogicOption = true;
dev.setPin1Direction(gpio_direction::gpio_output);
dev.setPin2Direction(gpio_direction::gpio_output);
dev.setPin3Direction(gpio_direction::gpio_output);
break;
}
case PLUGIN_GET_DEVICENAME:
{
string = F(PLUGIN_NAME_055);
break;
}
case PLUGIN_GET_DEVICEGPIONAMES:
{
event->String1 = formatGpioName_output(F("Driver#1"));
event->String2 = formatGpioName_output(F("Driver#2"));
event->String3 = formatGpioName_output(F("Driver#4"));
break;
}
case PLUGIN_WEBFORM_SHOW_GPIO_DESCR:
{
string = concat(F("Driver#8: "), formatGpioLabel(static_cast<int>(Settings.TaskDevicePin[3][event->TaskIndex]), false));
success = true;
break;
}
case PLUGIN_WEBFORM_LOAD:
{
// default values
if (PCONFIG(0) <= 0) { // Plugin_055_millisChimeTime
PCONFIG(0) = 60;
}
if (PCONFIG(1) <= 0) { // Plugin_055_millisPauseTime
PCONFIG(1) = 400;
}
// FIXME TD-er: Should we add support for 4 pin definitions?
addFormPinSelect(PinSelectPurpose::Generic_output, formatGpioName_output(F("Driver#8")), F("TDP4"),
static_cast<int>(Settings.TaskDevicePin[3][event->TaskIndex]));
addFormSubHeader(F("Timing"));
addFormNumericBox(F("Chiming/Strike Time (ct)"), F("chimetime"), PCONFIG(0));
addUnit(F("ms"));
addFormNumericBox(F("Normal Pause Time (t)"), F("pausetime"), PCONFIG(1));
addUnit(F("ms"));
addFormNote(F("'1=1'&rArr;3t, '1-1' or '11'&rArr;1t, '1.1'&rArr;&#8531;t, '1|1'&rArr;&frac12;ct"));
addFormSubHeader(F("Chiming Clock"));
addFormCheckBox(F("Hourly Chiming Clock Strike"), F("chimeclock"), PCONFIG(2));
// addHtml(F("<TR><TD><TD>"));
addButton(F("'control?cmd=chimeplay,hours'"), F("Test 1&hellip;12"));
if (PCONFIG(2) && !(node_time.getTimeSource() < timeSource_t::Restore_RTC_time_source)) { // Somewhat reliable timesource?
addFormNote(F("Configure a timesource! (NTP, P2P, RTC, GPS)"));
}
success = true;
break;
}
case PLUGIN_WEBFORM_SAVE:
{
Settings.TaskDevicePin[3][event->TaskIndex] = (int8_t)getFormItemInt(F("TDP4"));
PCONFIG(0) = getFormItemInt(F("chimetime"));
PCONFIG(1) = getFormItemInt(F("pausetime"));
PCONFIG(2) = isFormItemChecked(F("chimeclock"));
success = true;
break;
}
case PLUGIN_INIT:
{
if (!Plugin_055_Data) {
Plugin_055_Data = new (std::nothrow) CPlugin_055_Data();
}
if (Plugin_055_Data != nullptr) {
Plugin_055_Data->lowActive = Settings.TaskDevicePin1Inversed[event->TaskIndex];
Plugin_055_Data->millisChimeTime = PCONFIG(0);
Plugin_055_Data->millisPauseTime = PCONFIG(1);
Plugin_055_Data->chimeClock = PCONFIG(2);
String log = F("Chime: GPIO: ");
for (uint8_t i = 0; i < 4; ++i)
{
int pin = Settings.TaskDevicePin[i][event->TaskIndex];
Plugin_055_Data->pin[i] = pin;
if (pin >= 0)
{
pinMode(pin, OUTPUT);
digitalWrite(pin, Plugin_055_Data->lowActive);
}
log += pin;
log += ' ';
}
if (Plugin_055_Data->lowActive) {
log += F("!");
}
addLogMove(LOG_LEVEL_INFO, log);
success = true;
}
break;
}
case PLUGIN_EXIT:
{
if (Plugin_055_Data != nullptr) {
delete Plugin_055_Data;
Plugin_055_Data = nullptr;
}
break;
}
case PLUGIN_WRITE:
{
if (!Plugin_055_Data) {
break;
}
String command = parseString(string, 1);
if (equals(command, F("chime")))
{
String param = parseStringToEndKeepCase(string, 2);
if (param.length() > 0) {
Plugin_055_AddStringFIFO(param);
}
success = true;
}
if (equals(command, F("chimeplay")))
{
String name = parseString(string, 2);
if (name.length() > 0) {
String param;
Plugin_055_ReadChime(name, param);
Plugin_055_AddStringFIFO(param);
}
success = true;
}
if (equals(command, F("chimesave")))
{
String name = parseString(string, 2);
String param = parseStringToEndKeepCase(string, 3);
if ((name.length() > 0) && (param.length() > 0)) {
Plugin_055_WriteChime(name, param);
Plugin_055_AddStringFIFO("1");
}
success = true;
}
break;
}
case PLUGIN_CLOCK_IN:
{
if (!Plugin_055_Data) {
break;
}
String tokens;
uint8_t hours = node_time.hour();
const uint8_t minutes = node_time.minute();
if (Plugin_055_Data->chimeClock)
{
const String tmpString = strformat(F("%02d%02d"), hours, minutes);
if (Plugin_055_ReadChime(tmpString, tokens)) {
Plugin_055_AddStringFIFO(tokens);
}
if (minutes == 0)
{
if (Plugin_055_ReadChime("hours", tokens) == 0) {
tokens = F("1111!,111!1,111!1!,11!11,11!11!,11!1!1,11!1!1!,1!111,1!111!,1!11!1,1!11!1!,1!1!11"); // 1..12
}
// hours 0..23 -> 1..12
hours = hours % 12;
if (hours == 0) {
hours = 12;
}
tokens = parseString(tokens, hours);
Plugin_055_AddStringFIFO(tokens);
}
}
success = true;
break;
}
case PLUGIN_FIFTY_PER_SECOND:
// case PLUGIN_TEN_PER_SECOND:
{
if (!Plugin_055_Data) {
break;
}
long millisAct = millis();
if (Plugin_055_Data->millisStateEnd > 0) // just striking?
{
if (timeDiff(millisAct, Plugin_055_Data->millisStateEnd) <= 0) // end reached?
{
for (uint8_t i = 0; i < 4; ++i)
{
if (Plugin_055_Data->pin[i] >= 0) {
digitalWrite(Plugin_055_Data->pin[i], Plugin_055_Data->lowActive);
}
}
Plugin_055_Data->millisStateEnd = 0;
}
}
if (Plugin_055_Data->millisStateEnd == 0) // just finished?
{
if (!Plugin_055_IsEmptyFIFO())
{
char c = Plugin_055_ReadFIFO();
# ifndef BUILD_NO_DEBUG
if (loglevelActiveFor(LOG_LEVEL_DEBUG)) {
addLog(LOG_LEVEL_DEBUG, strformat(F("Chime: Process '%c'"), c));
}
# endif // ifndef BUILD_NO_DEBUG
switch (c)
{
case 'a':
case 'b':
case 'c':
case 'd':
case 'e':
case 'f':
case 'A':
case 'B':
case 'C':
case 'D':
case 'E':
case 'F':
c -= 'A' - '0' - 10;
// vvv
case '0': // strikes 1=1st bell, 2=2nd bell, 4=3rd bell, 8=4rd bell
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
{
uint8_t mask = 1;
for (uint8_t i = 0; i < 4; ++i)
{
if (Plugin_055_Data->pin[i] >= 0) {
if (c & mask) {
digitalWrite(Plugin_055_Data->pin[i], !Plugin_055_Data->lowActive);
}
}
mask <<= 1;
}
Plugin_055_Data->millisStateEnd = millisAct + Plugin_055_Data->millisChimeTime;
break;
}
case '=': // long pause
case ' ':
case ',':
Plugin_055_Data->millisStateEnd = millisAct + Plugin_055_Data->millisPauseTime * 3;
break;
case '-': // single pause
Plugin_055_Data->millisStateEnd = millisAct + Plugin_055_Data->millisPauseTime;
break;
case '.': // short pause
Plugin_055_Data->millisStateEnd = millisAct + Plugin_055_Data->millisPauseTime / 3;
break;
case '|': // shortest pause
Plugin_055_Data->millisStateEnd = millisAct + Plugin_055_Data->millisChimeTime / 2;
break;
case '#': // comment -> eat till FIFO is empty
while (Plugin_055_ReadFIFO()) {}
break;
default: // unknown char -> do nothing
break;
}
}
}
success = true;
break;
}
}
return success;
}
// FIFO functions
void Plugin_055_WriteFIFO(char c)
{
if (Plugin_055_Data->FIFO_IndexR == ((Plugin_055_Data->FIFO_IndexW + 1) & PLUGIN_055_FIFO_MASK)) { // FIFO full?
return;
}
Plugin_055_Data->FIFO[Plugin_055_Data->FIFO_IndexW] = c;
Plugin_055_Data->FIFO_IndexW++;
Plugin_055_Data->FIFO_IndexW &= PLUGIN_055_FIFO_MASK;
}
char Plugin_055_ReadFIFO()
{
if (Plugin_055_IsEmptyFIFO()) {
return '\0';
}
char c = Plugin_055_Data->FIFO[Plugin_055_Data->FIFO_IndexR];
Plugin_055_Data->FIFO_IndexR++;
Plugin_055_Data->FIFO_IndexR &= PLUGIN_055_FIFO_MASK;
return c;
}
char Plugin_055_PeekFIFO()
{
if (Plugin_055_IsEmptyFIFO()) {
return '\0';
}
return Plugin_055_Data->FIFO[Plugin_055_Data->FIFO_IndexR];
}
boolean Plugin_055_IsEmptyFIFO()
{
return Plugin_055_Data->FIFO_IndexR == Plugin_055_Data->FIFO_IndexW;
}
void Plugin_055_AddStringFIFO(const String& param)
{
if (param.isEmpty()) {
return;
}
uint8_t i = 0;
char c = param[i];
char c_last = '\0';
while (c != 0)
{
if (isDigit(c) && isDigit(c_last)) { // "11" is shortcut for "1-1" -> add pause
Plugin_055_WriteFIFO('-');
}
if (c == '!') // double strike -> add shortest pause and repeat last strike
{
Plugin_055_WriteFIFO('|');
c = c_last;
}
Plugin_055_WriteFIFO(c);
c_last = c;
c = param[++i];
}
Plugin_055_WriteFIFO('=');
}
// File I/O functions
void Plugin_055_WriteChime(const String& name, const String& tokens)
{
const String fileName = strformat(F("chime_%s.txt"), name.c_str());
String log;
if (loglevelActiveFor(LOG_LEVEL_INFO)) {
log = strformat(F("Chime: write %s "), fileName.c_str());
}
fs::File f = tryOpenFile(fileName, "w");
if (f)
{
f.print(tokens);
f.close();
// flashCount();
if (loglevelActiveFor(LOG_LEVEL_INFO)) {
log += tokens;
}
}
addLogMove(LOG_LEVEL_INFO, log);
}
uint8_t Plugin_055_ReadChime(const String& name, String& tokens)
{
const String fileName = strformat(F("chime_%s.txt"), name.c_str());
String log;
if (loglevelActiveFor(LOG_LEVEL_INFO)) {
log = strformat(F("Chime: read %s "), fileName.c_str());
}
free_string(tokens);
fs::File f = tryOpenFile(fileName, "r");
if (f)
{
tokens.reserve(f.size());
char c;
while (f.available())
{
c = f.read();
tokens += c;
}
f.close();
if (loglevelActiveFor(LOG_LEVEL_INFO)) {
log += tokens;
}
}
addLogMove(LOG_LEVEL_INFO, log);
return tokens.length();
}
#endif // USES_P055