~toykeeper/flashlight-firmware/trunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
Spaghetti Monster: A UI toolkit library for flashlights
-------------------------------------------------------

This toolkit takes care of most of the obnoxious parts of dealing with 
tiny embedded chips and flashlight hardware, leaving you to focus on the 
interface and user-visible features.

For a quick start, look at the example UIs provided to see how things 
are done.  They are probably the most useful reference.  However, other 
details can be found here or in the FSM source code.


Why is it called Spaghetti Monster?

  This toolkit is a finite state machine, or FSM.  Another thing FSM 
  stands for is Flying Spaghetti Monster.  Source code tends to weave 
  into intricate knots like spaghetti, called spaghetti code, 
  particularly when the code isn't using appropriate abstractions for 
  the task it implements.

  Prior e-switch light code had a tendency to get pretty spaghetti-like, 
  and it made the code difficult to write, understand, and modify.  So I 
  started from scratch and logically separated the hardware details from 
  the UI.  This effectively put the spaghetti monster in a box, put it 
  on a leash, to make it behave and stay out of the way while we focus 
  on the user interface.

  Also, it's just kind of a fun name.  :)


General concept:

  Spaghetti Monster (FSM) implements a stack-based finite state machine 
  with an event-handling system.

  Each FSM program should have a setup() function, a loop() function, 
  and at least one State:

    - The setup() function runs once each time power is connected.

    - The loop() function is called repeatedly whenever the system is 
      otherwise idle.  Put your long-running tasks here, preferably with 
      consideration taken to allow for cooperative multitasking.

    - The States on the stack will be called whenever an event happens.  
      States are called in top-to-bottom order until a state returns an 
      "EVENT_HANDLED" signal.  Only do quick tasks here.


Finite State Machine:

  Each "State" is simply a callback function which handles events.  It 
  should return EVENT_HANDLED for each event type it does something 
  with, or EVENT_NOT_HANDLED otherwise.

  Transitions between states typically involve mapping an Event to a new 
  State, such as this:

    // 3 clicks: go to strobe modes
    else if (event == EV_3clicks) {
        set_state(strobe_state, 0);
        return EVENT_HANDLED;
    }

  It is strongly recommended that your State functions never do anything 
  which takes more than a few milliseconds...  and certainly not longer 
  than 16ms.  If you do this, the pending events may pile up to the 
  point where new events get thrown away.  So, do only quick tasks in 
  the event handler, and do your longer-running tasks in the loop() 
  function instead.  Preferably with precautions taken to allow for 
  cooperative multitasking.

  If your State function takes longer than one WDT tick (16ms) once in a 
  while, the system won't break.  Several events can be queued.  But be 
  sure not to do it very often.

  Several state management functions are provided:

    - set_state(new_state, arg): Replace the current state on the stack.  
      Send 'arg' to the new state for its init event.

    - push_state(new_state, arg): Add a new state to the stack, leaving 
      the current state below it.  Send 'arg' to the new state for its 
      init event.

    - pop_state(): Get rid of (and return) the top-most state.  Re-enter 
      the state below.


