powerpic

Replacement board for a Casio CA-53W

/** @file alarm.c
 * 
 * This library implements configurable alarms.
*/

#include <xc.h>

#include "drivers/rtcc.h"

#include "lib/isr.h"
#include "lib/events.h"

#define LOG_TAG "lib.alarm"
#include "lib/logging.h"

#include "lib/alarm.h"


/**
 * This array holds the alarms that are currently registered.
*/
static datetime_t registered_alarms[ALARM_MAX_ALARMS] = {0};

/** Number of currently registered alarms. */
static volatile unsigned char registered_alarms_count = 0;

/** Index of current registered alarm. */
static volatile unsigned char registered_alarms_head = 0;

/**
 * This inserts an alarm at the given index. Shifts alarms around if needed.
*/
void alarm_insert(unsigned char index, datetime_t *alarm_dt);

/**
 * Delete an alarm event at a given index. Shifts the the alarms up.
*/
void alarm_delete(unsigned char index);

/** Alarm interrupt service routine. */
static void alarm_isr (void);

void
alarm_init (void)
{
    LOG_INFO("Initializing alarm...");
    registered_alarms_count = 0;
    registered_alarms_head = 0;

    // Set alarm mask to match HH:MM:SS
    rtcc_alarm_mask_set(0b0110);

    // Register alarm isr
    isr_register(8, _PIR8_RTCCIF_MASK, &alarm_isr);

    // Enable alarm interrupts
    rtcc_alarm_interrupt_enable();
}

void
alarm_get (datetime_t *alarm_datetime)
{
    *alarm_datetime = registered_alarms[registered_alarms_head];
}

void
alarm_set_time (time_t *alarm_time, unsigned char event_data)
{
    // Check if we have a free spot
    //
    if ((ALARM_MAX_ALARMS > registered_alarms_count))
    {
        // Our datetime object that represents this alarm.
        datetime_t alarm_dt;

        // Set the time of our alarm.
        alarm_dt.time = *alarm_time;

        // Populate the alarm with current date.
        datetime_today(&alarm_dt.date);

        // Figure out true date of alarm.
        // NOTE: This will make all alarms sort correctly, but what happens
        // if today is the last day of the month? We work around this currently
        // by just matching the alarm time and not the date. It has yet to be
        // determined how this effects midnight rollover.
        //
        // A potential bug is when the next alarm to occur is not on the next
        // day.
        //
        // Check to see if time is before right now.
        if (alarm_time->hour <= HOURS)
        {
            if (alarm_time->hour == HOURS)
            {
                if (alarm_time->minute <= MINUTES)
                {
                    if (alarm_time->minute == MINUTES)
                    {
                        if (alarm_time->second <= SECONDS)
                        {
                            if (alarm_time->second == SECONDS)
                            {
                                // ALARM IS RIGHT NOW! PANIC!
                                // The caller probably wants it tomorrow?
                                alarm_dt.date.day += 1;
                                //Subject to change
                            }
                            else
                            {
                                // If the alarm second is less than the current
                                // second, we assume the alarm is for tomorrow.
                                alarm_dt.date.day += 1;
                            }
                        }
                        else
                        {
                            // The alarm is in less than a minute.
                        }
                    }
                    else
                    {
                        // If the alarm minute is less than the current minute, we
                        // assume the alarm is for tomorrow.
                        alarm_dt.date.day += 1;
                    }
                }
                else
                {
                    // The alarm is in less than an hour.
                }
            }
            else
            {
                // If the alarm hour is less than the current hour, we assume the
                // alarm is for tomorrow.
                alarm_dt.date.day += 1;
            }
        }
        else
        {
            // If it is later than the current hour, we assume today.
        }
        
        // Set our datetime
        alarm_set_datetime(&alarm_dt, event_data);
    }
    else
    {
        // Max alarms have been added
        LOG_ERROR("Max alarms added: %.2i:%.2i:%.2i",
            BCD2DEC(alarm_time->hour),
            BCD2DEC(alarm_time->minute),
            BCD2DEC(alarm_time->second)
        );
    }
}

