diff --git a/docs/index.rst b/docs/index.rst index b49c12182..ea6178eaf 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -72,6 +72,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 diff --git a/docs/log-html-view.jpeg b/docs/log-html-view.jpeg new file mode 100644 index 000000000..f9b736c36 Binary files /dev/null and b/docs/log-html-view.jpeg differ diff --git a/docs/log-my_data.png b/docs/log-my_data.png new file mode 100644 index 000000000..b64e01a91 Binary files /dev/null and b/docs/log-my_data.png differ diff --git a/docs/log.rst b/docs/log.rst new file mode 100644 index 000000000..9cc21e6b2 --- /dev/null +++ b/docs/log.rst @@ -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 +`_. + +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 diff --git a/docs/microbit.rst b/docs/microbit.rst index 97bb697e4..816f77689 100644 --- a/docs/microbit.rst +++ b/docs/microbit.rst @@ -40,6 +40,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() diff --git a/examples/data-logging.py b/examples/data-logging.py new file mode 100644 index 000000000..c3decb641 --- /dev/null +++ b/examples/data-logging.py @@ -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)