MicroPython Skill Builders — #10 Файлы

MicroPython Skill Builders — #10 Файлы

Добро пожаловать в десятый выпуск серии MicroPython Skill Builders, цель которой — улучшить ваши навыки программирования на MicroPython с одновременным знакомством с новыми компонентами и техниками написания кода — с использованием Raspberry Pi Pico!

Сегодня мы рассмотрим, как работать с файлами в MicroPython на Pico: создавать их, а затем считывать и обрабатывать данные. Этот урок основывается на предыдущем уроке о строках.

Что вам понадобится

Предполагается, что вы уже установили Thonny на компьютер и настроили Raspberry Pi Pico с актуальной прошивкой MicroPython (UF2). Если нет — воспользуйтесь руководством по началу работы с Raspberry Pi Pico, где всё это подробно описано.

Вам понадобится:

  • Raspberry Pi Pico с контактными гребёнками (пинами)

  • Кабель Micro-USB (для питания и программирования Pico)

Дополнительно (для раздела «Что попробовать» в конце):

  • Потенциометр 10 кОм

  • Датчик DHT11 или DHT12

  • Соединительные провода (папа-папа и папа-мама, желательно разных цветов)

Что такое файл?

Файл — это набор информации, который прежде всего идентифицируется по своему имени, например: data.csv или myfile.txt.

Зачем нужны файлы?

Если вы, например, сохраняете серию показаний датчика в виде списка в вашей программе, эти данные будут потеряны при отключении питания Pico. Это связано с тем, что оперативная память (RAM) используется для хранения переменных во время выполнения программы: хотя она очень быстрая, она также является энергозависимой и «забывает» данные при отключении питания.

Если мы сохраним показания в файл во Flash-памяти, мы сможем прочитать их при следующем включении Pico.

Пример 1 — Создание файла

В примере ниже мы создадим простой файл CSV (Comma Separated Values — значения, разделённые запятыми). Это обычный текстовый файл с шестью записями.

Каждая запись состоит из двух полей: целого числа (без десятичной точки) и числа с плавающей точкой (с десятичной точкой). Каждое поле состоит из набора текстовых символов.

Вот пример такого текстового файла в том виде, в котором он выводится при печати в Thonny:

736,2247.38
547,6768.42
837,8634.73
415,4498.85
165,6724.25
872,566.26

Здесь 6 строк (записей). Запятая в каждой строке разделяет запись на два поля.

Каждая запись заканчивается символом \n — символом перевода строки. Он невидим при выводе, однако обеспечивает перенос следующей записи на новую строку.

Чтобы наглядно это показать, вот как выглядел бы вывод, если бы мы заменили символ /n переноса строки на обычный символ N:

736,2247.38N547,6768.42N837,8634.73N415,4498.85N165,6724.25N872,566.26N

Такой файл может быть очень длинным. Flash-память Raspberry Pi Pico составляет 2 МБ — 2 мегабайта — 2 миллиона байт! Байт — это 8 бит — двоичных цифр, 1 или 0 (а знаете ли вы, что половина байта называется нибблом!)

Мы напишем программу для создания такого файла, а затем извлечём и обработаем из него данные.

Создание файла

Запустите код ниже в Thonny:

# Data File Example
# Tony Goodhew for thepihut.com 25 July 2023
import random # Random Number Library - Built-In

print("\n#### Basic FILE Test ####\n")

# Part #1 - Create a .csv file
print("Values Recorded in file \n")
with open("/test.csv", "w") as f: # w = overwrite if already there / a = append to end of file
    for count in range(6):
        irand = random.randint(0, 999)       # Generate a random integer
        rand = random.randint(0, 999999)
        fp = rand/100.0                      # Generate a random FP number
        record = str(irand) + "," + str(fp)  # Build record string
        f.write(record + "\n")               # Write record to file with RTN/newline
        print(record)
    f.close()

Программа выдаёт вывод, подобный приведённому ниже, а в разделе Files в левом нижнем углу окна Thonny появляется новый файл.

  • Нет раздела Files? Выберите View > Files, чтобы показать эту панель в Thonny.

  • Файл не появляется? Возможно, нужно обновить представление, чтобы увидеть вновь созданный файл (нажмите на маленькую кнопку меню «бургер» в разделе Files).

#### Basic FILE Test ####

Values Recorded in file

949,5474.87
202,5445.35
416,2676.86
564,7689.08
59,4670.19
558,3469.9

Как работает код создания файла?

Новый файл создаётся инструкцией:

with open("/test.csv", "w")

«w» указывает режим записи и перезапишет существующий файл с таким же именем, если он уже существует. Использование «a» вместо этого добавило бы новые данные в конец существующего файла или создало бы новый, если файл не найден.

Мы настраиваем счётный цикл на 6 итераций. Внутри цикла используется функция random.randint(0, n) для генерации пары случайных целых чисел разного размера. Второе делится на 100.0, чтобы превратить его в число с плавающей точкой, fp.

Затем мы строим запись файла, преобразуя числа в строки и объединяя их с разделяющим символом запятой:

record = str(irand) + "," + str(fp)  # Build record string

После этого мы записываем запись в файл, добавляя в конец обязательный символ новой строки:

f.write(record + "\n") # Write record to file with RTN/newline

Наконец, мы выводим содержимое записи в окно Shell, чтобы видеть, что происходит.