void
alarm_set_datetime (datetime_t *alarm_datetime, unsigned char event_data)
{
    // Check if we have a free spot
    //
    if ((ALARM_MAX_ALARMS > registered_alarms_count))
    {
        // TODO: Verify alarm is within bounds of time:
        // - Hour     0-23
        // - Minute   0-59
        // - Seconds  0-59

        // We store the event data in the weekday field
        alarm_datetime->date.weekday = event_data;

        int alarm_index = -1;
        unsigned char alarm_position = 0;
        
        // Loop through the list of registered alarms and compare.
        // Right now we only compare up to the day, so monthly alarms might now
        // work correctly.
        for (unsigned char c = 0; c < registered_alarms_count; c++)
        {
            // Loop from head to tail based on the head and the length
            unsigned char i = (registered_alarms_head + c) % ALARM_MAX_ALARMS;
            alarm_position = c;

            if (alarm_datetime->date.day < registered_alarms[i].date.day)
            {
                // This is our alarm index
                alarm_index = i;
                break;
            }
            else if (alarm_datetime->date.day == registered_alarms[i].date.day)
            {
                // If the day is the same, compare the times.

                if (alarm_datetime->time.hour < registered_alarms[i].time.hour)
                {
                    // This is our alarm index
                    alarm_index = i;
                    break;
                }
                else if (alarm_datetime->time.hour == registered_alarms[i].time.hour)
                {
                    // If the hour is the same, compare the minute.

                    if (alarm_datetime->time.minute < registered_alarms[i].time.minute)
                    {
                        // This is our alarm index
                        alarm_index = i;
                        break;
                    }
                    else if (alarm_datetime->time.minute == registered_alarms[i].time.minute)
                    {
                        // If the minute is the same, compare the seconds.

                        if (alarm_datetime->time.second < registered_alarms[i].time.second)
                        {
                            // This is our alarm index
                            alarm_index = i;
                            break;
                        }
                        else if (alarm_datetime->time.second == registered_alarms[i].time.second)
                        {
                            // An alarm with that time is already registered.
                            // Check if this is the last registered alarm.
                            if ((c+1) == registered_alarms_count)
                            {
                                LOG_WARNING("Duplicate alarm added: %.2i:%.2i:%.2i",
                                    BCD2DEC(alarm_datetime->time.hour),
                                    BCD2DEC(alarm_datetime->time.minute),
                                    BCD2DEC(alarm_datetime->time.second)
                                );
                                // This is our alarm index
                                alarm_index = i+1;
                                alarm_position++;
                                break;
                            }
                        }
                        else
                        {
                            // Alarm is after this index

                            // Check if this is the last registered alarm.
                            if ((c+1) == registered_alarms_count)
                            {
                                // This is our alarm index
                                alarm_index = i+1;
                                alarm_position++;
                                break;
                            }
                        }   
                    }
                    else
                    {
                        // Alarm is after this index

                        // Check if this is the last registered alarm.
                        if ((c+1) == registered_alarms_count)
                        {
                            // This is our alarm index
                            alarm_index = i+1;
                            alarm_position++;
                            break;
                        }
                    }
                }
                else
                {
                    // Alarm is after this index

                    // Check if this is the last registered alarm.
                    if ((c+1) == registered_alarms_count)
                    {
                        // This is our alarm index
                        alarm_index = i+1;
                        alarm_position++;
                        break;
                    }
                }
            }
            else
            {
                // Alarm is after this index

                // Check if this is the last registered alarm.
                if ((c+1) == registered_alarms_count)
                {
                    // This is our alarm index
                    alarm_index = i+1;
                    alarm_position++;
                    break;
                }
            }
        }

        // Make sure our alarm index has been set in the spaghetti above.
        if (-1 == alarm_index)
        {
            // Check if we have any alarms registered
            if (0 == registered_alarms_count)
            {
                // Alarm is the only registered alarm
                alarm_index = registered_alarms_head;
            }
            else
            {
                // We should never get here
                LOG_ERROR("Couldn't register alarm!");
                return;
            }
        }

        // Handle tail wrapping
        alarm_index = alarm_index % ALARM_MAX_ALARMS;

        // Insert our alarm into the list.
        alarm_insert((unsigned char)alarm_index, alarm_datetime);

        // rtcc_alarm_interrupt_enable();
        // LOG_DEBUG("ALARM: %x %x %x", ALRMCON, ALRMRPT, PIE8);

        LOG_INFO("Registered alarm [%i] (%i/%i) %.2i:%.2i:%.2i %.2i/%.2i x%.2X%.2X",
            alarm_index,
            alarm_position+1,
            registered_alarms_count,
            BCD2DEC(alarm_datetime->time.hour),
            BCD2DEC(alarm_datetime->time.minute),
            BCD2DEC(alarm_datetime->time.second),
            BCD2DEC(alarm_datetime->date.month),
            BCD2DEC(alarm_datetime->date.day),
            alarm_datetime->date.weekday,
            ALARM_EVENT
        );
    }
    else
    {
        // Max alarms have been added
        LOG_ERROR("Max alarms added: %.2i:%.2i:%.2i",
            BCD2DEC(alarm_datetime->time.hour),
            BCD2DEC(alarm_datetime->time.minute),
            BCD2DEC(alarm_datetime->time.second)
        );
    }
}

