Skip to content

Commit

Permalink
docs: Add data logging API (#720)
Browse files Browse the repository at this point in the history
Includes microbit.run_every function/decorator.
  • Loading branch information
microbit-mark authored and microbit-carlos committed Feb 26, 2024
1 parent 47baec9 commit 78e4833
Show file tree
Hide file tree
Showing 6 changed files with 197 additions and 0 deletions.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Projects related to MicroPython on the BBC micro:bit include:
ble.rst
button.rst
compass.rst
log.rst
display.rst
filesystem.rst
i2c.rst
Expand Down
Binary file added docs/log-html-view.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/log-my_data.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
122 changes: 122 additions & 0 deletions docs/log.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
Data Logging **V2**
*******************

.. py:module:: log
This module lets you log data to a ``MY_DATA`` file saved on a micro:bit
**V2** ``MICROBIT`` USB drive.

.. image:: log-my_data.png

The data is structured in a table format and it can be viewed and plotted with
a browser.

.. image:: log-html-view.jpeg

Further guidance on this feature can be found on the
`data logging page of the microbit.org website
<https://microbit.org/get-started/user-guide/data-logging/>`_.

Functions
=========

.. py:function:: set_labels(*labels, timestamp=log.SECONDS)
Set up the log file header.

This function accepts any number of positional arguments, each creates
a column header, e.g. ``log.set_labels("X", "Y", "Z")``.

Ideally this function should be called a single time, before any data is
logged, to configure the data table header once.

If a log file already exists when the programme starts, or if this function
is called multiple times, it will check the labels already defined in the
log file.
If this function call contains any new labels not already present, it will
generate a new header row with the additional columns.

By default the first column contains a time stamp for each row. The time
unit can be selected via the ``timestamp`` argument, e.g.
``log.set_labels("temp", timestamp=log.MINUTES)``

:param \*labels: Any number of positional arguments, each corresponding to
an entry in the log header.
:param timestamp: Select the timestamp unit that will be automatically
added as the first column in every row. Timestamp values can be one of
``log.MILLISECONDS``, ``log.SECONDS``, ``log.MINUTES``, ``log.HOURS``,
``log.DAYS`` or ``None`` to disable the timestamp. The default value
is ``log.SECONDS``.

.. py:function:: set_mirroring(serial)
Configure mirroring of the data logging activity to the serial output.

Serial mirroring is disabled by default. When enabled, it will print to
serial each row logged into the log file.

:param serial: ``True`` enables mirroring data to the serial output.

.. py:function:: delete(full=False)
Delete the contents of the log, including headers.

To add the log headers again the ``set_labels`` function should to be
called after this function.

There are two erase modes; "full" completely removes the data from the
physical storage, and "fast" invalidates the data without removing it.

:param full: ``True`` selects a "full" erase and ``False`` selects the
"fast" erase method.

.. py:function:: add( data_dictionary, /, *, **kwargs)
Add a data row to the log.

There are two ways to log data with this function:

#. Via keyword arguments, each argument name representing a label.

* e.g. ``log.add(X=compass.get_x(), Y=compass.get_y())``

#. Via a dictionary, each dictionary key representing a label.

* e.g. ``log.add({ "X": compass.get_x(), "Y": compass.get_y() })``

The keyword argument option can be easier to use, and the dictionary option
allows the use of spaces (and other special characters), that could not be
used with the keyword arguments.

New labels not previously specified via the ``set_labels`` function, or by
a previous call to this function, will trigger a new header entry to be
added to the log with the extra labels.

Labels previously specified and not present in a call to this function will
be skipped with an empty value in the log row.

:raise OSError: When the log is full this function raises an ``OSError``
exception with error code 28 ``ENOSPC``, which indicates there is no
space left in the device.

Examples
========

A minimal example::

from microbit import *
import log

# Set the timer to log data every 5 seconds
@run_every(s=5)
def log_temp():
log.add(temp=temperature())

while True:
# Needed so that the programme doesn't end
sleep(100)

An example that runs through all of the functions of the log module API:

.. include:: ../examples/data-logging.py
:code: python
32 changes: 32 additions & 0 deletions docs/microbit.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,38 @@ Functions
:param n: An integer or floating point number indicating the number of
milliseconds to wait.

.. py:function:: run_every(callback, h=None, min=None, s=None, ms=None)
Schedule to run a function at the interval specified by the time arguments.

``run_every`` can be used in two ways:

* As a **Decorator** - placed on top of the function to schedule.
For example::

@run_every(h=1, min=20, s=30, ms=50)
def my_function():
# Do something here

* As a **Function** - passing the callback as a positional argument.
For example::

def my_function():
# Do something here
run_every(my_function, s=30)

Each arguments corresponds to a different time unit and they are additive.
So ``run_every(min=1, s=30)`` schedules the callback every minute and
a half.

When an exception is thrown inside the callback function it deschedules
the function. To avoid this you can catch exceptions with ``try/except``.

:param callback: Function to call at the provided interval.
:param h: Sets the hour mark for the scheduling.
:param min: Sets the minute mark for the scheduling.
:param s: Sets the second mark for the scheduling.
:param ms: Sets the millisecond mark for the scheduling.

.. py:function:: temperature()
Expand Down
42 changes: 42 additions & 0 deletions examples/data-logging.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from microbit import *
import log

# Configure the labels and select a time unit for the timestamp
log.set_labels('temp', 'brightness', timestamp=log.SECONDS)

# Send each data row to the serial output
log.set_mirroring(True)

continue_logging = True

# This decorator schedules this function to run every 10s 50ms
@run_every(s=10, ms=50)
def log_data():
"""Log the temperature and light level, and display an icon."""
global continue_logging
if continue_logging:
display.show(Image.SURPRISED)
try:
log.add(temp=temperature(), brightness=display.read_light_level())
except OSError:
continue_logging = False
display.scroll("Log full")
sleep(500)

while True:
if button_a.is_pressed() and button_b.is_pressed():
display.show(Image.CONFUSED)
# Delete the log file using the "full" options, which takes
# longer but ensures the data is wiped from the device
log.delete(full=True)
continue_logging = True
elif button_a.is_pressed():
display.show(Image.HAPPY)
# Log only the light level, the temp entry will be empty. If the log
# is full this will throw an exception and the programme will stop
log.add({
"brightness": display.read_light_level()
})
else:
display.show(Image.ASLEEP)
sleep(500)

0 comments on commit 78e4833

Please sign in to comment.