diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e24be6a --- /dev/null +++ b/.gitignore @@ -0,0 +1,164 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +secrets.py +settings.py + +.vscode/Pico-W-Stub \ No newline at end of file diff --git a/.micropico b/.micropico new file mode 100644 index 0000000..3de3977 --- /dev/null +++ b/.micropico @@ -0,0 +1,3 @@ +{ + "info": "This file is just used to identify a project folder." +} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..fbc7999 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "ms-python.python", + "visualstudioexptteam.vscodeintellicode", + "ms-python.vscode-pylance", + "paulober.pico-w-go" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a02d5f2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "python.languageServer": "Pylance", + "python.analysis.diagnosticSeverityOverrides": { + "reportMissingModuleSource": "none" + }, + "python.analysis.extraPaths": [ + "c:\\Users\\adakovacs\\.vscode\\extensions\\joedevivo.vscode-circuitpython-0.1.20-win32-x64\\boards\\0x239A\\0x8120", + "c:\\Users\\adakovacs\\.vscode\\extensions\\joedevivo.vscode-circuitpython-0.1.20-win32-x64\\stubs", + "c:\\Users\\adakovacs\\AppData\\Roaming\\Code\\User\\globalStorage\\joedevivo.vscode-circuitpython\\bundle\\20231124\\adafruit-circuitpython-bundle-py-20231124\\lib", + ".vscode\\Pico-W-Stub" + ], + "circuitpython.board.version": null, + "circuitpython.board.vid": "0x239A", + "circuitpython.board.pid": "0x8120", + "python.linting.enabled": true, + "python.analysis.typeCheckingMode": "basic", + "micropico.syncFolder": "", + "micropico.openOnStart": true, + "python.analysis.typeshedPaths": [ + ".vscode\\Pico-W-Stub" + ] +} \ No newline at end of file diff --git a/led.py b/led.py new file mode 100644 index 0000000..715864d --- /dev/null +++ b/led.py @@ -0,0 +1,104 @@ +import utime +import random +from machine import Pin + +def time_ms() -> int: + return int(utime.ticks_ms()) + +""" global led1 +global led2 +global led3 +global led4 +global led1_state +global led2_state +global led3_state +global led4_state +global next_on_time_1 +global next_on_time_2 +global next_on_time_3 +global next_on_time_4 """ + +led1 = Pin(2, Pin.OUT) +led2 = Pin(3, Pin.OUT) +led3 = Pin(4, Pin.OUT) +led4 = Pin(5, Pin.OUT) + +current_time = time_ms() +led1_last_time = current_time +led2_last_time = current_time +led3_last_time = current_time +led4_last_time = current_time + +led1_state = 0 +led2_state = 0 +led3_state = 0 +led4_state = 0 + +next_on_time_1 = current_time + 1000 +next_on_time_2 = current_time + 1000 +next_on_time_3 = current_time + 1000 +next_on_time_4 = current_time + 1000 + +def single_led_test(led): + led.value(0) + utime.sleep_ms(400) + led.value(1) + utime.sleep_ms(400) + +def led_test(): + single_led_test(led1) + single_led_test(led2) + single_led_test(led3) + single_led_test(led4) + +def update_led(state, next_on_time, led): + current_time = time_ms() + + try: + if state == 0: + if next_on_time <= current_time: + state = 1 + next_on_time = current_time + random.randint(100, 400) + led.value(0) + else: + if next_on_time <= current_time: + state = 0 + led.value(1) + next_on_time = current_time + random.randint(100, 2000) + except Exception as e: + print('Error in update_led: ' + str(e)) + pass + + return state, next_on_time + +def update_leds(): + global led1 + global led2 + global led3 + global led4 + global led1_state + global led2_state + global led3_state + global led4_state + global next_on_time_1 + global next_on_time_2 + global next_on_time_3 + global next_on_time_4 + + (led1_state, next_on_time_1) = update_led(led1_state, next_on_time_1, led1) + (led2_state, next_on_time_2) = update_led(led2_state, next_on_time_2, led2) + (led3_state, next_on_time_3) = update_led(led3_state, next_on_time_3, led3) + (led4_state, next_on_time_4) = update_led(led4_state, next_on_time_4, led4) + +def led_init(): + global led1 + global led2 + global led3 + global led4 + + print('Led init') + + led1.value(1) + led2.value(1) + led3.value(1) + led4.value(1) \ No newline at end of file diff --git a/main1.py b/main1.py new file mode 100644 index 0000000..6e88a1b --- /dev/null +++ b/main1.py @@ -0,0 +1,379 @@ +# The MIT License (MIT) +# Copyright (c) 2022 Mike Teachman +# https://opensource.org/licenses/MIT + +# Purpose: Play a pure audio tone out of a speaker or headphones +# +# - write audio samples_0 containing a pure tone to an I2S amplifier or DAC module +# - tone will play continuously in a loop until +# a keyboard interrupt is detected or the board is reset +# +# Blocking version +# - the write() method blocks until the entire sample buffer is written to I2S + +import os +import sys +import math +import struct +import utime +import micropython_ota +import machine +import requests +import ubinascii +import network +import random +import _thread +import gc +from led import led_init, led_test, time_ms, update_leds +from machine import I2S +from machine import Pin +from time import sleep +from settings import settings +from secrets import secrets + +ota_project_name = 'pestrep' +ota_branch = settings['ota_branch'] +ota_soft_reset_device=False + +ntfy_topic = settings['ntfy_topic'] + +def make_tone(rate, bits, frequency): + # create a buffer containing the pure tone samples_0 + samples_0_per_cycle = rate // frequency + sample_size_in_bytes = bits // 8 + samples_0 = bytearray(int(samples_0_per_cycle * sample_size_in_bytes)) + volume_reduction_factor = 32 # This sound be 32 while developing + range = pow(2, bits) // 2 // volume_reduction_factor + + if bits == 16: + format = " 0: + if wlan.status() < 0 or wlan.status() >= 3: + break + timeout -= 1 + print('Waiting for connection...') + blink_onboard_led(2) + utime.sleep(1) + # Handle connection error + # Error meanings + # 0 Link Down + # 1 Link Join + # 2 Link NoIp + # 3 Link Up + # -1 Link Fail + # -2 Link NoNet + # -3 Link BadAuth + + wlan_status = wlan.status() + blink_onboard_led(wlan_status) + + if wlan_status != 3: + blink_onboard_led(5) + raise RuntimeError('Wi-Fi connection failed') + else: + blink_onboard_led(1) + print('Connected') + status = wlan.ifconfig() + print('ip = ' + status[0]) + +def disconnect_wifi(): + wlan.disconnect() + wlan.active(False) + wlan.deinit() + print('Disconnected') + +def send_notification(title, tags): + try: + send_notification_to_server('https://ntfy.sh/' + ntfy_topic, title, tags) + except: + print('Error sending notification') + blink_onboard_led(3) + send_notification_to_server('https://ntfy.adix.link/' + ntfy_topic, title, tags) + +def send_notification_to_server(notify_url, title, tags): + print('Sending notification to ' + notify_url + '...') + # Send notification + request = requests.post(notify_url, data="Csengo", headers={ + 'Title': title, + 'Priority': '5', + 'X-Tags': tags + }) + print(request.content) + request.close() + +# ======= I2S CONFIGURATION ======= +SCK_PIN = 16 #BCLK +WS_PIN = 17 #WSEL/LRC +SD_PIN = 18 #DIN +I2S_ID = 0 +BUFFER_LENGTH_IN_BYTES = 2000 + +# ======= AUDIO CONFIGURATION ======= +SAMPLE_SIZE_IN_BITS = 16 +FORMAT = I2S.MONO # only MONO supported in this example +# ======= AUDIO CONFIGURATION ======= + + + +tones = [ + { + 'frequency_start': int(22_000), + 'frequency_end': int(32_000), + 'sample_rate_multiplier': 2 + } +] +""" + { + 'frequency_start': int(4_000), + 'frequency_end': int(8_000), + 'sample_rate_multiplier': 4 + } """ + + + +print('Starting up...') + +led_init() + +led_test() + +wlan = network.WLAN(network.STA_IF) +ssid = secrets['ssid'] + +""" try: + connect_wifi() + print('Searching for update...') + micropython_ota.ota_update('https://iot-sw.adix.link', ota_project_name, ota_branch) +except: + print('Error while checking updates') """ + +# continuously write tone sample buffer to an I2S DAC +print("========== START PLAYBACK ==========") +last_update_time = time_ms() +last_led_update_time = time_ms() + +animation_start_time = time_ms() +animation_duration_ms = 1000 +audio_select = 0 + +sound = 0 + +def select_next_tone_range(next_tone_id): + global sound + global animation_duration_ms + global animation_start_time + global animation_direction + global TONE_START_FREQUENCY_IN_HZ + global TONE_END_FREQUENCY_IN_HZ + global sample_rate_multiplier + + animation_start_time = time_ms() + animation_duration_ms = random.randint(1000, 3000) + + if next_tone_id >= len(tones): + sound = 0 + else: + sound = 1 + animation_direction = random.randint(0,1) + + next_tone = tones[next_tone_id] + + tone_start_frequency_in_hz = int(next_tone['frequency_start']) + tone_end_frequency_in_hz = int(next_tone['frequency_end']) + + tone_diff_half = int(tone_end_frequency_in_hz - tone_start_frequency_in_hz) / 2 + + start_frequency = random.randint(tone_start_frequency_in_hz, int(tone_start_frequency_in_hz + tone_diff_half)) + end_frequency = random.randint(int(start_frequency + tone_diff_half), tone_end_frequency_in_hz) + + if end_frequency < start_frequency + tone_diff_half / 3: + end_frequency = start_frequency + tone_diff_half / 3 + + TONE_START_FREQUENCY_IN_HZ = start_frequency + TONE_END_FREQUENCY_IN_HZ = end_frequency + + sample_rate_multiplier = next_tone['sample_rate_multiplier'] + +def set_next_tone(): + global audio_out_0 + global samples_0 + global audio_out_1 + global samples_1 + global audio_select + global animation_start_time + global animation_duration_ms + global animation_direction + global TONE_START_FREQUENCY_IN_HZ + global TONE_END_FREQUENCY_IN_HZ + global sample_rate_multiplier + + global I2S_ID + global SCK_PIN + global WS_PIN + global SD_PIN + global BUFFER_LENGTH_IN_BYTES + global FORMAT + + delta_frequency = TONE_END_FREQUENCY_IN_HZ - TONE_START_FREQUENCY_IN_HZ + current_delta_frequency = int(delta_frequency * (time_ms() - animation_start_time) / animation_duration_ms) + + if animation_direction == 0: + current_frequency = TONE_START_FREQUENCY_IN_HZ + current_delta_frequency + random.randint(0, int(delta_frequency / 8)) + else: + current_frequency = TONE_END_FREQUENCY_IN_HZ - current_delta_frequency - random.randint(0, int(delta_frequency / 8)) + + current_sample_rate_in_hz = int(current_frequency * sample_rate_multiplier) + + audio_out = I2S( + I2S_ID, #I2S_ID, + sck=Pin(SCK_PIN), + ws=Pin(WS_PIN), + sd=Pin(SD_PIN), + mode=I2S.TX, + bits=SAMPLE_SIZE_IN_BITS, + format=FORMAT, + rate=current_sample_rate_in_hz, + ibuf=BUFFER_LENGTH_IN_BYTES, + ) + samples = make_tone(current_sample_rate_in_hz, SAMPLE_SIZE_IN_BITS, current_frequency) + + if audio_select == 1: + audio_out_0 = audio_out + samples_0 = samples + audio_select = 0 + else: + audio_out_1 = audio_out + samples_1 = samples + audio_select = 1 + + +select_next_tone_range(0) + +def play_sound(): + global sound + global audio_select + global audio_out_0 + global samples_0 + global audio_out_1 + global samples_1 + global run + + while run: + try: + if sound > 0: + if audio_select == 0: + audio_out_0.write(samples_0) + else: + audio_out_1.write(samples_1) + + except KeyboardInterrupt: + break + except Exception as e: + print("caught exception in audio thread {} {}".format(type(e).__name__, e)) + sleep(1) + + _thread.exit() + +select_next_tone_range(0) +set_next_tone() + +mode = 0 +run = True +second_thread = _thread.start_new_thread(play_sound, ()) + +while True: + try: + current_time = time_ms() + + if current_time - last_led_update_time > 20: + update_leds() + last_led_update_time = current_time + + # Check Update + #try: + # if current_time - last_update_time > 36_000_000: + # version_changed, remote_version = micropython_ota.check_version('https://iot-sw.adix.link', ota_project_name, ota_branch) + # if version_changed: + # send_notification(title = 'Updating to ' + remote_version, tags = 'new') + # if ota_soft_reset_device: + # print(f'Found new version {remote_version}, soft-resetting device...') + # machine.soft_reset() + # else: + # print(f'Found new version {remote_version}, hard-resetting device...') + # machine.reset() + # else: + # print('No new version available') + # last_update_time = current_time + #except: + # pass + + #print(str(current_time) + ' ' + str(last_update_time)) + if current_time - last_update_time > 500: + if current_time >= animation_start_time + animation_duration_ms: + mode = random.randint(0,3) + + if mode == 0: + sound = 0 + else: #mode=1 is constant freq, mode=2 is changing freq + next_tone_id = random.randint(0, len(tones)-1) + select_next_tone_range(next_tone_id) + set_next_tone() + + gc.collect() + elif sound > 0 and mode == 2: + set_next_tone() + + last_update_time = current_time + + except KeyboardInterrupt: + break + except Exception as e: + print("caught exception in main loop {} {}".format(type(e).__name__, e)) + sys.print_exception(e) + sleep(1) + +# cleanup +run = False +audio_out_0.deinit() +audio_out_1.deinit() +print("Done") diff --git a/micropython_ota.py b/micropython_ota.py new file mode 100644 index 0000000..68dcd70 --- /dev/null +++ b/micropython_ota.py @@ -0,0 +1,115 @@ +# Based on: +# https://raw.githubusercontent.com/olivergregorius/micropython_ota/main/micropython_ota.py + +import machine +import ubinascii +import uos +import urequests +import os + +def reset_version(): + try: + os.remove('version') + except: + pass + +def check_version(host, project, branch, auth=None, timeout=5) -> (bool, str): + current_version = '' + try: + if 'version' in uos.listdir(): + with open('version', 'r') as current_version_file: + current_version = current_version_file.readline().strip() + + if auth: + response = urequests.get(f'{host}/{project}/version_{branch}', headers={'Authorization': f'Basic {auth}'}, timeout=timeout) + else: + response = urequests.get(f'{host}/{project}/version_{branch}', timeout=timeout) + response_status_code = response.status_code + response_text = response.text + response.close() + if response_status_code != 200: + print(f'Remote version file {host}/{project}/version_{branch} not found') + return False, current_version + remote_version = response_text.strip() + return current_version != remote_version, remote_version + except Exception as ex: + print(f'Something went wrong: {ex}') + return False, current_version + + +def generate_auth(user=None, passwd=None) -> str | None: + if not user and not passwd: + return None + if (user and not passwd) or (passwd and not user): + raise ValueError('Either only user or pass given. None or both are required.') + auth_bytes = ubinascii.b2a_base64(f'{user}:{passwd}'.encode()) + return auth_bytes.decode().strip() + + +def ota_update(host, project, branch='stable', use_version_prefix=False, user=None, passwd=None, hard_reset_device=True, soft_reset_device=False, timeout=5) -> None: + all_files_found = True + auth = generate_auth(user, passwd) + prefix_or_path_separator = '_' if use_version_prefix else '/' + try: + version_changed, remote_version = check_version(host, project, branch, auth=auth, timeout=timeout) + if version_changed: + try: + uos.mkdir('tmp') + except: + pass + + if auth: + response = urequests.get(f'{host}/{project}/{remote_version}{prefix_or_path_separator}.files', headers={'Authorization': f'Basic {auth}'}, timeout=timeout) + else: + response = urequests.get(f'{host}/{project}/{remote_version}{prefix_or_path_separator}.files', timeout=timeout) + files = str.split(response.text, '\n') + + while("" in files): + files.remove("") + + for filename in files: + + if auth: + response = urequests.get(f'{host}/{project}/{remote_version}{prefix_or_path_separator}{filename}', headers={'Authorization': f'Basic {auth}'}, timeout=timeout) + else: + response = urequests.get(f'{host}/{project}/{remote_version}{prefix_or_path_separator}{filename}', timeout=timeout) + response_status_code = response.status_code + response_text = response.text + response.close() + if response_status_code != 200: + print(f'Remote source file {host}/{project}/{remote_version}{prefix_or_path_separator}{filename} not found') + all_files_found = False + continue + with open(f'tmp/{filename}', 'w') as source_file: + source_file.write(response_text) + if all_files_found: + for filename in files: + with open(f'tmp/{filename}', 'r') as source_file, open(filename, 'w') as target_file: + target_file.write(source_file.read()) + uos.remove(f'tmp/{filename}') + try: + uos.rmdir('tmp') + except: + pass + with open('version', 'w') as current_version_file: + current_version_file.write(remote_version) + if soft_reset_device: + print('Soft-resetting device...') + machine.soft_reset() + if hard_reset_device: + print('Hard-resetting device...') + machine.reset() + except Exception as ex: + print(f'Something went wrong: {ex}') + + +def check_for_ota_update(host, project, branch='stable', user=None, passwd=None, timeout=5, soft_reset_device=False): + auth = generate_auth(user, passwd) + version_changed, remote_version = check_version(host, project, branch, auth=auth, timeout=timeout) + if version_changed: + if soft_reset_device: + print(f'Found new version {remote_version}, soft-resetting device...') + #machine.soft_reset() + else: + print(f'Found new version {remote_version}, hard-resetting device...') + #machine.reset()