unsigned char
alarm_del_event (unsigned char event_data)
{
    unsigned char deleted_alarms = 0;

    // Loop through the registered_alarm list and remove alarms that have the
    // specified event_data. The event_data is stored in the weekday field.

    for (unsigned char c = 0; c < registered_alarms_count; c++)
    {
        // Loop from head to tail based on the head and the length
        unsigned char i = (registered_alarms_head + c) % ALARM_MAX_ALARMS;

        if (event_data == registered_alarms[i].date.weekday)
        {
            LOG_DEBUG("Removing alarm: (x%.2X) %.2i:%.2i",
                registered_alarms[i].date.weekday,
                BCD2DEC(registered_alarms[i].time.hour),
                BCD2DEC(registered_alarms[i].time.minute)
            );

            // Remove alarm
            alarm_delete(i);
            deleted_alarms++;

            // Resume search from previous index
            c--;
        }
    }

    return deleted_alarms;
}

void
alarm_delete (unsigned char index)
{
    // Bail early if the index is not in the range of registered alarms.
    if (!(((index >= registered_alarms_head) && (index-registered_alarms_head <= registered_alarms_count)) ||
        ((index < registered_alarms_head) && (index < (registered_alarms_head+registered_alarms_count)%ALARM_MAX_ALARMS))))
        {
            LOG_ERROR("Alarm is out of bounds!");
            return;
        }

    // Shift up all alarms that are:
    // 1) Behind the head
    // 2) Behind the index
    // 3) Before the end of the buffer (no wrapping)
    //
    //    head v   v index         index v     v head
    // 0 1 2 3 4 5 6 7 8 9           0 1 2 3 4 5 6 7 8 9
    //    shift left ^ ^ ^

    if (index >= registered_alarms_head)
    {
        for (unsigned char i = index+1; i < ALARM_MAX_ALARMS; i++)
        {
            // Shift each time and date to the previous position
            registered_alarms[i-1].time = registered_alarms[i].time;
            registered_alarms[i-1].date = registered_alarms[i].date;
        }
    }


    // Next, handle the wrapping of the circular buffer if needed
    //
    //    head v   v index         index v     v head
    // 0 1 2 3 4 5 6 7 8 9           0 1 2 3 4 5 6 7 8 9
    // > (if count > 6)  ^

    if ((index >= registered_alarms_head) && (registered_alarms_head+registered_alarms_count > ALARM_MAX_ALARMS))
    {
        // Shift index 0 to the last index
        registered_alarms[ALARM_MAX_ALARMS-1].time = registered_alarms[0].time;
        registered_alarms[ALARM_MAX_ALARMS-1].date = registered_alarms[0].date;

        // Set index to 0
        // This will shift all alarms from index 1 until the tail in the next loop
        index = 0;
    }


    // Finally, shift the alarms at the beginning (end):
    // 1) Before the head
    // 2) Before the tail
    // 3) Behind the index
    //
    //    head v   v index         index v     v head
    // 0 1 2 3 4 5 6 7 8 9           0 1 2 3 4 5 6 7 8 9
    //   ^ ^ ^ shift left                  ^ ^ shift left

    if (registered_alarms_head+registered_alarms_count > ALARM_MAX_ALARMS)
    {
        for (unsigned char i = index+1; i < (registered_alarms_head+registered_alarms_count)%ALARM_MAX_ALARMS; i++)
        {
            // Shift each time and date to the previous position
            registered_alarms[i-1].time = registered_alarms[i].time;
            registered_alarms[i-1].date = registered_alarms[i].date;
        }
    }

    // Decrement alarm count
    registered_alarms_count--;

}