Когда цикл завершается, мы закрываем файл инструкцией:

f.close()

Пример 2 — Обработка данных файла

Теперь мы обработаем данные из файла, созданного выше.

Добавьте строки ниже в конец файла из предыдущего примера и запустите его в Thonny:

# Part #2 - Process the file by first copying it into memory
print("\nProcessing lines from file: Method #1 - input whole file first\n")
with open("/test.csv", "r") as f: # Open the file for reading - "r"
    lines = f.readlines()         # Read whole file into memory
    print(len(lines),"\n")        # Print number of lines stored
    for line in lines:            # Get the next line from RAM
        length = len(line)        # Find length of the current line
        line = line[:length-1]    # Remove last character - New Line
        p = line.find(",")        # Find position of the comma
        first = line[:p]          # Characters up to comma
        i2 = int(first)           # Convert to integer
        last = line[p + 1:]       # Characters after the comma
        fp2 =float(last)          # Convert to floating-point number
        result = i2 * fp2         # Process the values = Calculate a dummy result
        print("Int =",i2,"\tFP =",fp2, "\tAnswer =",result) # Tabbed printing

    f.close()  # Close the file when finished

Как работает код обработки?

Мы выводим заголовочную строку и открываем файл в режиме ЧТЕНИЯ, «r»:

with open("/test.csv", "r") as f: # Open the file for reading - "r"

Следующая инструкция читает весь файл в RAM, разбивая его на набор отдельных строк по каждому символу новой строки:

lines = f.readlines()         # Read whole file into memory
print(len(lines),"\n")        # Print number of lines stored

Инструкция print подтверждает, что хранится 6 строк.

Затем мы перебираем строки одну за другой, выполняя следующие операции:

  • Находим длину текущей строки: length = len(line)

  • Удаляем символ новой строки с конца: line = line[:length-1]

  • Находим позицию запятой в строке: p = line.find(«,»)

  • Используем позицию, чтобы извлечь строку перед запятой: first = line[:p]

  • Преобразуем строку в целое число: i2 = int(first)

  • Используем позицию, чтобы извлечь строку после запятой: last = line[p + 1:]

  • Преобразуем строку в число с плавающей точкой: fp2 = float(last)

  • Обрабатываем числа, умножая их: result = i2 * fp2

  • Выводим числа и произведение с использованием «\t» для выравнивания символом табуляции

Наконец, мы закрываем файл после обработки всех строк с помощью f.close()

После запуска программы убедитесь, что числа, сохранённые в первой части, точно восстановлены и обработаны во второй части.

Нехватка RAM!

Измените счётчик цикла с 6 на 100 и запустите снова. Должно работать нормально и довольно быстро, даже со всем этим выводом.

Теперь попробуйте изменить число итераций на 10000. Да, десять тысяч! В Pico достаточно Flash-памяти для этого. Запустите код снова — это займёт значительно больше времени, но успешно сохранит все записи; однако обработка может завершиться с ошибкой из-за нехватки RAM.

К сожалению, RAM не хватает для хранения всех строк одновременно при попытке прочитать и сохранить их по отдельности.

Решение проблемы нехватки RAM

Нам нужен другой метод для обработки этого большого файла и обхода ограничений RAM. Он очень прост — нужно всего лишь читать строки по одной и обрабатывать их на ходу.

Замените код обработки (Часть #2) блоком ниже (Часть #3) и попробуйте снова. Это занимает время, но завершается без сбоев:

# Part #3 - Process the file by reading one record at a time from the file in ROM
with open("/test.csv", "r") as f: # Open the file for reading - "r"
    print("\nProcessing lines from file: Method #2 - Input single lines\n") # Read one line at a time
    line = f.readline()           # Read in characters to make a line which ends with New line character from ROM/Flash memory
    while line != '':             # Empty string - Nothing left to read - End of File found
        length = len(line)        # Find length of current line
        line = line[:length-1]    # Remove last character New Line
        p = line.find(",")        # Find position of the comma
        first = line[:p]          # Characters up to comma
        i2 = int(first)           # Convert to integer
        last = line[p + 1:]       # Characters after the comma
        fp2 =float(last)          # Convert to floating-point number
        result = i2 * fp2         # Process the values = Calculate a dummy result
        print("Int =",i2,"\tFP =",fp2, "\tAnswer =",result) # Tabbed printing
        line = f.readline()

    f.close()  # Close the file when finished

Что попробовать

  • Добавьте счётчик в цикл последнего кода и проверьте, что он действительно читает все 10000 записей

  • Подключите потенциометр и записывайте значения при его вращении в течение 30 секунд

  • Считывайте температуру и влажность с датчика и сохраните 20 записей с интервалом в полминуты

  • Считайте их обратно и выведите аккуратно (только один знак после запятой)

  • Считайте их обратно и вычислите среднее, максимальное и минимальное значения температуры и влажности

  • Сохраняйте текущее время в начале каждой записи

Вот подсказка для последнего пункта…

import time
for i in range(10):
    now = time.time() # Seconds since epoch
    print(now)
    time.sleep(1)

Об авторе

Тони Гудью — бывший учитель информатики, начавший писать код ещё в 1968 году, когда это называлось программированием — он начинал с FORTRAN IV на IBM 1130! Активный участник сообщества Raspberry Pi, сейчас его главные интересы — программирование на MicroPython, путешествия и фотография.