Skip to content

Commit

Permalink
Begin chron based update checker (#31)
Browse files Browse the repository at this point in the history
  • Loading branch information
alexwohlbruck committed May 11, 2022
1 parent 8a8e652 commit bb695fa
Show file tree
Hide file tree
Showing 4 changed files with 324 additions and 0 deletions.
201 changes: 201 additions & 0 deletions micro/app/mcron/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import utime
import machine

PERIOD_CENTURY = const(100 * 365 * 24 * 60 * 60) # Warning: average value
PERIOD_YEAR = const(365 * 24 * 60 * 60) # Warning: average value
PERIOD_MONTH = const((365 * 24 * 60 * 60) // 12) # Warning: average value
PERIOD_WEEK = const(7 * 24 * 60 * 60)
PERIOD_DAY = const(24 * 60 * 60)
PERIOD_HOUR = const(60 * 60)
PERIOD_MINUTE = const(60)

# {
# <period: int>: {
# period_steps: [<callback_id>, <callback_id>, ...]
# }
# }
timer_table = {}

# {
# <callback_id>: {}
# }
memory_table = {}

# {
# <callback_id>: <callback>
# }
callback_table = {}

# machine.Timer instance
timer = None

# A list of functions that process the captured exception, received from the running callback.
# processor_exception_function(exception_instance)
callback_exception_processors = [lambda e: print('Callback EXCEPTION: %s' % e)]

STEP_TYPE_RANGE = const(1)
STEP_TYPE_SET = const(2)

_last_run_time = None
_timer_period = None # You have to turn the timer on by: init_timer
_max_time_task_calls = None # You have to turn the timer on by: init_timer


class TLPTimeException(Exception):
"""
Too long processing time.
The maximum time is: 1000 [ms] - 1.5 * period [ms]
"""
pass


def insert(period, period_steps, callback_id, callback, period_offset=0, from_now=False):
"""
Examples:
* Starting once after XX seconds.
insert(<seconds_from_now>+1, {<seconds_from_now>}, 'callback-id', run_times(1)(<callback>), from_now=True)
* Running twice a day at 23:59 and 6:00 a.m.
insert(mcron.PERIOD_DAY, {23 * 59 * 59, 6 * 60 * 60}, 'callback-id', <callback>)
:param period: A period of time during which the steps are repeated.
:type period: int
:param period_steps: The steps where the callbacks are called. The steps must be integer type.
:type period_steps: range or set
:param callback_id:
:type callback_id: str
:param callback: callable(callback_id, current_time, callback_memory)
:type callback: callable
:param period_offset: The beginning of the period is shifted by the set value.
:type period_offset: int
:param from_now: The period will start when the task is added.
:return:
"""
global timer_table, memory_table, callback_table, callback_exception_processors

if callback_id in callback_table:
raise Exception('Callback ID - exists')

if type(period) is not int:
raise TypeError('period is not int')

if from_now:
period_offset = -1 * utime.time() % period

if type(period_offset) is not int:
raise TypeError('period_offset is not int')

if type(period_steps).__name__ == 'range':
period_steps = (STEP_TYPE_RANGE,) + (period_steps.start, period_steps.stop, period_steps.step)
elif type(period_steps).__name__ == 'set':
period_steps = (STEP_TYPE_SET,) + tuple(period_steps)
else:
raise Exception('period_steps wrong type')

for s in period_steps[1:]:
if type(s) is not int:
raise TypeError('period step is not int %s' % str(period_steps))

callback_table[callback_id] = callback

period_info = (period, period_offset)
if period_info in timer_table:
period_data = timer_table[period_info]
else:
period_data = {}
timer_table[period_info] = period_data

if period_steps in period_data:
callback_ids = period_data[period_steps]
else:
callback_ids = set()
period_data[period_steps] = callback_ids
callback_ids.add(callback_id)


def remove(callback_id):
global timer_table, memory_table, callback_table
to_del_pi = []
for period_info, period_data in timer_table.items():
to_del_ps = []
for period_steps, callback_ids in period_data.items():
if callback_id in callback_ids:
callback_ids.remove(callback_id)
if callback_id in memory_table:
memory_table.pop(callback_id)
if len(callback_ids) <= 0:
to_del_ps.append(period_steps)
for period_steps in to_del_ps:
period_data.pop(period_steps)
del to_del_ps
if not period_data:
to_del_pi.append(period_info)
for period_info in to_del_pi:
timer_table.pop(period_info)
del to_del_pi
if callback_id in callback_table:
callback_table.pop(callback_id)


def remove_all():
global callback_table
for cid in callback_table.keys():
remove(cid)


def get_actions(current_time):
global timer_table, memory_table, callback_table, callback_exception_processors
for period_info, period_data in timer_table.items():
period, period_offset = period_info
period_pointer = (current_time + period_offset) % period
for period_steps, callback_ids in period_data.items():
if STEP_TYPE_SET == period_steps[0] and period_pointer in period_steps[1:] or \
STEP_TYPE_RANGE == period_steps[0] and period_pointer in range(*period_steps[1:]):
yield from callback_ids


def run_actions(current_time):
global timer_table, memory_table, callback_table, callback_exception_processors
for callback_id in get_actions(current_time):
callback_memory = memory_table.setdefault(callback_id, {})
action_callback = callback_table[callback_id]

try:
action_callback(callback_id, current_time, callback_memory)
except Exception as e:
for processor in callback_exception_processors:
processor(e)


def run_actions_callback(*args, **kwargs):
global timer_table, memory_table, callback_table, callback_exception_processors, _last_run_time, _timer_period, _max_time_task_calls

current_time = utime.time()
if current_time == _last_run_time:
return
_last_run_time = current_time
start = utime.ticks_ms()

run_actions(current_time)

stop = utime.ticks_ms()
processing_time = utime.ticks_diff(stop, start)
if processing_time > _max_time_task_calls:
e = TLPTimeException(current_time, processing_time, ' '.join(get_actions(current_time)))
for processor in callback_exception_processors:
processor(e)


def init_timer(timer_id=3, timer_period=250):
"""
:param timer_id:
:param timer_period: Number of milliseconds between run_actions calls. The recommended value is 250ms. For values greater than 1000ms some action calls may be omitted.
:type timer_period: int
:return:
"""
global timer, _timer_period, _max_time_task_calls, timer
_timer_period = timer_period
_max_time_task_calls = 1000 - 1.5 * _timer_period
timer = machine.Timer(timer_id)
timer.init(period=_timer_period, mode=machine.Timer.PERIODIC, callback=run_actions_callback)
108 changes: 108 additions & 0 deletions micro/app/mcron/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import app.mcron

RUN_TIMES_MEM_ID = const(2)


def run_times(times):
"""
The decorator determines how many times the given callback can be started.
:param times: number of start-ups
:return:
"""

def decorator(callback):
def wrapper(callback_id, current_time, callback_memory):
callback_memory[RUN_TIMES_MEM_ID] = callback_memory.setdefault(RUN_TIMES_MEM_ID, 0) + 1
out = callback(callback_id, current_time, callback_memory)
if callback_memory[RUN_TIMES_MEM_ID] >= times:
mcron.remove(callback_id)
return out

return wrapper

return decorator


SUCCESSFULLY_RUN_TIMES_MEM_ID = const(3)


def successfully_run_times(times):
"""
The decorator determines how many times the given callback can be started.
Launching a task is considered correct only if the callback returns True.
:param times: number of start-ups
:return:
"""
MEM_ID = '__srt'

def decorator(callback):
def wrapper(callback_id, current_time, callback_memory):
out = callback(callback_id, current_time, callback_memory)
callback_memory[SUCCESSFULLY_RUN_TIMES_MEM_ID] = \
callback_memory.setdefault(SUCCESSFULLY_RUN_TIMES_MEM_ID, 0) + int(bool(out))
if callback_memory[SUCCESSFULLY_RUN_TIMES_MEM_ID] >= times:
mcron.remove(callback_id)
return out

return wrapper

return decorator


CALL_COUNTER_MEM_ID = const(4)


def call_counter(callback):
"""
Decorator counts the number of callback calls.
The number of calls is stored in memory[call_counter.ID].
:param callback:
:return:
"""

def decorator(callback_id, current_time, callback_memory):
callback_memory[CALL_COUNTER_MEM_ID] = callback_memory.setdefault(CALL_COUNTER_MEM_ID, 0) + 1
return callback(callback_id, current_time, callback_memory)

return decorator


def debug_call(callback):
"""
The decorator displays information about the current call
:param callback:
:return:
"""

@call_counter
def wrap(callback_id, current_time, callback_memory):
import utime
print(
'START call(%3d): %25s, pointer%18s' % (
callback_memory[CALL_COUNTER_MEM_ID],
callback_id,
str(utime.localtime(current_time))
)
)
mem_before = dict([(k, d) for k, d in callback_memory.items() if not k.startswith('__')])
print(' Memory before call: %s' % mem_before)
out = callback(callback_id, current_time, callback_memory)
mem_after = dict([(k, d) for k, d in callback_memory.items() if not k.startswith('__')])
print(' Memory after call: %s' % mem_after)
print(
'END call(%3d): %25s, pointer%18s' % (
callback_memory[CALL_COUNTER_MEM_ID],
callback_id,
str(utime.localtime(current_time))
)
)
print()
return out

return wrap
1 change: 1 addition & 0 deletions micro/app/mcron/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = '1.1.2'
14 changes: 14 additions & 0 deletions micro/app/startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import app.input as inputio
from app.led import animate
from app.commander import start_commander
import app.mcron as mcron
import app.mcron.decorators

def check_update_and_install():
otaUpdater = OTAUpdater('https://github.com/alexwohlbruck/covalent', github_src_dir='micro', main_dir='app')
Expand All @@ -17,6 +19,17 @@ def check_update_and_install():
del(otaUpdater)
gc.collect()

def counter(callback_id, current_time, callback_memory):
check_update_and_install()

def start_updater_chron():
mcron.init_timer()
mcron.insert(15, {0}, '15s', counter)

def my_exception_processor(e):
print(e)

mcron.callback_exception_processors.append(my_exception_processor)

def run_startup():
wifi_success = connect_wifi_from_config()
Expand All @@ -29,6 +42,7 @@ def run_startup():

# Start commander service
start_commander()
start_updater_chron()

# Run LED animation loop
# animate()
Expand Down

0 comments on commit bb695fa

Please sign in to comment.