Event types:

  Event types are defined in fsm-events.h.  You may want to adjust these 
  to fit your program, but the defaults are:

    State transitions:

      - EV_enter_state: Sent to each new State once when it goes onto 
        the stack.  The 'arg' is whatever you define it to be.

      - EV_leave_state: Sent to a state immediately before it is removed 
        from the stack.

      - EV_reenter_state: If a State gets pushed on top of this one, and 
        then it pops off, a re-enter Event happens.  This should handle 
        things like consuming the return value of a nested input handler 
        State.

    Time passing:

      - EV_tick: This happens once per clock tick, which is 16ms or 
        62.5Hz by default.  The 'arg' is the number of ticks since 
        entering the state.  When 'arg' exceeds 65535, it wraps around 
        to 32768.

      - EV_sleep_tick: This happens every 0.5s during standby, if 
        enabled at compile time.  The 'arg' is the number of ticks since 
        entering the state.  When 'arg' exceeds 65535, it wraps around 
        to 32768.

    LVP and thermal regulation:

      - EV_voltage_low: Sent whenever the input power drops below the 
        VOLTAGE_LOW threshold.  Minimum of VOLTAGE_WARNING_SECONDS 
        between events.

      - EV_temperature_high: Sent whenever the MCU's projected temperature 
        is higher than therm_ceil.  Minimum of one second between events.
        The 'arg' indicates how far the temperature exceeds the limit.

      - EV_temperature_low: Sent whenever the MCU's projected temperature 
        is lower than (therm_ceil - THERMAL_WINDOW_SIZE).  Minimum of 
        one second between events.  The 'arg' indicates how far the
        temperature exceeds the limit.

    Button presses:

      Button events can be referred to either by pre-defined symbols, or 
      by teasing out the flags manually.  The structure of a button 
      event is as follows:

        - Bit 7: 1 for button events, 0 otherwise.

        - Bit 6: 1 for a "timeout" event (signals the end of a 
          sequence), or 0 otherwise.

        - Bit 5: 1 for a "hold" event, 0 otherwise.  This flag is only 
          necessary because, without it, it would be impossible to 
          distinguish between "click, click, timeout" and "click, hold, 
          release".

        - Bit 4: 1 if button is currently pressed, 0 otherwise.  Button 
          release events look just like button press events, except this 
          is not set.

        - Bits 0,1,2,3: Counter for how many clicks there have been.  
          The first click is 1, second is 2, and it goes up to 15 clicks 
          in a row.  Clicks after 15 are coded as 15.

      The pre-defined button event symbols are like the following:

        - EV_click1_press: The user pressed the button, but no time has 
          passed since then.

        - EV_click1_release: The user pressed and released the button, 
          but no time has passed since then.

        - EV_click1_complete: The user clicked the e-switch, released 
          it, and enough time passed that no more clicks were detected.
          (a.k.a. EV_1click)

        - EV_click1_hold: The user pressed the button, and continued 
          holding it long enough to count as a "hold" event.  This event 
          is sent once per timer tick as long as the button is held, and 
          the 'arg' value indicates how many timer ticks since the 
          button state went from 'press' to 'hold'. 

        - EV_click1_hold_release: The button was released at the end of 
          a "hold" event.  This is the end of the input sequence, 
          because no timeout period is used after a hold.

          It's worth noting that a "hold" event can only happen at the 
          end of an input sequence, and the sequence will reset to empty 
          after the hold is released.

      If the user pressed the button more than once, events follow the 
      same pattern.  These are the same as above, except with a full 
      short-press and release first.

        - EV_click2_press
        - EV_click2_release
        - EV_click2_complete (a.k.a. EV_2clicks)
        - EV_click2_hold
        - EV_click2_hold_release

      Each of the above patterns continues up to 15 clicks.

      To match entire categories of events, use the bitmasks provided.  
      For example, to match button events where the button is down or 
      the button is up, the code would look like this:

        if ((event & (B_CLICK | B_PRESS)) == (B_CLICK | B_PRESS)) {
          // button is down (can be a press event or a hold event)
        }
        else if ((event & (B_CLICK | B_PRESS)) == (B_CLICK)) {
          // button was just released
        }

  In theory, you could also define your own arbitrary event types, and 
  emit() them as necessary, and handle them in State functions the same 
  as any other event.