void
alarm_insert (unsigned char index, datetime_t *alarm_dt)
{
    // Shift down all alarms that are:
    // 1) Before the head
    // 2) Behind the index (in order)
    // 3) Before the tail
    //
    //    head v   v index         index v     v head
    // 0 1 2 3 4 5 6 7 8 9           0 1 2 3 4 5 6 7 8 9
    // ^ ^ ^ right shift                 ^ ^ right shift

    if (registered_alarms_head+registered_alarms_count > ALARM_MAX_ALARMS) // Check if the buffer wraps around
    {
        unsigned char start;
        if (index >= registered_alarms_head)
        {
            start = 0; // If index is behind the head, shift all of the alarms
        } else {
            start = index; // Else shift until the index
        }
        for (unsigned char i = (registered_alarms_head+registered_alarms_count)%ALARM_MAX_ALARMS;i > start; i--)
        {
            // Shift each time and date to the following position
            registered_alarms[i].time = registered_alarms[i-1].time;
            registered_alarms[i].date = registered_alarms[i-1].date;
        }
    }


    // Next, handle the wrapping of the circular buffer if needed
    //
    //    head v   v index         index v     v head
    // 0 1 2 3 4 5 6 7 8 9           0 1 2 3 4 5 6 7 8 9
    // ^ (if count > 6) <<

    if ((index >= registered_alarms_head) && (registered_alarms_head+registered_alarms_count > ALARM_MAX_ALARMS))
    {
        // Shift the last index to index 0
        registered_alarms[0].time = registered_alarms[ALARM_MAX_ALARMS-1].time;
        registered_alarms[0].date = registered_alarms[ALARM_MAX_ALARMS-1].date;
    }


    // Finally, shift the alarms at the end (beginning):
    // 1) After the head
    // 2) Before the end of the buffer
    // 3) Behind the index
    //
    //    head v   v index         index v     v head
    // 0 1 2 3 4 5 6 7 8 9           0 1 2 3 4 5 6 7 8 9
    // shift right ^ ^ ^

    if (index >= registered_alarms_head)
    {
        for (unsigned char i = ALARM_MAX_ALARMS-1; i > index; i--)
        {
            // Shift each time and date to the following position
            registered_alarms[i].time = registered_alarms[i-1].time;
            registered_alarms[i].date = registered_alarms[i-1].date;
        }
    }

    // After shift the alarms around we can insert it at the position
    registered_alarms[index] = *alarm_dt;

    // Increment alarm count
    registered_alarms_count++;

    // If the alarm is going to happen the soonest (head), then set the
    // alarm registers.
    if (index == registered_alarms_head)
    {
        // LOG_DEBUG("Setting alarm HEAD");

        // Enable RTCC writes (for alarm enable bit)
        rtcc_writes_enable();

        // Disable alarm to change registers
        rtcc_alarm_disable();

        // Set Registers
        // ALRMDAY = registered_alarms[0].date.day;
        ALRMHR  = registered_alarms[index].time.hour;
        ALRMMIN = registered_alarms[index].time.minute;
        if (0 == (registered_alarms[index].time.second & 0x0F))
        {
            // if (0 == registered_alarms[index].time.second)
            // {
            //     ALRMSEC = 0x5A;
            //     ALRMMIN = DEC2BCD(BCD2DEC(registered_alarms[index].time.minute) - 1);
            // }
            // else
            // {
            //     ALRMSEC = (((registered_alarms[index].time.second >> 4) - 1) << 4 | 0xA);
            // }
            ALRMSEC = registered_alarms[index].time.second + 1;
        }
        else
        {
            ALRMSEC = registered_alarms[index].time.second;
        }

        // Enable alarm
        rtcc_alarm_enable();

        // Disable writes
        rtcc_writes_disable();
    }
}


