Add time parser and command to update the shutter schedule
[wk-misc.git] / ebus / shutter.c
1 /* shutter.c - Elektor Bus node to control a shutter
2  * Copyright (C) 2011 g10 Code GmbH
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; either version 3 of the License, or
7  * (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, see <http://www.gnu.org/licenses/>.
16  */
17
18 /* This node is used to control the shutter in a living room.  The
19    shutter is operator by a motor with two coils and limit switches
20    connected to a solid state relays which is controlled by this node.
21    A future version of this controller will also support some sensor
22    to allow pulling down the shutter only to some percentage.  In any
23    case the limit switches of the motor control the endpoints.  The
24    control logic takes down the relays after the time required to pull
25    the shutters up or down plus some extra time.  The S2 and S3
26    switches are used for manually controlling the shutter.  They are
27    interlocked and operate in a toggle on/off way.  The hardware
28    itself is also interlocked so that it is not possible to drive both
29    motors at the same time.
30
31    The used relays are FINDER 34.51.7.012.0010 which feature a high
32    sensitive coil (~18mA) and are able to switch 6A (the datasheet
33    even exlicitly allows motors of up to 185W).  The first relay is
34    used for direction selection with NC connected to the pull-up motor
35    and NO to the pull-down motor.  Our control logic makes sure that
36    this relay is not switched under load.  The second one connects its
37    NO to the first relay and a snubber circuit is used to protect its
38    contacts.  They are both connected to the 12V rail and flyback
39    diodes are used for each.  Two BC547 are used to drive them.
40
41    Historical note: A first version of the shutter rig and this
42    software used two (not interlocked) solid state relays made up from
43    S202S02 opto-triacs along with snubbers and varistors to cope with
44    the voltage peaks induced by the other motor.  However the spikes
45    were obviously too high and after some weeks the triacs bricked
46    themself.  A second try using S202S01s had the same effect.  Better
47    save your money and use mechanical relays.
48  */
49
50 #include "hardware.h"
51
52 #include <stdio.h>
53 #include <stdlib.h>
54 #include <string.h>
55 #include <avr/io.h>
56 #include <avr/interrupt.h>
57 #include <avr/sleep.h>
58 #include <avr/eeprom.h>
59
60 #include "ebus.h"
61 #include "proto-busctl.h"
62 #include "proto-h61.h"
63
64
65 #define MOTOR_on       (PORTC)  /* Switch motor on.  */
66 #define MOTOR_on_BIT   (PC2)
67 #define MOTOR_down     (PORTC)  /* Switch direction relay to down.  */
68 #define MOTOR_down_BIT (PC3)
69
70
71 #define SCHEDULE_ACTION_NOP   0 /* No operation.  */
72 #define SCHEDULE_ACTION_UP    1 /* Minute + 10s := pull up  */
73 #define SCHEDULE_ACTION_DOWN  5 /* Minute + 50s := pull down  */
74
75
76 /* Allowed action values.  Note that the hardware interlocks both
77    motors.  However to avoid switch the direction under load we first
78    set the direction, wait a moment and then switch the motor on.  */
79 enum __attribute__ ((packed)) action_values {
80   action_none = 0,
81   action_up,
82   action_down
83 };
84
85 enum __attribute__ ((packed)) motor_state_values {
86   motor_state_off = 0,
87   motor_state_pre_off,
88   motor_state_pre_off2,
89   motor_state_pre_up,
90   motor_state_pre_up2,
91   motor_state_up,
92   motor_state_up_ready,
93   motor_state_pre_down,
94   motor_state_pre_down2,
95   motor_state_down,
96   motor_state_down_ready
97 };
98
99 /* Remember the last action time.  */
100 static uint16_t schedule_last_tfound;
101
102 /* The next action to do.  It's value is changed by the commands
103    and a ticker is switching the motor depending on these values.  */
104 static volatile enum action_values next_action;
105
106
107 /* The shutter state byte.  This is directly used for the
108    QueryShutterState response message.  */
109 static volatile byte shutter_state;
110
111 /* Event flag, triggred (surprise) every 10 seconds.  */
112 static volatile byte ten_seconds_event;
113
114
115 \f
116 /* Local prototypes.  */
117 static void init_eeprom (byte force);
118
119
120
121 \f
122 /* This code is called by the 1ms ticker interrupt service routine
123    with the current clock value given in milliseconds from 0..9999. */
124 void
125 ticker_bottom (unsigned int clock)
126 {
127   static volatile enum action_values current_action;
128   static volatile uint16_t action_delay;
129   static volatile enum motor_state_values state = motor_state_off;
130   enum action_values save_action;
131
132   /* Blink the activity LED if the motor is not in off state.  */
133   if (state != motor_state_off)
134     {
135       if (!(clock % 1000))
136         {
137           if ((LED_Collision & _BV(LED_Collision_BIT)))
138             LED_Collision &= ~_BV (LED_Collision_BIT);
139           else
140             LED_Collision |= _BV(LED_Collision_BIT);
141         }
142     }
143
144   /* Get next action.  */
145   save_action = current_action;
146   if (read_key_s2 ())
147     current_action = current_action != action_down ? action_down : action_none;
148   if (read_key_s3 ())
149     current_action = current_action != action_up? action_up : action_none;
150   if (current_action != save_action || next_action)
151     {
152       if (next_action)
153         {
154           current_action = next_action;
155           next_action = 0;
156         }
157       switch (current_action)
158         {
159         case action_none: state = motor_state_pre_off; break;
160         case action_up:   state = motor_state_pre_up; break;
161         case action_down: state = motor_state_pre_down; break;
162         }
163       /* When a new action has been selected we need to limit the
164          action delay to the minimum value so that we don't stick in
165          the long down or up states.  We also need to make sure that
166          there is an action delay so that we enter the state
167          transition switch. */
168       if (!action_delay)
169         action_delay = 1;
170       else if (action_delay > 200)
171         action_delay = 200;
172     }
173
174   if (action_delay && !--action_delay)
175     {
176       switch (state)
177         {
178         case motor_state_off:
179           LED_Collision &= ~_BV (LED_Collision_BIT); /* Clear LED.  */
180           /* Make sure the motor flags are cleared in the state info.  */
181           shutter_state &= 0b00111111;
182           break;
183
184         label_pre_off:
185         case motor_state_pre_off:
186           MOTOR_on &= ~_BV(MOTOR_on_BIT);    /* Switch motor off. */
187           action_delay = 200; /*ms*/
188           state = motor_state_pre_off2;
189           break;
190         case motor_state_pre_off2:
191           MOTOR_down &= ~_BV(MOTOR_down_BIT);/* Switch direction relay off. */
192           action_delay = 200; /*ms*/
193           state = motor_state_off;
194           break;
195
196         case motor_state_pre_up:
197           MOTOR_on &= ~_BV(MOTOR_on_BIT);    /* Switch motor off. */
198           action_delay = 200; /*ms*/
199           state = motor_state_pre_up2;
200           break;
201         case motor_state_pre_up2:
202           MOTOR_down &= ~_BV(MOTOR_down_BIT);/* Switch direction relay off. */
203           action_delay = 200; /*ms*/
204           state = motor_state_up;
205           break;
206         case motor_state_up:
207           MOTOR_on |= _BV(MOTOR_on_BIT);     /* Switch motor on. */
208           shutter_state = 0b11000000;
209           /*                |!------- Direction set to up
210            *                +-------- Motor running.        */
211           action_delay = 25000; /*ms*/
212           state = motor_state_up_ready;
213           break;
214         case motor_state_up_ready:
215           shutter_state = 0b00100000;
216           /*                  | !~~!- Value: 0 = 0% closed
217            *                  +------ State in bits 3..0 is valid.  */
218           goto label_pre_off;
219
220         case motor_state_pre_down:
221           MOTOR_on &= ~_BV(MOTOR_on_BIT);    /* Switch motor off. */
222           action_delay = 200; /*ms*/
223           state = motor_state_pre_down2;
224           break;
225         case motor_state_pre_down2:
226           MOTOR_down |= _BV(MOTOR_down_BIT); /* Switch direction relay on. */
227           action_delay = 200; /*ms*/
228           state = motor_state_down;
229           break;
230         case motor_state_down:
231           MOTOR_on |= _BV(MOTOR_on_BIT);     /* Switch motor on. */
232           shutter_state = 0b10000000;
233           /*                |!------- Direction set to down
234            *                +-------- Motor running.        */
235           action_delay = 25000; /*ms*/
236           state = motor_state_down_ready;
237           break;
238         case motor_state_down_ready:
239           shutter_state = 0b00101111;
240           /*                  | !~~!--- Value: 15 = 100% closed
241            *                  +-------- State in bits 3..0 is valid.  */
242           goto label_pre_off;
243         }
244     }
245
246   if (!clock)
247     {
248       ten_seconds_event = 1;
249       wakeup_main = 1;
250     }
251 }
252
253
254 /* static void */
255 /* send_dbgmsg (const char *string) */
256 /* { */
257 /*   byte msg[16]; */
258
259 /*   msg[0] = PROTOCOL_EBUS_DBGMSG; */
260 /*   msg[1] = config.nodeid_hi; */
261 /*   msg[2] = config.nodeid_lo; */
262 /*   memset (msg+3, 0, 13); */
263 /*   strncpy (msg+3, string, 13); */
264 /*   csma_send_message (msg, 16); */
265 /* } */
266
267 /* static void */
268 /* send_dbgmsg_fmt (const char *format, ...) */
269 /* { */
270 /*   va_list arg_ptr; */
271 /*   char buffer[16]; */
272
273 /*   va_start (arg_ptr, format); */
274 /*   vsnprintf (buffer, 16, format, arg_ptr); */
275 /*   va_end (arg_ptr); */
276 /*   send_dbgmsg (buffer); */
277 /* } */
278
279
280 /* Process scheduled actions.  TIME is the current time.  If
281    FORCED_TLOW is not 0 the scheduler will run the last action between
282    FORCED_TLOW and TIME regardless on whether it has already been run.
283    This feature is required to cope with a changed system time:
284    Consider the shutter shall be closed at 19:00, the current system
285    time is 18:59 and the system time is updated to 19:10 - without
286    that feature the closing at 19:00 would get lost.  */
287 static void
288 process_schedule (uint16_t time, uint16_t forced_tlow)
289 {
290   uint16_t tlow, thigh, t, tfound;
291   byte i;
292
293   if (!time_has_been_set)
294     return; /* Don't schedule while running without a valid clock.  */
295
296   if (schedule_last_tfound > time || forced_tlow)
297     schedule_last_tfound = 0;  /* Time wrapped to the next week or forced
298                                   schedule action.  */
299
300   /* We look up to 5 minutes back into the past to cope with lost events.  */
301   time /= 6;
302   time *= 6;
303   tlow = forced_tlow? forced_tlow : time;
304   if (tlow >= 5 * 6)
305     tlow -= 5 * 6;
306   else
307     tlow = 0;
308   if (schedule_last_tfound && schedule_last_tfound > tlow)
309     tlow = schedule_last_tfound;
310   thigh = time + 5;
311
312   /* send_dbgmsg_fmt ("time=%u", time); */
313   /* send_dbgmsg_fmt ("lst=%u", schedule_last_tfound); */
314   /* send_dbgmsg_fmt ("low=%u", tlow); */
315   /* send_dbgmsg_fmt ("hig=%u", thigh); */
316
317   /* Walk the schedule and find the last entry. */
318   for (tfound=0, i=0; i < DIM (ee_data.u.shutterctl.schedule); i++)
319     {
320       t = eeprom_read_word (&ee_data.u.shutterctl.schedule[i]);
321       if (!t)
322         break;
323       if (t > tlow && t <= thigh)
324         tfound = t;
325     }
326   if (tfound)
327     {
328       schedule_last_tfound = tfound;
329       /* send_dbgmsg_fmt ("fnd=%u", ((tfound/6)*6)); */
330       tfound %= 6;
331       /* send_dbgmsg_fmt ("act=%u", tfound); */
332       if (tfound == SCHEDULE_ACTION_UP)
333         next_action = action_up;
334       else if (tfound == SCHEDULE_ACTION_DOWN)
335         next_action = action_down;
336     }
337 }
338
339
340 /* Process a shutter command.  */
341 static void
342 process_shutter_cmd (byte *msg)
343 {
344   uint16_t val16;
345   byte err = 0;
346
347   switch (msg[6])
348     {
349     case P_H61_SHUTTER_QUERY:
350       {
351         msg[1] = msg[3];
352         msg[2] = msg[4];
353         msg[3] = config.nodeid_hi;
354         msg[4] = config.nodeid_lo;
355         msg[5] |= P_BUSCTL_RESPMASK;
356
357         msg[7] = 0; /* No error.  */
358         msg[8] = shutter_state;
359         memset (msg+9, 0, 7);
360         csma_send_message (msg, MSGSIZE);
361       }
362       break;
363
364     case P_H61_SHUTTER_DRIVE:
365       {
366         if (msg[7] > 1 /* Only all shutters or shutter 1 are allowed.  */
367             || msg[9] || msg[10] || msg[11] || msg[12] || msg[13]
368             || msg[14] || msg[15] /* Reserved bytes are not zero.  */
369             || (msg[8] & 0x10)    /* Reserved bit is set.  */
370             || (msg[8] & 0x20)    /* Not yet supported.  */ )
371           err = 1;
372         else if ((msg[8] & 0xc0) == 0xc0)
373           next_action = action_up;
374         else if ((msg[8] & 0xc0) == 0x80)
375           next_action = action_down;
376         else
377           err = 1;
378
379         msg[1] = msg[3];
380         msg[2] = msg[4];
381         msg[3] = config.nodeid_hi;
382         msg[4] = config.nodeid_lo;
383         msg[5] |= P_BUSCTL_RESPMASK;
384         msg[7] = err;
385         msg[8] = shutter_state;
386         memset (msg+9, 0, 7);
387         csma_send_message (msg, MSGSIZE);
388       }
389       break;
390
391     case P_H61_SHUTTER_QRY_TIMINGS:
392       break;
393
394     case P_H61_SHUTTER_UPD_TIMINGS:
395       break;
396
397     case P_H61_SHUTTER_QRY_SCHEDULE:
398       {
399         byte i;
400
401         msg[1] = msg[3];
402         msg[2] = msg[4];
403         msg[3] = config.nodeid_hi;
404         msg[4] = config.nodeid_lo;
405         msg[5] |= P_BUSCTL_RESPMASK;
406         msg[7] = 0; /* We only have a global schedule for now.  */
407         msg[8] = 0; /* No error.  */
408         for (i=0; i < DIM (ee_data.u.shutterctl.schedule); i++)
409           {
410             val16 = eeprom_read_word (&ee_data.u.shutterctl.schedule[i]);
411             switch ((val16 % 6))
412               {
413               case SCHEDULE_ACTION_UP:   msg[13] = 0b11000000; break;
414               case SCHEDULE_ACTION_DOWN: msg[13] = 0b10000000; break;
415               default: msg[13] = 0;  break; /* Undefined.  */
416               }
417             val16 /= 6;
418             val16 *= 6;
419             msg[9] = DIM (ee_data.u.shutterctl.schedule);
420             msg[10] = i;
421             msg[11] = val16 >> 8;
422             msg[12] = val16;
423             /* msg[13] already set.  */
424             msg[14] = 0;
425             msg[15] = 0;
426             csma_send_message (msg, MSGSIZE);
427           }
428       }
429       break;
430
431     case P_H61_SHUTTER_UPD_SCHEDULE:
432       if (msg[8] || msg[14] || msg[15] || msg[9] != 1
433           || msg[10] >= DIM (ee_data.u.shutterctl.schedule))
434         {
435           /* Bad message or eeprom reset  */
436           if (msg[7] == 0xf0 && msg[9] == 16 && msg[10] == 0xf0
437               && msg[11] == 0xf0 && msg[12] == 0xf0 && msg[13] == 0xf0)
438             {
439               init_eeprom (1);
440             }
441         }
442       else
443         {
444           /* Get time and round down to the full minute.  */
445           val16 = (msg[11] << 8) | msg[12];
446           val16 /= 6;
447           val16 *= 6;
448           /* Include the action.  Note that SCHEDULE_ACTION_NOP is the
449              default.  */
450           if (msg[13] == 0b11000000)
451             val16 += SCHEDULE_ACTION_UP;
452           else if (msg[13] == 0b10000000)
453             val16 += SCHEDULE_ACTION_DOWN;
454
455           eeprom_write_word (&ee_data.u.shutterctl.schedule[msg[10]], val16);
456         }
457       break;
458
459     default:
460       break;
461     }
462 }
463
464
465
466 /* Process a sensor command.  */
467 static void
468 process_sensor_cmd (byte *msg)
469 {
470   uint16_t val16;
471   byte err = 0;
472
473   switch (msg[6])
474     {
475     case P_H61_SENSOR_TEMPERATURE:
476       {
477         msg[1] = msg[3];
478         msg[2] = msg[4];
479         msg[3] = config.nodeid_hi;
480         msg[4] = config.nodeid_lo;
481         msg[5] |= P_BUSCTL_RESPMASK;
482         msg[7] = (1 << 4 | 1); /* Group 1 of 1.  */
483         msg[8] = 0;
484         msg[9] = 0;
485         msg[10] = 0x80; /* No sensor.  */
486         msg[11] = 0;
487         msg[12] = 0x80;
488         msg[13] = 0;
489         msg[14] = 0x80;
490         msg[15] = 0;
491         memset (msg+10, 0, 6);
492         csma_send_message (msg, MSGSIZE);
493       }
494       break;
495
496     default:
497       break;
498     }
499 }
500
501
502 /* A new message has been received and we must now parse the message
503    quickly and see what to do.  We need to return as soon as possible,
504    so that the caller may re-enable the receiver.  */
505 static void
506 process_ebus_h61 (byte *msg)
507 {
508   char is_response = !!(msg[5] & P_H61_RESPMASK);
509
510   if (!(msg[1] == config.nodeid_hi || msg[2] == config.nodeid_lo))
511     return; /* Not addressed to us.  */
512
513   switch ((msg[5] & ~P_H61_RESPMASK))
514     {
515     case P_H61_SHUTTER:
516       if (!is_response)
517         process_shutter_cmd (msg);
518       break;
519
520     case P_H61_SENSOR:
521       if (!is_response)
522         process_sensor_cmd (msg);
523       break;
524
525     default:
526       break;
527     }
528 }
529
530
531 /* Process busctl messages.  */
532 static void
533 process_ebus_busctl (byte *msg)
534 {
535   uint16_t val16;
536   byte     val8;
537   char is_response = !!(msg[5] & P_BUSCTL_RESPMASK);
538
539   if (is_response)
540     return;  /* Nothing to do.  */
541   else if (msg[3] == 0xff || msg[4] == 0xff || msg[4] == 0)
542     return ; /* Bad sender address.  */
543   else if (msg[1] == config.nodeid_hi && msg[2] == config.nodeid_lo)
544     ; /* Directed to us.  */
545   else if ((msg[1] == config.nodeid_hi || msg[1] == 0xff) && msg[2] == 0xff)
546     ; /* Broadcast. */
547   else
548     return; /* Not addressed to us.  */
549
550   switch ((msg[5] & ~P_BUSCTL_RESPMASK))
551     {
552     case P_BUSCTL_TIME:
553       {
554         uint16_t t;
555
556         t = get_current_time ();
557         val16 = (msg[7] << 8) | msg[8];
558         val8  = (msg[6] & 0x02)? msg[9] : 0;
559         set_current_fulltime (val16, val8);
560         if (val16 > t)
561           process_schedule (val16, t);
562       }
563       break;
564
565     case P_BUSCTL_QRY_TIME:
566       msg[1] = msg[3];
567       msg[2] = msg[4];
568       msg[3] = config.nodeid_hi;
569       msg[4] = config.nodeid_lo;
570       msg[5] |= P_BUSCTL_RESPMASK;
571       msg[6] = 0; /* fixme: return an error for unknown shutter numbers.  */
572       val16 = get_current_fulltime (&val8);
573       msg[7] = val16 >> 8;
574       msg[8] = val16;
575       msg[9] = val8;
576       memset (msg+10, 0, 6);
577       csma_send_message (msg, MSGSIZE);
578       break;
579
580     case P_BUSCTL_QRY_VERSION:
581       msg[1] = msg[3];
582       msg[2] = msg[4];
583       msg[3] = config.nodeid_hi;
584       msg[4] = config.nodeid_lo;
585       msg[5] |= P_BUSCTL_RESPMASK;
586       msg[6] = eeprom_read_byte (&ee_data.nodetype);
587       msg[7] = 0;
588       memcpy_P (msg+8, PSTR (GIT_REVISION), 7);
589       msg[15] = 0;
590       csma_send_message (msg, MSGSIZE);
591       break;
592
593     default:
594       break;
595     }
596 }
597
598
599
600 /* Init our eeprom data if needed. */
601 static void
602 init_eeprom (byte force)
603 {
604   uint16_t uptime, downtime;
605   byte i;
606
607   if (force || !eeprom_read_word (&ee_data.u.shutterctl.schedule[0]))
608     {
609       /* The schedule is empty - set up reasonable values for every
610          day of the week.  */
611       uptime = (7 * 60 + 30) * 6 + SCHEDULE_ACTION_UP;
612       downtime = (18 * 60 + 15) * 6 + SCHEDULE_ACTION_DOWN;
613       for (i=0; i < 7*2 && i < DIM (ee_data.u.shutterctl.schedule); i++)
614         {
615           if (i==6*2) /* Pull up on Sundays one hour later.  */
616             uptime += 60 * 6;
617           eeprom_write_word (&ee_data.u.shutterctl.schedule[i], uptime);
618           i++;
619           eeprom_write_word (&ee_data.u.shutterctl.schedule[i], downtime);
620           uptime += 24 * 60 * 6;
621           downtime += 24 * 60 * 6;
622         }
623       for (; i < DIM (ee_data.u.shutterctl.schedule); i++)
624         eeprom_write_word (&ee_data.u.shutterctl.schedule[i], 0);
625     }
626 }
627
628
629 /*
630     Entry point
631  */
632 int
633 main (void)
634 {
635   byte *msg;
636
637   hardware_setup (NODETYPE_SHUTTER);
638   init_eeprom (0);
639
640   /* Port C configuration changes.  Configure motor ports for output
641      and switch them off. */
642   PORTC &= ~(_BV(MOTOR_down_BIT) | _BV(MOTOR_on_BIT));
643   DDRC  |= _BV(MOTOR_down_BIT) | _BV(MOTOR_on_BIT);
644
645   csma_setup ();
646   onewire_setup ();
647
648   sei (); /* Enable interrupts.  */
649
650   for (;;)
651     {
652       set_sleep_mode (SLEEP_MODE_IDLE);
653       while (!wakeup_main)
654         {
655           cli();
656           if (!wakeup_main)
657             {
658               sleep_enable ();
659               sei ();
660               sleep_cpu ();
661               sleep_disable ();
662             }
663           sei ();
664         }
665       wakeup_main = 0;
666
667       if (ten_seconds_event)
668         {
669           uint16_t t;
670
671           ten_seconds_event = 0;
672
673           t = get_current_time ();
674           if (!(t % 6))
675             {
676               /* Code to run every minute.  */
677               process_schedule (t, 0);
678             }
679         }
680
681
682       msg = csma_get_message ();
683       if (msg)
684         {
685           /* Process the message.  */
686           switch (msg[0])
687             {
688             case PROTOCOL_EBUS_BUSCTL:
689               process_ebus_busctl (msg);
690               break;
691             case PROTOCOL_EBUS_H61:
692               process_ebus_h61 (msg);
693               break;
694             default:
695               /* Ignore all other protocols.  */
696               break;
697             }
698           /* Re-enable the receiver.  */
699           csma_message_done ();
700         }
701     }
702
703 }