Cooperative multitasking:

  Since we don't have true preemptive multitasking, the best we can do 
  is cooperative multitasking.  In practice, this means:

    - Declare global variables as volatile if they can be changed by an 
      event handler.  This keeps the compiler from caching the value and 
      causing incorrect behavior.

    - Don't put long-running tasks into State functions.  Each State 
      will get called at least once every 16ms for a clock tick, so they 
      should not run for longer than 16ms.

    - Put long-running tasks into loop() instead.

    - For long delay() calls, use nice_delay_ms().  This allows the MCU 
      to process events while we wait.  It also automatically aborts if 
      it detects a state change, and returns a different value.

      In many cases, it shouldn't be necessary to do anything more than 
      this, but sometimes it will also be a good idea to check the 
      return value and abort the current task:

          if (! nice_delay_ms(mydelay)) break;

    - In general, try to do small amounts of work and then return 
      control to other parts of the program.  Keep doing small amounts 
      and yielding until a task is done, instead of trying to do it all 
      at once.


Persistent data in EEPROM:

  To save data which lasts after a battery change, use the eeprom 
  functions.  Define an eeprom style (or two) at the top, define how 
  many bytes to allocate, and then use the relevant functions as 
  appropriate.

    - USE_EEPROM / USE_EEPROM_WL: Enable the eeprom-related functions.  
      With "WL", it uses wear-levelling.  Without, it does not.  Note:  
      Wear levelling is not necessarily better -- it uses more ROM, and 
      it writes more bytes per save().  So, use it only for a few bytes 
      which change frequently -- not for many bytes or infrequent 
      changes.

    - EEPROM_BYTES N / EEPROM_WL_BYTES N: Allocate N bytes for the 
      eeprom data.

    - load_eeprom() / load_eeprom_wl(): Load the stored data into the 
      eeprom[] or eeprom_wl[] arrays.
      Returns 1 if data was found, 0 otherwise.

    - save_eeprom() / save_eeprom_wl(): Save the eeprom[] or eeprom_wl[] 
      array data to persistent storage.  The WL version erases all old 
      values and writes new ones in a different part of the eeprom 
      space.  The non-WL version updates values in place, and does not 
      overwrite values which didn't change.

  Note that all interrupts will be disabled during eeprom operations.


Useful #defines:

  A variety of things can be #defined before including 
  spaghetti-monster.h in your program.  This allows you to tweak the 
  behavior and set options to fit your needs:

    - FSM_something_LAYOUT: Select a driver type from tk-attiny.h.  This 
      controls how many power channels there are, which pins they're on, 
      and what other driver features are available.

    - USE_LVP: Enable low-voltage protection.

      - VOLTAGE_LOW: What voltage should LVP trigger at?  Defaults to 29 (2.9V).

      - VOLTAGE_FUDGE_FACTOR: Add this much to the voltage measurements, 
        to compensate for voltage drop across the reverse-polarity 
        diode.

      - VOLTAGE_WARNING_SECONDS: How long to wait between LVP events.

    - USE_THERMAL_REGULATION: Enable thermal regulation

      - DEFAULT_THERM_CEIL: Set the temperature limit to use by default 
        when the user hasn't configured anything.

    - USE_RAMPING: Enable smooth ramping helpers.

      - RAMP_LENGTH: Pick a pre-defined ramp by length.  Defined sizes 
        are 50, 75, and 150 levels.

    - USE_DELAY_4MS, USE_DELAY_MS, USE_DELAY_ZERO: Enable the delay_4ms, 
      delay_ms(), and delay_zero() functions.  Useful for timing-related 
      activities.

    - HOLD_TIMEOUT: How many clock ticks before a "press" event becomes 
      a "hold" event?

    - RELEASE_TIMEOUT: How many clock ticks before a "release" event 
      becomes a "click" event?  Basically, the maximum time between 
      clicks in a double-click or triple-click.

    - USE_BATTCHECK: Enable the battcheck function.  Also define one of 
      the following to select a display style:

        - BATTCHECK_VpT: Volts, pause, tenths.
        - BATTCHECK_4bars: Blink up to 4 times.
        - BATTCHECK_6bars: Blink up to 6 times.
        - BATTCHECK_8bars: Blink up to 8 times.

    - ... and many others.  Will try to document them over time, but 
      they can be found by searching for pretty much anything in 
      all-caps in the fsm-*.[ch] files.