static void
alarm_isr (void)
{
    // Alarm at head has occurred. alarm is disabled.
    // rtcc_alarm_disable();

    unsigned char consumed_alarms = 1;
    // Emit event of alarm that triggered interrupt
    event_isr((unsigned)
        EVENT_ID(
            ALARM_EVENT,
            registered_alarms[registered_alarms_head].date.weekday)
    );
    // Match all alarms and emit events
    for (unsigned char c = 1; c < registered_alarms_count; c++)
    {
        // Loop from head to tail based on the head and the length
        unsigned char i = (registered_alarms_head + c) % ALARM_MAX_ALARMS;

        if (registered_alarms[registered_alarms_head].time.second >= registered_alarms[i].time.second)
        {
            if (registered_alarms[registered_alarms_head].time.minute >= registered_alarms[i].time.minute)
            {
                if (registered_alarms[registered_alarms_head].time.hour >= registered_alarms[i].time.hour)
                {
                    if (registered_alarms[registered_alarms_head].date.day >= registered_alarms[i].date.day)
                    {
                        if (registered_alarms[registered_alarms_head].date.month >= registered_alarms[i].date.month)
                        {
                            if (registered_alarms[registered_alarms_head].date.year >= registered_alarms[i].date.year)
                            {
                                // Alarm matches
                                consumed_alarms++;
                                // Emit event
                                event_isr((unsigned)
                                    EVENT_ID(
                                        ALARM_EVENT,
                                        registered_alarms[i].date.weekday)
                                );
                            } else {
                                break;
                            }
                        } else {
                            break;
                        }
                    } else {
                        break;
                    }
                } else {
                    break;
                }
            } else {
                break;
            }
        } else {
            break;
        }
    }

    // Consume alarm(s)
    registered_alarms_head = (registered_alarms_head + consumed_alarms) % ALARM_MAX_ALARMS;

    // Decrement count of registered alarms
    registered_alarms_count -= consumed_alarms;

    // Check if we have any alarms registered
    if (registered_alarms_count)
    {
        // Set alarm registers with head values.
        ALRMHR  = registered_alarms[registered_alarms_head].time.hour;
        ALRMMIN = registered_alarms[registered_alarms_head].time.minute;
        if (0 == (registered_alarms[registered_alarms_head].time.second & 0x0F))
        {
            // if (0 == registered_alarms[registered_alarms_head].time.second)
            // {
            //     ALRMSEC = 0x5A;
            //     ALRMMIN = DEC2BCD(BCD2DEC(registered_alarms[registered_alarms_head].time.minute) - 1);
            // }
            // else
            // {
            //     ALRMSEC = (((registered_alarms[registered_alarms_head].time.second >> 4) - 1) << 4 | 0xA);
            //     // ALRMSEC = ((registered_alarms[registered_alarms_head].time.second & 0xF0) | 0xA);
            // }
            ALRMSEC = registered_alarms[registered_alarms_head].time.second + 1;
        }
        else
        {
            ALRMSEC = registered_alarms[registered_alarms_head].time.second;
        }
        
        // Enable alarm (write enable required)
        rtcc_writes_enable();
        rtcc_alarm_enable();
        rtcc_writes_disable();
    }

    // Clear alarm interrupt flag
    rtcc_alarm_interrupt_clear();
}


// EOF //