diff --git a/.forgejo/workflows/push.yaml b/.forgejo/workflows/push.yaml new file mode 100644 index 0000000..cabc4e2 --- /dev/null +++ b/.forgejo/workflows/push.yaml @@ -0,0 +1,136 @@ +name: "BTClock CI" + +on: + push: + tags: + - "*" + workflow_dispatch: + +jobs: + build: + runs-on: docker + container: + image: ghcr.io/catthehacker/ubuntu:js-22.04 + permissions: + contents: write + checks: write + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: yarn + cache-dependency-path: "**/yarn.lock" + - uses: actions/cache@v4 + with: + path: | + ~/.cache/pip + ~/.platformio/.cache + ~/data/node_modules + .pio + data/node_modules + key: ${{ runner.os }}-pio + - uses: actions/setup-python@v5 + with: + python-version: "3.9" + cache: "pip" + - name: Get current date + id: dateAndTime + shell: bash + run: echo "dateAndTime=$(date +'%Y-%m-%d-%H:%M')" >> $GITHUB_OUTPUT + - name: Install PlatformIO Core + shell: bash + run: pip install --upgrade platformio + - name: Build BTClock firmware + shell: bash + run: pio run + - name: Build BTClock filesystem + shell: bash + run: pio run --target buildfs + - name: Copy bootloader to output folder + run: cp ~/.platformio/packages/framework-arduinoespressif32/tools/partitions/boot_app0.bin .pio + - name: Upload artifacts + uses: https://code.forgejo.org/forgejo/upload-artifact@v4 + with: + include-hidden-files: true + retention-days: 1 + name: prepared-outputs + path: .pio/**/*.bin + merge: + runs-on: docker + container: + image: ghcr.io/catthehacker/ubuntu:js-22.04 + permissions: + contents: write + checks: write + needs: build + continue-on-error: true + strategy: + matrix: + chip: + - name: lolin_s2_mini + version: esp32s2 + - name: lolin_s3_mini + version: esp32s3 + - name: orangeclock + version: esp32s3 + epd_variant: [213epd, 29epd] + exclude: + - chip: { name: orangeclock, version: esp32s3 } + epd_variant: 213epd + steps: + - uses: https://code.forgejo.org/forgejo/download-artifact@v4 + with: + name: prepared-outputs + path: .pio + - name: Install esptools.py + run: pip install --upgrade esptool + - name: Create merged firmware binary + run: mkdir -p ${{ matrix.chip.name }}_${{ matrix.epd_variant }} && esptool.py --chip ${{ matrix.chip.version }} merge_bin -o ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${{ matrix.chip.name }}_${{ matrix.epd_variant }}.bin --flash_mode dio 0x0000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/bootloader.bin 0x8000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/partitions.bin 0xe000 .pio/boot_app0.bin 0x10000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/firmware.bin 0x369000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/littlefs.bin + + - name: Create checksum for firmware + shell: bash + run: shasum -a 256 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/firmware.bin | awk '{print $1}' > ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${{ matrix.chip.name }}_${{ matrix.epd_variant }}_firmware.bin.sha256 + + - name: Create checksum for merged binary + run: shasum -a 256 ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${{ matrix.chip.name }}_${{ matrix.epd_variant }}.bin | awk '{print $1}' > ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${{ matrix.chip.name }}_${{ matrix.epd_variant }}.sha256 + + - name: Upload artifacts + uses: https://code.forgejo.org/forgejo/upload-artifact@v4 + with: + name: build-${{ matrix.chip.name }}-${{ matrix.epd_variant }} + path: | + ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/*.bin + ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/*.sha256 + release: + runs-on: docker + permissions: + contents: write + checks: write + needs: merge + steps: + - name: Download matrix outputs + uses: https://code.forgejo.org/forgejo/download-artifact@v4 + with: + pattern: build-* + merge-multiple: false + path: temp + - name: Copy files + run: | + mkdir -p release + find temp -type f \( -name "*.bin" -o -name "*.sha256" \) -exec cp -f {} release/ \; + - name: Create release + uses: https://code.forgejo.org/actions/forgejo-release@v2.4.0 + with: + url: "https://git.btclock.dev" + repo: "${{ github.repository }}" + direction: upload + tag: "${{ github.ref_name }}" + sha: "${{ github.sha }}" + release-dir: release + token: ${{ secrets.TOKEN }} + override: ${{ github.ref_type != 'tag' && github.ref_name != 'main' }} + prerelease: ${{ github.ref_type != 'tag' && github.ref_name != 'main' }} + release-notes-assistant: false diff --git a/.github/actions/install-build/action.yml b/.github/actions/install-build/action.yml index 3850177..891a8a5 100644 --- a/.github/actions/install-build/action.yml +++ b/.github/actions/install-build/action.yml @@ -4,11 +4,11 @@ description: "Install and build" runs: using: "composite" steps: - # - uses: actions/setup-node@v4 - # with: - # node-version: lts/* - # cache: yarn - # cache-dependency-path: '**/yarn.lock' + - uses: actions/setup-node@v4 + with: + node-version: lts/* + cache: yarn + cache-dependency-path: '**/yarn.lock' - uses: actions/cache@v3 with: path: | diff --git a/.github/workflows/tagging.yml b/.github/workflows/tagging.yml index 82fb7a0..5643d3b 100644 --- a/.github/workflows/tagging.yml +++ b/.github/workflows/tagging.yml @@ -22,20 +22,26 @@ jobs: - name: Upload artifacts uses: actions/upload-artifact@v4 with: - name: build-outputs - path: .pio + retention-days: 1 + name: prepared-outputs + path: .pio/**/*.bin build: needs: prepare + continue-on-error: true strategy: matrix: - epd_variant: [213epd, 29epd] chip: - name: lolin_s2_mini version: esp32s2 - # chips: - # - name: lolin_s3_mini - # version: esp32s3 + - name: lolin_s3_mini + version: esp32s3 + - name: orangeclock + version: esp32s3 + epd_variant: [213epd, 29epd] + exclude: + - chip: orangeclock + epd_variant: 213epd runs-on: ubuntu-latest permissions: contents: write @@ -43,12 +49,12 @@ jobs: steps: - uses: actions/download-artifact@v4 with: - name: build-outputs + name: prepared-outputs path: .pio - name: Install esptools.py run: pip install --upgrade esptool - name: Create merged firmware binary - run: mkdir -p ${{ matrix.chip.name }}_${{ matrix.epd_variant }} && esptool.py --chip ${{ matrix.chip.version }} merge_bin -o ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${{ matrix.chip.name }}_${{ matrix.epd_variant }}.bin --flash_mode dio 0x0000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/bootloader.bin 0x8000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/partitions.bin 0xe000 ~/.pio/boot_app0.bin 0x10000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/firmware.bin 0x369000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/littlefs.bin + run: mkdir -p ${{ matrix.chip.name }}_${{ matrix.epd_variant }} && esptool.py --chip ${{ matrix.chip.version }} merge_bin -o ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${{ matrix.chip.name }}_${{ matrix.epd_variant }}.bin --flash_mode dio 0x0000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/bootloader.bin 0x8000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/partitions.bin 0xe000 .pio/boot_app0.bin 0x10000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/firmware.bin 0x369000 .pio/build/${{ matrix.chip.name }}_${{ matrix.epd_variant }}/littlefs.bin - name: Create checksum for merged binary run: shasum -a 256 ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${{ matrix.chip.name }}_${{ matrix.epd_variant }}.bin | awk '{print $1}' > ${{ matrix.chip.name }}_${{ matrix.epd_variant }}/${{ matrix.chip.name }}_${{ matrix.epd_variant }}.sha256 @@ -79,11 +85,12 @@ jobs: - name: Download matrix outputs uses: actions/download-artifact@v4 with: - name: build-* + pattern: build-* + merge-multiple: true - name: Create release uses: ncipollo/release-action@v1 with: - artifacts: "*/*.bin,*/*.sha256" + artifacts: "**/*.bin,**/*.sha256" allowUpdates: true removeArtifacts: true makeLatest: true \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..55b6d4f --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "data"] + path = data + url = https://git.btclock.dev/btclock/oc-webui.git diff --git a/README.md b/README.md new file mode 100644 index 0000000..1fb871f --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# OrangeBTClock (working title) + +[![BTClock CI](https://github.com/btclock/OrangeBTClock/actions/workflows/tagging.yml/badge.svg)](https://github.com/btclock/OrangeBTClock/actions/workflows/tagging.yml) + +Firmware for cheap ESP32-S2/S3 hardware combined with a eInk display + +See releases for prebuilt binaries, ready to flash (e.g. with the [esphome web flasher](https://web.esphome.io/)) + +## Development + +- [PlatformIO](https://platformio.org/platformio-ide). +- [Node.js](https://nodejs.org/en) and [yarn](https://yarnpkg.com/). + diff --git a/boards/orangeclock.json b/boards/orangeclock.json new file mode 100644 index 0000000..7ea7754 --- /dev/null +++ b/boards/orangeclock.json @@ -0,0 +1,62 @@ +{ + "build": { + "arduino":{ + "ldscript": "esp32s3_out.ld", + "partitions": "default_8MB.csv", + "memory_type": "qio_opi" + }, + "core": "esp32", + "extra_flags": [ + "-DBOARD_HAS_PSRAM", + "-DARDUINO_ORANGECLOCK", + "-DARDUINO_ESP32S3_DEV", + "-DIS_ORANGECLOCK", + "-DARDUINO_USB_MODE=0", + "-DARDUINO_RUNNING_CORE=1", + "-DARDUINO_EVENT_RUNNING_CORE=1", + "-DARDUINO_USB_CDC_ON_BOOT=1" + ], + "f_cpu": "240000000L", + "f_flash": "80000000L", + "flash_mode": "qio", + "psram_type": "opi", + "espidf": { + "sdkconfig_path": "boards" + }, + "hwids": [ + [ + "0x303A", + "0x1001" + ] + ], + "mcu": "esp32s3", + "variant": "esp32s3" + }, + "connectivity": [ + "bluetooth", + "wifi" + ], + "debug": { + "default_tool": "esp-builtin", + "onboard_tools": [ + "esp-builtin" + ], + "openocd_target": "esp32s3.cfg" + }, + "frameworks": [ + "arduino", + "espidf" + ], + "name": "OrangeClock", + "upload": { + "flash_size": "8MB", + "maximum_ram_size": 327680, + "maximum_size": 16777216, + "use_1200bps_touch": true, + "wait_for_upload_port": true, + "require_upload_port": true, + "speed": 460800 + }, + "url": "http://github.com/btclock", + "vendor": "BTClock" +} \ No newline at end of file diff --git a/data b/data new file mode 160000 index 0000000..8332fec --- /dev/null +++ b/data @@ -0,0 +1 @@ +Subproject commit 8332fec4a1ec0045d91f063617bb441914e7b67a diff --git a/data/build/index.html b/data/build/index.html deleted file mode 100644 index e69de29..0000000 diff --git a/platformio.ini b/platformio.ini index fa6c962..af5bf0f 100644 --- a/platformio.ini +++ b/platformio.ini @@ -9,24 +9,27 @@ ; https://docs.platformio.org/page/projectconf.html [platformio] -data_dir = data/build -default_envs = lolin_s2_mini_213epd, lolin_s2_mini_29epd, lolin_s3_mini_213epd +data_dir = data/build_gz +default_envs = lolin_s2_mini_213epd, lolin_s2_mini_29epd, lolin_s3_mini_213epd, lolin_s3_mini_29epd, orangeclock_29epd [btclock_base] platform = espressif32 framework = arduino +platform_packages = platformio/framework-arduinoespressif32 monitor_speed = 115200 monitor_filters = esp32_exception_decoder, colorize board_build.filesystem = littlefs board_build.partitions = partition.csv +extra_scripts = post:scripts/extra_script.py build_flags = + !python scripts/git_rev.py + -DLAST_BUILD_TIME=$UNIX_TIME lib_deps = - zinggjm/GxEPD2@^1.5.6 + zinggjm/GxEPD2@^1.6.1 https://github.com/tzapu/WiFiManager.git#v2.0.17 - bblanchon/ArduinoJson@^7.0.3 - mathieucarbou/ESP Async WebServer - gilmaimon/ArduinoWebsockets@^0.5.3 - + bblanchon/ArduinoJson@^7.2.1 + mathieucarbou/ESP Async WebServer@^3.0.6 + fastled/FastLED@^3.9.6 [env:lolin_s2_mini] extends = btclock_base board = lolin_s2_mini @@ -57,4 +60,15 @@ build_flags = extends = env:lolin_s3_mini build_flags = ${btclock_base.build_flags} - -D VERSION_EPD_2_9 \ No newline at end of file + -D VERSION_EPD_2_9 + + +[env:orangeclock_29epd] +extends = btclock_base +board = orangeclock +build_flags = + ${btclock_base.build_flags} + -D VERSION_EPD_2_9 + -D IS_ORANGECLOCK + -D BUTTON_PIN=45 + -D NUM_LEDS=2 \ No newline at end of file diff --git a/scripts/extra_script.py b/scripts/extra_script.py new file mode 100644 index 0000000..4a1c118 --- /dev/null +++ b/scripts/extra_script.py @@ -0,0 +1,38 @@ +Import("env") +import os +import gzip +from shutil import copyfileobj, rmtree +from pathlib import Path + +def gzip_file(input_file, output_file): + with open(input_file, 'rb') as f_in: + with gzip.open(output_file, 'wb') as f_out: + copyfileobj(f_in, f_out) + +def process_directory(input_dir, output_dir): + if os.path.exists(output_dir): + rmtree(output_dir) + for root, dirs, files in os.walk(input_dir): + relative_path = os.path.relpath(root, input_dir) + output_root = os.path.join(output_dir, relative_path) + + Path(output_root).mkdir(parents=True, exist_ok=True) + + for file in files: + # if file.endswith(('.html', '.css', '.js')): + input_file_path = os.path.join(root, file) + output_file_path = os.path.join(output_root, file + '.gz') + gzip_file(input_file_path, output_file_path) + print(f'Compressed: {input_file_path} -> {output_file_path}') + + + +# Build web interface before building FS +def before_buildfs(source, target, env): + env.Execute("cd data && yarn && yarn postinstall && yarn build") + input_directory = 'data/dist' + output_directory = 'data/build_gz' + process_directory(input_directory, output_directory) + +os.environ["PUBLIC_BASE_URL"] = "" +env.AddPreAction("$BUILD_DIR/littlefs.bin", before_buildfs) diff --git a/scripts/git_rev.py b/scripts/git_rev.py new file mode 100644 index 0000000..9594475 --- /dev/null +++ b/scripts/git_rev.py @@ -0,0 +1,8 @@ +import subprocess + +revision = ( + subprocess.check_output(["git", "rev-parse", "HEAD"]) + .strip() + .decode("utf-8") +) +print("'-DGIT_REV=\"%s\"'" % revision) \ No newline at end of file diff --git a/src/bitmap.hpp b/src/bitmap.hpp new file mode 100644 index 0000000..72dbda9 --- /dev/null +++ b/src/bitmap.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include +// 'oclogo', 250x37px +const unsigned char epd_bitmap_oclogo [] PROGMEM = { + 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x7f, 0xc3, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xfc, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xf0, 0x0c, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0xe3, 0x9c, 0x07, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x07, 0xc7, 0x9c, 0x03, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf0, 0x00, 0x00, + 0x0f, 0x8f, 0x9c, 0x01, 0xe0, 0x00, 0x07, 0xf0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x00, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf0, 0x00, 0x00, + 0x0f, 0x07, 0x9c, 0x00, 0xf0, 0x00, 0x1f, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x03, 0xff, 0xc0, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf0, 0x00, 0x00, + 0x1e, 0x63, 0x9c, 0x00, 0x78, 0x00, 0x7f, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x07, 0xff, 0xf0, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf0, 0x00, 0x00, + 0x1e, 0x71, 0x9c, 0x00, 0x78, 0x00, 0xff, 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x0f, 0xff, 0xf8, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf0, 0x00, 0x00, + 0x3c, 0xf8, 0x9c, 0x00, 0x38, 0x01, 0xff, 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x1f, 0xff, 0xf8, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x03, 0xf0, 0x00, 0x00, + 0x3c, 0xfc, 0x1c, 0x00, 0x3c, 0x01, 0xfc, 0x1f, 0xe1, 0xf3, 0xc3, 0xfc, 0x03, 0xe7, 0xe0, 0x03, + 0xf3, 0xe0, 0x1f, 0xc0, 0x3f, 0xc1, 0xf0, 0xf8, 0x07, 0xf0, 0x00, 0x7f, 0x03, 0xf0, 0x7e, 0x00, + 0x39, 0xfe, 0x1c, 0x00, 0x1c, 0x03, 0xf8, 0x07, 0xe1, 0xff, 0xdf, 0xff, 0x03, 0xff, 0xf0, 0x0f, + 0xff, 0xe0, 0x7f, 0xf0, 0x3f, 0x80, 0x40, 0xf8, 0x1f, 0xfc, 0x01, 0xff, 0xc3, 0xf0, 0xfe, 0x00, + 0x39, 0xff, 0x1c, 0x00, 0x1c, 0x03, 0xf0, 0x07, 0xf1, 0xff, 0xdf, 0xff, 0x83, 0xff, 0xf8, 0x1f, + 0xff, 0xe0, 0xff, 0xf8, 0x3f, 0x00, 0x00, 0xf8, 0x3f, 0xff, 0x03, 0xff, 0xe3, 0xf1, 0xfc, 0x00, + 0x39, 0xff, 0x9c, 0x00, 0x1c, 0x03, 0xf0, 0x03, 0xf1, 0xff, 0xcf, 0xff, 0x83, 0xff, 0xfc, 0x3f, + 0xff, 0xe1, 0xff, 0xfc, 0x7e, 0x00, 0x00, 0xf8, 0x7f, 0xff, 0x07, 0xff, 0xf3, 0xf3, 0xf8, 0x00, + 0x78, 0x00, 0x1c, 0x00, 0x1c, 0x07, 0xe0, 0x03, 0xf1, 0xff, 0xce, 0x1f, 0xc3, 0xff, 0xfc, 0x3f, + 0x8f, 0xe1, 0xf8, 0x7e, 0x7e, 0x00, 0x00, 0xf8, 0x7f, 0x3f, 0x8f, 0xe3, 0xe3, 0xf7, 0xf0, 0x00, + 0x78, 0x00, 0x1e, 0x00, 0x1c, 0x07, 0xe0, 0x03, 0xf1, 0xfc, 0x00, 0x0f, 0xc3, 0xf0, 0xfc, 0x7f, + 0x07, 0xe3, 0xf0, 0x3e, 0x7e, 0x00, 0x00, 0xf8, 0xfc, 0x1f, 0x8f, 0xc1, 0xc3, 0xff, 0xe0, 0x00, + 0x78, 0x00, 0x1f, 0x00, 0x1c, 0x07, 0xe0, 0x03, 0xf1, 0xf8, 0x00, 0x0f, 0xc3, 0xe0, 0x7c, 0x7e, + 0x03, 0xe3, 0xf0, 0x3e, 0x7e, 0x00, 0x00, 0xf8, 0xfc, 0x0f, 0xcf, 0x80, 0x03, 0xff, 0xc0, 0x00, + 0x39, 0xff, 0x0f, 0x80, 0x1c, 0x07, 0xe0, 0x03, 0xf1, 0xf8, 0x07, 0xff, 0xc3, 0xe0, 0x7e, 0x7e, + 0x03, 0xe3, 0xff, 0xfe, 0x7e, 0x00, 0x00, 0xf8, 0xfc, 0x0f, 0xdf, 0x80, 0x03, 0xff, 0xc0, 0x00, + 0x39, 0xff, 0x07, 0xc0, 0x1c, 0x03, 0xf0, 0x03, 0xf1, 0xf8, 0x1f, 0xff, 0xc3, 0xe0, 0x7e, 0x7e, + 0x03, 0xe3, 0xff, 0xfe, 0x7f, 0x00, 0x00, 0xf8, 0xf8, 0x0f, 0xdf, 0x80, 0x03, 0xff, 0xe0, 0x00, + 0x39, 0xfe, 0x23, 0xe0, 0x1c, 0x03, 0xf0, 0x07, 0xf1, 0xf8, 0x1f, 0xff, 0xc3, 0xe0, 0x7e, 0x7f, + 0x07, 0xe3, 0xf0, 0x00, 0x3f, 0x00, 0x40, 0xf8, 0xfc, 0x0f, 0xcf, 0x80, 0x03, 0xff, 0xe0, 0x00, + 0x3c, 0xfc, 0x61, 0xf0, 0x3c, 0x03, 0xf8, 0x0f, 0xe1, 0xf8, 0x3f, 0x0f, 0xc3, 0xe0, 0x7e, 0x3f, + 0x8f, 0xe3, 0xf0, 0x00, 0x3f, 0x80, 0xe0, 0xf8, 0xfc, 0x1f, 0x8f, 0xc1, 0x83, 0xff, 0xf0, 0x00, + 0x3c, 0xf8, 0xe0, 0xf8, 0x38, 0x01, 0xff, 0x3f, 0xe1, 0xf8, 0x3f, 0x0f, 0xc3, 0xe0, 0x7e, 0x3f, + 0xff, 0xe1, 0xf8, 0x30, 0x1f, 0xe3, 0xf0, 0xf8, 0x7e, 0x3f, 0x8f, 0xe3, 0xe3, 0xf3, 0xf8, 0x00, + 0x1e, 0x71, 0xe4, 0x7c, 0x78, 0x00, 0xff, 0xff, 0xc1, 0xf8, 0x3f, 0x1f, 0xc3, 0xe0, 0x7e, 0x1f, + 0xff, 0xe1, 0xff, 0xf8, 0x1f, 0xff, 0xf8, 0xf8, 0x7f, 0xff, 0x87, 0xff, 0xf3, 0xf1, 0xfc, 0x00, + 0x1e, 0x43, 0xe6, 0x3c, 0x78, 0x00, 0x7f, 0xff, 0x81, 0xf8, 0x1f, 0xff, 0xc3, 0xe0, 0x7e, 0x0f, + 0xff, 0xe0, 0xff, 0xfc, 0x0f, 0xff, 0xf0, 0xf8, 0x3f, 0xff, 0x03, 0xff, 0xe3, 0xf0, 0xfe, 0x00, + 0x0f, 0x07, 0xe7, 0x1c, 0xf0, 0x00, 0x3f, 0xff, 0x01, 0xf8, 0x1f, 0xff, 0xc3, 0xe0, 0x7e, 0x03, + 0xf3, 0xe0, 0x7f, 0xf8, 0x03, 0xff, 0xe0, 0xf8, 0x1f, 0xfe, 0x01, 0xff, 0xc3, 0xf0, 0x7e, 0x00, + 0x0f, 0x8f, 0xe7, 0x81, 0xe0, 0x00, 0x0f, 0xfc, 0x01, 0xf8, 0x07, 0xe7, 0xc3, 0xe0, 0x7e, 0x00, + 0x03, 0xe0, 0x1f, 0xf0, 0x01, 0xff, 0x80, 0xf8, 0x07, 0xf8, 0x00, 0x7f, 0x83, 0xf0, 0x7f, 0x00, + 0x07, 0xc7, 0xe7, 0xe3, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, + 0x07, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x03, 0xe3, 0xe7, 0x87, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, + 0x1f, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x01, 0xf0, 0x00, 0x0f, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, + 0xff, 0xc0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xfc, 0x00, 0x3f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3f, + 0xff, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x7f, 0xc3, 0xfe, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f, + 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x1f, 0xff, 0xf8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, + 0xfc, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x07, 0xff, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + diff --git a/src/config.cpp b/src/config.cpp index 0c30984..3788a9a 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -3,8 +3,12 @@ Preferences preferences; const char *ntpServer = "pool.ntp.org"; -const long gmtOffset_sec = 0; +// const long gmtOffset_sec = 0; const int daylightOffset_sec = 3600; +TaskHandle_t OTAHandle = NULL; +SemaphoreHandle_t xButtonSemaphore = NULL; +const TickType_t debounceDelay = pdMS_TO_TICKS(500); +TickType_t lastButtonPressTime = 0; #define STA_SSID "" #define STA_PASS "" @@ -13,7 +17,7 @@ bool isUpdating = false; void setupTime() { - configTime(gmtOffset_sec, daylightOffset_sec, ntpServer); + configTime(preferences.getInt(SETTING_TIME_OFFSET_MIN), daylightOffset_sec, ntpServer); } void setupPreferences() @@ -37,16 +41,60 @@ void setupPreferences() { preferences.putString(SETTING_CURRENCY, CURRENCY_USD); } + + if (!preferences.isKey(SETTING_HOSTNAME_PREFIX)) + { + preferences.putString(SETTING_HOSTNAME_PREFIX, "oc"); + } + + if (!preferences.isKey(SETTING_MEMPOOL_INSTANCE)) + { + preferences.putString(SETTING_MEMPOOL_INSTANCE, "https://mempool.space"); + } + + if (!preferences.isKey(SETTING_TIME_FORMAT)) + { + preferences.putString(SETTING_TIME_FORMAT, "%H:%M:%S"); + } + + if (!preferences.isKey(SETTING_DATE_FORMAT)) + { + preferences.putString(SETTING_DATE_FORMAT, "%d-%m-%Y"); + } + + if (!preferences.isKey(SETTING_DECIMAL_SEPARATOR)) + { + preferences.putChar(SETTING_DECIMAL_SEPARATOR, '.'); + } + + if (!preferences.isKey(SETTING_POWER_SAVE_MODE)) + { + preferences.putBool(SETTING_POWER_SAVE_MODE, false); + } + + if (!preferences.isKey(SETTING_TIME_OFFSET_MIN)) + { + preferences.putInt(SETTING_TIME_OFFSET_MIN, 0); + } } void setupWifi() { + uint8_t mac[6]; + WiFi.macAddress(mac); + unsigned long seed = 0; + for (int i = 0; i < 6; i++) + { + seed += (unsigned long)mac[i] << ((i & 1) * 8); + } + randomSeed(seed); + // WiFi.begin(, "); - WiFi.setAutoConnect(true); WiFi.setAutoReconnect(true); WiFiManager wm; +#ifndef ARDUINO_ORANGECLOCK // Touch pin 14 to reset if (touchRead(14) > 9000) { @@ -63,16 +111,12 @@ void setupWifi() wm.resetSettings(); } } +#endif - byte mac[6]; - WiFi.macAddress(mac); String softAP_SSID = String("OrangeBTClock"); WiFi.setHostname(softAP_SSID.c_str()); - String softAP_password = - base64::encode(String(mac[2], 16) + String(mac[4], 16) + - String(mac[5], 16) + String(mac[1], 16)) - .substring(2, 10); + String softAP_password = getAPPassword(); // wm.setConfigPortalTimeout(preferences.getUInt("wpTimeout", 600)); wm.setWiFiAutoReconnect(false); @@ -109,6 +153,7 @@ void setupWifi() Serial.println("WiFi connected"); Serial.println("IP address: "); Serial.println(WiFi.localIP()); + epdShowIp(); // WiFi.setTxPower(WIFI_POWER_8_5dBm); // enableWiFi(); } @@ -135,7 +180,6 @@ void wakeModemSleep() void enableWiFi() { - adc_power_on(); delay(200); WiFi.disconnect(false); // Reconnect the network @@ -160,7 +204,6 @@ void enableWiFi() void disableWiFi() { - adc_power_off(); WiFi.disconnect(true); // Disconnect from the network WiFi.mode(WIFI_OFF); // Switch WiFi off Serial.println(""); @@ -201,4 +244,104 @@ void setupOTA() else if (error == OTA_END_ERROR) Serial.println("End Failed"); }); ArduinoOTA.begin(); + + xTaskCreatePinnedToCore( + OTAUpdateTask, // Task function + "OTAUpdateTask", // Task name + 4096, // Stack size + NULL, // Task parameters + 1, // Priority (higher value means higher priority) + &OTAHandle, // Task handle + 0 // Core to run the task (0 or 1) + ); +} + +void OTAUpdateTask(void *pvParameters) +{ + for (;;) + { + ArduinoOTA.handle(); // Handle OTA updates + vTaskDelay(1000 / portTICK_PERIOD_MS); // Delay to avoid high CPU usage + } +} + +void HandleButtonTask(void *pvParameters) +{ + for (;;) + { + if (xSemaphoreTake(xButtonSemaphore, portMAX_DELAY) == pdTRUE) + { + TickType_t currentTime = xTaskGetTickCount(); + if ((currentTime - lastButtonPressTime) >= debounceDelay) + { + lastButtonPressTime = currentTime; + + Serial.println("Button Pressed"); + + #ifdef NUM_LEDS + leds[0] = CRGB::SkyBlue; + leds[1] = CRGB::Black; + + FastLED.show(); + + vTaskDelay(100); + + leds[0] = CRGB::Black; + leds[1] = CRGB::DarkOrange; + + FastLED.show(); + + vTaskDelay(100); + + leds[0] = CRGB::Black; + leds[1] = CRGB::Black; + + FastLED.show(); + #endif + } + } + } +} + +char getCurrencyIcon() +{ + char ret; + String currency = preferences.getString(SETTING_CURRENCY); + if (currency.equals(CURRENCY_USD)) + { + ret = ICON_DOLLAR; + } + else if (currency.equals(CURRENCY_EUR)) + { + ret = ICON_EURO; + } + else if (currency.equals(CURRENCY_GBP)) + { + ret = ICON_POUND; + } + else if (currency.equals(CURRENCY_JPY)) + { + ret = ICON_YEN; + } + + return ret; +} + +void IRAM_ATTR onButtonPress() +{ + xSemaphoreGiveFromISR(xButtonSemaphore, NULL); +} + +void setupButtonISR() +{ + xButtonSemaphore = xSemaphoreCreateBinary(); + + xTaskCreatePinnedToCore( + HandleButtonTask, // Task function + "Button Task", // Task name + 2048, // Stack size (bytes) + NULL, // Task parameters + 1, // Priority (1 is default) + NULL, // Task handle + 0); // Core to run the task (0 or 1) } \ No newline at end of file diff --git a/src/config.hpp b/src/config.hpp index f3bb73d..8a1aaa4 100644 --- a/src/config.hpp +++ b/src/config.hpp @@ -3,7 +3,6 @@ #include #include #include "shared.hpp" -#include "driver/adc.h" #include #include #include "epd.hpp" @@ -15,7 +14,12 @@ void setupTime(); void setupPreferences(); void setupWifi(); void setupOTA(); +void OTAUpdateTask(void *pvParameters); + void wakeModemSleep(); void setModemSleep(); -bool inPowerSaveMode(); \ No newline at end of file +bool inPowerSaveMode(); +char getCurrencyIcon(); +void IRAM_ATTR onButtonPress(); +void setupButtonISR(); \ No newline at end of file diff --git a/src/data.cpp b/src/data.cpp index b5f7eca..eab3341 100644 --- a/src/data.cpp +++ b/src/data.cpp @@ -1,10 +1,11 @@ #include "data.hpp" -const String mempoolInstance = "https://mempool.space"; +//const String mempoolInstance = "https://mempool.space"; -const String mempoolPriceApiUrl = mempoolInstance + "/api/v1/prices"; -const String mempoolBlockApiUrl = mempoolInstance + "/api/blocks/tip/height"; -const String mempoolFeeApiUrl = mempoolInstance + "/api/v1/fees/recommended"; +const String mempoolPriceApi = "/api/v1/prices"; +const String mempoolBlockApi = "/api/blocks/tip/height"; +const String mempoolFeeApi = "/api/v1/fees/recommended"; +const String mempoolMedianFeeApi = "/api/v1/fees/mempool-blocks"; uint lastPrice; uint lastBlock; @@ -14,7 +15,7 @@ uint getPrice() HTTPClient http; // Send HTTP request to CoinGecko API - http.begin(mempoolPriceApiUrl); + http.begin(preferences.getString(SETTING_MEMPOOL_INSTANCE) + mempoolPriceApi); int httpCode = http.GET(); @@ -44,7 +45,7 @@ uint getBlock() HTTPClient http; // Send HTTP request to CoinGecko API - http.begin(mempoolBlockApiUrl); + http.begin(preferences.getString(SETTING_MEMPOOL_INSTANCE) + mempoolBlockApi); int httpCode = http.GET(); @@ -71,7 +72,7 @@ String getMempoolFees() HTTPClient http; // Send HTTP request to CoinGecko API - http.begin(mempoolFeeApiUrl); + http.begin(preferences.getString(SETTING_MEMPOOL_INSTANCE) + mempoolFeeApi); int httpCode = http.GET(); @@ -95,4 +96,114 @@ String getMempoolFees() http.end(); return ""; +} + +uint getMempoolFeesMedian() +{ + HTTPClient http; + + // Send HTTP request to CoinGecko API + http.begin(preferences.getString(SETTING_MEMPOOL_INSTANCE) + mempoolMedianFeeApi); + + int httpCode = http.GET(); + + if (httpCode == 200) + { + char feeString[20]; + String payload = http.getString(); + JsonDocument doc; + deserializeJson(doc, payload); + + snprintf(feeString, 20, "L: %d M: %d H: %d", doc["hourFee"].as(), doc["halfHourFee"].as(), doc["fastestFee"].as()); + + return round(doc[0]["medianFee"].as()); + + // preferences.putUInt("lastPrice", eurPrice); + } + else + { + Serial.printf("HTTP GET request mempool median fees failed with error: %s\n", http.errorToString(httpCode).c_str()); + } + http.end(); + + return 0; +} + +double getSupplyAtBlock(std::uint32_t blockNr) +{ + if (blockNr >= 33 * 210000) + { + return 20999999.9769; + } + + const int initialBlockReward = 50; // Initial block reward + const int halvingInterval = 210000; // Number of blocks before halving + + int halvingCount = blockNr / halvingInterval; + double totalBitcoinInCirculation = 0; + + for (int i = 0; i < halvingCount; ++i) + { + totalBitcoinInCirculation += halvingInterval * initialBlockReward * std::pow(0.5, i); + } + + totalBitcoinInCirculation += (blockNr % halvingInterval) * initialBlockReward * std::pow(0.5, halvingCount); + + return totalBitcoinInCirculation; +} + +String formatNumberWithSuffix(std::uint64_t num, int numCharacters) +{ + static char result[20]; // Adjust size as needed + const long long quadrillion = 1000000000000000LL; + const long long trillion = 1000000000000LL; + const long long billion = 1000000000; + const long long million = 1000000; + const long long thousand = 1000; + + double numDouble = (double)num; + int numDigits = (int)log10(num) + 1; + char suffix; + + if (num >= quadrillion || numDigits > 15) + { + numDouble /= quadrillion; + suffix = 'Q'; + } + else if (num >= trillion || numDigits > 12) + { + numDouble /= trillion; + suffix = 'T'; + } + else if (num >= billion || numDigits > 9) + { + numDouble /= billion; + suffix = 'B'; + } + else if (num >= million || numDigits > 6) + { + numDouble /= million; + suffix = 'M'; + } + else if (num >= thousand || numDigits > 3) + { + numDouble /= thousand; + suffix = 'K'; + } + else + { + sprintf(result, "%llu", (unsigned long long)num); + return result; + } + + // Add suffix + int len = snprintf(result, sizeof(result), "%.0f%c", numDouble, suffix); + + // If there's room, add decimal places + if (len < numCharacters) + { + snprintf(result, sizeof(result), "%.*f%c", numCharacters - len - 1, numDouble, suffix); + } + + return result; } \ No newline at end of file diff --git a/src/data.hpp b/src/data.hpp index 9b2e363..3c7e94e 100644 --- a/src/data.hpp +++ b/src/data.hpp @@ -9,4 +9,7 @@ uint getPrice(); uint getBlock(); -String getMempoolFees(); \ No newline at end of file +String getMempoolFees(); +uint getMempoolFeesMedian(); +double getSupplyAtBlock(std::uint32_t blockNr); +String formatNumberWithSuffix(std::uint64_t num, int numCharacters); \ No newline at end of file diff --git a/src/epd.cpp b/src/epd.cpp index 9552f3c..ec23ee6 100644 --- a/src/epd.cpp +++ b/src/epd.cpp @@ -9,6 +9,10 @@ String currentRow1 = ""; String currentRow2 = ""; String currentRow3 = ""; +char currentIcon1; +char currentIcon2; +char currentIcon3; + void setupDisplay() { display.init(0, true); @@ -18,17 +22,26 @@ void setupDisplay() display.setRotation(1); display.setFont(&Antonio_SemiBold20pt7b); display.setTextColor(GxEPD_WHITE); - int16_t tbx, tby; - uint16_t tbw, tbh; - display.getTextBounds("OrangeBTClock", 0, 0, &tbx, &tby, &tbw, &tbh); - // center the bounding box by transposition of the origin: - uint16_t x = ((display.width() - tbw) / 2) - tbx; - uint16_t y = ((display.height() - tbh) / 2) - tby; + // int16_t tbx, tby; + // uint16_t tbw, tbh; + // display.getTextBounds("OrangeBTClock", 0, 0, &tbx, &tby, &tbw, &tbh); + // // center the bounding box by transposition of the origin: + // uint16_t x = ((display.width() - tbw) / 2) - tbx; + // uint16_t y = ((display.height() - tbh) / 2) - tby; display.fillScreen(GxEPD_BLACK); - display.setCursor(x, y); - display.print("OrangeBTClock"); + // display.setCursor(x, y); +// display.print("OrangeBTClock"); + +// display.drawImage(epd_bitmap_allArray[0], GxEPD_WHITE, 0,0 250,37); + + int xPos = (display.width() - 250) / 2; + int yPos = (display.height() - 37) / 2; + display.drawBitmap(xPos,yPos, epd_bitmap_oclogo, 250, 37, GxEPD_WHITE); display.display(false); + display.setCursor(0, 37); + + // display.fillScreen(GxEPD_WHITE); // display.drawLine(0, 10, display.width(), 10, GxEPD_BLACK); // display.drawLine(0, row2, display.width(), row2, GxEPD_BLACK); @@ -43,11 +56,11 @@ void setupDisplay() // display.display(true); - display.setRotation(1); - // display.fillRect(0, row1, display.width(), 54, GxEPD_BLACK); - display.displayWindow(0, row1, display.width(), row2); + // display.setRotation(1); + // // display.fillRect(0, row1, display.width(), 54, GxEPD_BLACK); + // display.displayWindow(0, row1, display.width(), row2); - display.display(true); + // display.display(false); // display.fillRect(0, row2, display.width(), 54, GxEPD_BLACK); // display.displayWindow(0, row2, display.width(), 54); @@ -60,8 +73,27 @@ void setupDisplay() // display.display(true); } -void updateRow2(String c) +void epdShowIp() { + display.setRotation(1); + display.setFont(&LibreFranklin_SemiBold10pt7b); + display.setTextColor(GxEPD_WHITE); + String ipStr = WiFi.localIP().toString(); + int16_t tbx, tby; + uint16_t tbw, tbh; + display.getTextBounds(ipStr, 0, 0, &tbx, &tby, &tbw, &tbh); + // center the bounding box by transposition of the origin: + uint16_t x = ((display.width() - tbw) / 2) - tbx; + uint16_t y = ((display.height() - tbh) / 2) - tby + 37; + display.setCursor(x, y); + display.println(WiFi.localIP()); + display.display(true); +} + +void updateRow2(String c, char icon) { + if (c.equals(currentRow2) && icon == currentIcon2) + return; + display.setRotation(1); display.setFont(&ROW2_FONT); display.setTextColor(GxEPD_BLACK); @@ -82,17 +114,23 @@ void updateRow2(String c) display.setFont(&ROW2_ICONFONT); display.setCursor(x, y); - display.print(ICON_BLOCK); + display.print(icon); display.setFont(&ROW2_FONT); display.setCursor(x + ROW2_ICONWIDTH, y); display.print(c); } while (display.nextPage()); // display.display(true); + + currentRow2 = c; + currentIcon2 = icon; } -void updateRow3(String c) +void updateRow3(String c, char icon) { + if (c.equals(currentRow3) && icon == currentIcon3) + return; + display.setRotation(1); display.setFont(&LibreFranklin_SemiBold15pt7b); display.setTextColor(GxEPD_WHITE); @@ -114,13 +152,16 @@ void updateRow3(String c) display.setFont(&orangeclock_icons15pt7b); display.setCursor(x, y); - display.print(ICON_SATS); + display.print(icon); display.setFont(&LibreFranklin_SemiBold15pt7b); display.setCursor(x + ROW3_ICONWIDTH, y); display.print(c); } while (display.nextPage()); + + currentRow3 = c; + currentIcon3 = icon; } void showSetupText(String t) @@ -133,7 +174,7 @@ void showSetupText(String t) // center the bounding box by transposition of the origin: uint16_t x = ((display.width() - (tbw)) / 2) - tbx; uint16_t y = ((display.height() - tbh) / 2) - tby; - + display.setFullWindow(); display.firstPage(); do { @@ -147,8 +188,11 @@ void showSetupText(String t) } while (display.nextPage()); } -void updateRow1(String c) +void updateRow1(String c, char icon) { + if (c.equals(currentRow1) && icon == currentIcon1) + return; + // struct tm timeinfo; // if (!getLocalTime(&timeinfo)) // { @@ -180,13 +224,16 @@ void updateRow1(String c) display.setFont(&ROW1_ICONFONT); display.setCursor(x, y); - display.print(ICON_PIE); + display.print(icon); display.setFont(&ROW1_FONT); display.setCursor(x + ROW1_ICONWIDTH, y); display.print(c); } while (display.nextPage()); + + currentRow1 = c; + currentIcon1 = icon; } void updateRows(String row1Content, String row2Content, String row3Content) diff --git a/src/epd.hpp b/src/epd.hpp index a19d202..c566001 100644 --- a/src/epd.hpp +++ b/src/epd.hpp @@ -2,11 +2,12 @@ #include "shared.hpp" #include "fonts/fonts.hpp" - +#include "bitmap.hpp" void setupDisplay(); void showSetupText(String t); -void updateRow1(String c); -void updateRow2(String c); -void updateRow3(String c); -void updateRows(String row1Content, String row2Content, String row3Content); \ No newline at end of file +void updateRow1(String c, char icon); +void updateRow2(String c, char icon); +void updateRow3(String c, char icon); +void updateRows(String row1Content, String row2Content, String row3Content); +void epdShowIp(); \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index e6e4a87..3cf7b1b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -21,19 +21,44 @@ GxEPD2_BW display = EPD_CLASS(4, 2, 3, 1); GxEPD2_BW display = EPD_CLASS(5, 3, 2, 1); #endif -WiFiClientSecure client; +#ifdef ARDUINO_ORANGECLOCK +GxEPD2_BW display = EPD_CLASS(5, 3, 1, 2); +#endif + +typedef void (*MethodPtr)(String, char); + +MethodPtr methods[] = {nullptr, updateRow1, updateRow2, updateRow3}; + +WiFiClient client; uint currentPrice = 0; String currentBlock = ""; String currentFees = ""; +#ifdef NUM_LEDS +CRGB leds[NUM_LEDS]; +#endif + void setup() { // setCpuFrequencyMhz(40); Serial.begin(115200); - + #ifndef IS_ORANGECLOCK pinMode(LED_BUILTIN, OUTPUT); digitalWrite(LED_BUILTIN, HIGH); + #else + + pinMode(BUTTON_PIN, INPUT_PULLUP); + attachInterrupt(BUTTON_PIN, onButtonPress, FALLING); + + FastLED.addLeds(leds, NUM_LEDS); + leds[0] = CRGB::GreenYellow; + leds[1] = CRGB::OrangeRed; + + FastLED.show(); + + setupButtonISR(); + #endif setupPreferences(); setupDisplay(); @@ -41,20 +66,32 @@ void setup() setupWifi(); setupTime(); - if (!inPowerSaveMode()) + if (!inPowerSaveMode()) { setupWebserver(); setupOTA(); } - client.setInsecure(); + // client.setInsecure(); + #ifndef IS_ORANGECLOCK digitalWrite(LED_BUILTIN, LOW); + #else + leds[0] = CRGB::Black; + leds[1] = CRGB::Black; + + FastLED.show(); + delay(100); + #endif + display.setFullWindow(); + display.clearScreen(GxEPD_WHITE); + display.display(true); } void loop() { - if (isUpdating) { - ArduinoOTA.handle(); + if (isUpdating) + { + delay(1000); return; } @@ -66,132 +103,203 @@ void loop() return; } - client.setInsecure(); + // client.setInsecure(); // - IPAddress res; - uint result = WiFi.hostByName("mempool.space", res); + // IPAddress res; + // uint result = WiFi.hostByName("mempool.space", res); - if (result >= 0) - { - Serial.print("SUCCESS!"); - Serial.println(res.toString()); - } - else - { - WiFi.reconnect(); + // if (result >= 0) + // { + // Serial.print("SUCCESS!"); + // Serial.println(res.toString()); + // } + // else + // { + // WiFi.reconnect(); - while (WiFi.status() != WL_CONNECTED) + // while (WiFi.status() != WL_CONNECTED) + // { + // Serial.print('.'); + // delay(1000); + // } + // } + + for (uint i = 1; i <= 3; i++) + { + String rowContent = ""; + char icon; + char keyName[5]; + snprintf(keyName, sizeof(keyName), "row%d", i); + switch (preferences.getUInt(keyName)) { - Serial.print('.'); - delay(1000); - } - } - - String block = String(getBlock()); - - uint tryCount = 0; - while (block.equals("")) - { - block = getBlock(); - Serial.print("Retry block.."); - tryCount++; - - Serial.println(tryCount); - delay(1000); - - if (tryCount % 5) + case LINE_BLOCKHEIGHT: + icon = ICON_BLOCK; + rowContent = getBlock(); + break; + case LINE_MEMPOOL_FEES: + icon = ICON_PIE; + rowContent = getMempoolFees(); + break; + case LINE_MEMPOOL_FEES_MEDIAN: + icon = ICON_PIE; + rowContent = getMempoolFeesMedian(); + break; + case LINE_HALVING_COUNTDOWN: { - WiFi.disconnect(); - WiFi.reconnect(); - - while (WiFi.status() != WL_CONNECTED) - { - Serial.print('.'); - delay(1000); - } + icon = ICON_HOURGLASS; + uint currentBlock = getBlock(); + rowContent = 210000 - (currentBlock % 210000); + break; } + case LINE_SATSPERUNIT: + { + icon = ICON_SATS; + uint satsPerDollar = int(round(1 / float(getPrice()) * 10e7)); + rowContent = satsPerDollar; + break; + } + case LINE_FIATPRICE: + icon = getCurrencyIcon(); + rowContent = getPrice(); + break; + case LINE_MARKETCAP: + { + icon = getCurrencyIcon(); + int64_t marketCap = static_cast(getSupplyAtBlock(getBlock()) * double(getPrice())); + rowContent = String(formatNumberWithSuffix(marketCap, 8)); + break; + } + case LINE_TIME: + { + icon = ICON_GLOBE; + char dateString[16]; + strftime(dateString, sizeof(dateString), preferences.getString(SETTING_TIME_FORMAT).c_str(), &timeinfo); + rowContent = dateString; + break; + } + case LINE_DATE: + { + icon = ICON_GLOBE; + char dateString[16]; + strftime(dateString, sizeof(dateString), preferences.getString(SETTING_DATE_FORMAT).c_str(), &timeinfo); + rowContent = dateString; + break; + } + default: + rowContent = "DEFAULT"; + } + + methods[i](rowContent, icon); } - uint price = getPrice(); - tryCount = 0; - while (price == 0) - { - price = getPrice(); - if (Serial.available()) - Serial.print("Retry price.."); - tryCount++; - if (Serial.available()) - Serial.println(tryCount); - delay(1000); - } + // String block = String(getBlock()); - uint satsPerDollar = int(round(1 / float(price) * 10e7)); + // uint tryCount = 0; + // while (block.equals("")) + // { + // block = getBlock(); + // Serial.print("Retry block.."); + // tryCount++; - String mempoolFees = getMempoolFees(); - tryCount = 0; - while (mempoolFees.equals("")) - { - mempoolFees = getMempoolFees(); - Serial.print("Retry mempoolfees.."); - tryCount++; + // Serial.println(tryCount); + // delay(1000); - Serial.println(tryCount); - delay(1000); - } + // if (tryCount % 5) + // { + // WiFi.disconnect(); + // WiFi.reconnect(); - if (!currentFees.equals(mempoolFees)) - { - updateRow1(mempoolFees); - currentFees = mempoolFees; - Serial.print(F("Fees is now ")); - Serial.println(currentFees); - } - else - { - Serial.println(F("No need to update fees")); - } + // while (WiFi.status() != WL_CONNECTED) + // { + // Serial.print('.'); + // delay(1000); + // } + // } + // } - if (price != currentPrice) - { - updateRow3(String(satsPerDollar)); - currentPrice = price; - Serial.print(F("Price is now ")); - Serial.println(currentPrice); - } - else - { - Serial.println(F("No need to update price")); - } + // uint price = getPrice(); + // tryCount = 0; + // while (price == 0) + // { + // price = getPrice(); + // if (Serial.available()) + // Serial.print("Retry price.."); + // tryCount++; + // if (Serial.available()) + // Serial.println(tryCount); + // delay(1000); + // } - if (!block.equals(currentBlock)) - { - updateRow2(block); - currentBlock = block; - Serial.print(F("Block is now ")); - Serial.println(currentBlock); - } - else - { - Serial.println(F("No need to update block")); - } + // uint satsPerDollar = int(round(1 / float(price) * 10e7)); + + // String mempoolFees = getMempoolFees(); + // tryCount = 0; + // while (mempoolFees.equals("")) + // { + // mempoolFees = getMempoolFees(); + // Serial.print("Retry mempoolfees.."); + // tryCount++; + + // Serial.println(tryCount); + // delay(1000); + // } + + // if (!currentFees.equals(mempoolFees)) + // { + // updateRow1(mempoolFees); + // currentFees = mempoolFees; + // Serial.print(F("Fees is now ")); + // Serial.println(currentFees); + // } + // else + // { + // Serial.println(F("No need to update fees")); + // } + + // if (price != currentPrice) + // { + // updateRow3(String(satsPerDollar)); + // currentPrice = price; + // Serial.print(F("Price is now ")); + // Serial.println(currentPrice); + // } + // else + // { + // Serial.println(F("No need to update price")); + // } + + // if (!block.equals(currentBlock)) + // { + // updateRow2(block); + // currentBlock = block; + // Serial.print(F("Block is now ")); + // Serial.println(currentBlock); + // } + // else + // { + // Serial.println(F("No need to update block")); + // } // updateRows(mempoolFees, block, String(price)); delay(2 * 1000); - if (inPowerSaveMode()) { + if (inPowerSaveMode()) + { display.hibernate(); setModemSleep(); esp_sleep_enable_timer_wakeup(50 * 1000000); esp_light_sleep_start(); display.init(0, false); wakeModemSleep(); - } else { + } + else + { Serial.println(F("Sleeping")); sleep(50); -// delay(50 * 1000); + // delay(50 * 1000); Serial.println(F("Waking up")); } } diff --git a/src/shared.cpp b/src/shared.cpp new file mode 100644 index 0000000..a7dd264 --- /dev/null +++ b/src/shared.cpp @@ -0,0 +1,3 @@ +#include "shared.hpp" + +volatile bool buttonPressed = false; diff --git a/src/shared.hpp b/src/shared.hpp index 52ef439..dd17f5b 100644 --- a/src/shared.hpp +++ b/src/shared.hpp @@ -1,11 +1,16 @@ #pragma once #include -#include +#include #include #include #include +#include "utils.hpp" #include "fonts/fonts.hpp" +#include +#include +#include +#include #ifdef VERSION_EPD_2_13 #define EPD_CLASS GxEPD2_213_B74 @@ -26,40 +31,48 @@ #define EPD_CLASS GxEPD2_290_T94 #define ROW1_FONT LibreFranklin_SemiBold15pt7b #define ROW1_ICONFONT orangeclock_icons14pt7b - #define ROW1_ICONWIDTH 27 + #define ROW1_ICONWIDTH 29 #define ROW2_FONT LibreFranklin_Bold25pt7b #define ROW2_ICONFONT orangeclock_icons25pt7b #define ROW2_ICONWIDTH 52 #define ROW3_FONT LibreFranklin_SemiBold15pt7b #define ROW3_ICONFONT orangeclock_icons14pt7b - #define ROW3_ICONWIDTH 27 + #define ROW3_ICONWIDTH 29 #define SETUPFONT LibreFranklin_SemiBold12pt7b #endif -#define ICON_BLOCK "A" -#define ICON_EURO "B" -#define ICON_POUND "C" -#define ICON_YEN "D" -#define ICON_DOLLAR "E" -#define ICON_PIE "F" -#define ICON_GLOBE "G" -#define ICON_HOURGLASS "H" -#define ICON_LIGHTNING "I" -#define ICON_REFRESH "J" -#define ICON_NUCLEAR "K" -#define ICON_SATS "L" -#define ICON_SATUSD "M" -#define ICON_SETTINGS "N" -#define ICON_WIFI "O" -#define ICON_CROSS "P" -#define ICON_CHECK "Q" -#define ICON_WARNING "R" +#define ICON_BLOCK 'A' +#define ICON_EURO 'B' +#define ICON_POUND 'C' +#define ICON_YEN 'D' +#define ICON_DOLLAR 'E' +#define ICON_PIE 'F' +#define ICON_GLOBE 'G' +#define ICON_HOURGLASS 'H' +#define ICON_LIGHTNING 'I' +#define ICON_REFRESH 'J' +#define ICON_NUCLEAR 'K' +#define ICON_SATS 'L' +#define ICON_SATUSD 'M' +#define ICON_SETTINGS 'N' +#define ICON_WIFI 'O' +#define ICON_CROSS 'P' +#define ICON_CHECK 'Q' +#define ICON_WARNING 'R' #define SETTING_ROW1_CONTENT "row1" #define SETTING_ROW2_CONTENT "row2" #define SETTING_ROW3_CONTENT "row3" #define SETTING_CURRENCY "currency" +#define SETTING_HOSTNAME_PREFIX "hostnamePrefix" +#define SETTING_MEMPOOL_INSTANCE "mempoolInstance" +#define SETTING_POWER_SAVE_MODE "powerSaveMode" +#define SETTING_TIME_OFFSET_MIN "timeOffsetMin" +#define SETTING_DECIMAL_SEPARATOR "decSeparator" +#define SETTING_TIME_FORMAT "timeFormat" +#define SETTING_DATE_FORMAT "dateFormat" + const int LINE_BLOCKHEIGHT = 0; const int LINE_MEMPOOL_FEES = 1; @@ -79,7 +92,20 @@ const int LINE_DATE = 100; #define CURRENCY_AUD "AUD" #define CURRENCY_JPY "JPY" -extern WiFiClientSecure client; +extern WiFiClient client; extern GxEPD2_BW display; extern Preferences preferences; -extern bool isUpdating; \ No newline at end of file +extern bool isUpdating; + +extern String currentRow1; +extern String currentRow2; +extern String currentRow3; + +extern char currentIcon1; +extern char currentIcon2; +extern char currentIcon3; + +#ifdef NUM_LEDS +extern CRGB leds[NUM_LEDS]; +#endif +extern volatile bool buttonPressed; \ No newline at end of file diff --git a/src/utils.cpp b/src/utils.cpp new file mode 100644 index 0000000..38731ea --- /dev/null +++ b/src/utils.cpp @@ -0,0 +1,32 @@ +#include "utils.hpp" + +String getAPPassword() +{ + byte mac[6]; + WiFi.macAddress(mac); + const char charset[] = "abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRSTUVWXYZ23456789"; + char password[9]; // 8 characters + null terminator + snprintf(password, sizeof(password), "%c%c%c%c%c%c%c%c", + charset[mac[0] % (sizeof(charset) - 1)], + charset[mac[1] % (sizeof(charset) - 1)], + charset[mac[2] % (sizeof(charset) - 1)], + charset[mac[3] % (sizeof(charset) - 1)], + charset[mac[4] % (sizeof(charset) - 1)], + charset[mac[5] % (sizeof(charset) - 1)], + charset[(mac[0] + mac[1] + mac[2] + mac[3] + mac[4] + mac[5]) % (sizeof(charset) - 1)], + charset[(mac[0] * mac[1] * mac[2] * mac[3] * mac[4] * mac[5]) % (sizeof(charset) - 1)]); + + return password; +} + +String getMyHostname() +{ + uint8_t mac[6]; + // WiFi.macAddress(mac); + esp_efuse_mac_get_default(mac); + char hostname[15]; + String hostnamePrefix = preferences.getString(SETTING_HOSTNAME_PREFIX); + snprintf(hostname, sizeof(hostname), "%s-%02x%02x%02x", hostnamePrefix, + mac[3], mac[4], mac[5]); + return hostname; +} \ No newline at end of file diff --git a/src/utils.hpp b/src/utils.hpp new file mode 100644 index 0000000..80419f0 --- /dev/null +++ b/src/utils.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include "shared.hpp" +#include + +namespace ArduinoJson { +template <> +struct Converter { + static void toJson(char c, JsonVariant var) { + var.set(static_cast(c)); + } + + static char fromJson(JsonVariantConst src) { + return static_cast(src.as()); + } + + static bool checkJson(JsonVariantConst src) { + return src.is(); + } +}; +} + + +String getAPPassword(); +String getMyHostname(); diff --git a/src/webserver.cpp b/src/webserver.cpp index 7d31f7f..69b894c 100644 --- a/src/webserver.cpp +++ b/src/webserver.cpp @@ -2,7 +2,11 @@ #include AsyncWebServer server(80); -String uintSettings[] = {SETTING_ROW1_CONTENT, SETTING_ROW2_CONTENT, SETTING_ROW3_CONTENT}; +const String uintSettings[] = {SETTING_ROW1_CONTENT, SETTING_ROW2_CONTENT, SETTING_ROW3_CONTENT}; +const String intSettings[] = {SETTING_TIME_OFFSET_MIN}; +const String stringSettings[] = {SETTING_CURRENCY, SETTING_MEMPOOL_INSTANCE, SETTING_TIME_FORMAT, SETTING_DATE_FORMAT}; +const String charSettings[] = {SETTING_DECIMAL_SEPARATOR}; +const String boolSettings[] = {SETTING_POWER_SAVE_MODE}; void setupWebserver() { @@ -11,14 +15,20 @@ void setupWebserver() Serial.println(F("An Error has occurred while mounting LittleFS")); } + server.on("/api/status", HTTP_GET, onApiStatus); server.on("/api/settings", HTTP_GET, onApiSettingsGet); + server.on("/api/restart", HTTP_GET, onApiRestart); + server.on("/api/full_refresh", HTTP_GET, onApiFullRefresh); + AsyncCallbackJsonWebHandler *settingsPatchHandler = new AsyncCallbackJsonWebHandler("/api/json/settings", onApiSettingsPatch); server.addHandler(settingsPatchHandler); server.serveStatic("/build", LittleFS, "/build"); + server.serveStatic("/fonts", LittleFS, "/fonts"); server.on("/", HTTP_GET, onIndex); + server.onNotFound(onNotFound); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Origin", "*"); DefaultHeaders::Instance().addHeader("Access-Control-Allow-Methods", @@ -28,6 +38,31 @@ void setupWebserver() server.begin(); } +void onApiStatus(AsyncWebServerRequest *request) +{ + JsonDocument root; + + root["row1"] = currentRow1; + root["row2"] = currentRow2; + root["row3"] = currentRow3; + + root["icon1"] = String(currentIcon1); + root["icon2"] = String(currentIcon2); + root["icon3"] = String(currentIcon3); + + root["espUptime"] = esp_timer_get_time() / 1000000; + root["espFreeHeap"] = ESP.getFreeHeap(); + root["espHeapSize"] = ESP.getHeapSize(); + + root["rssi"] = WiFi.RSSI(); + + AsyncResponseStream *response = + request->beginResponseStream("application/json"); + serializeJson(root, *response); + + request->send(response); +} + void onApiSettingsGet(AsyncWebServerRequest *request) { JsonDocument root; @@ -36,6 +71,37 @@ void onApiSettingsGet(AsyncWebServerRequest *request) root[setting] = preferences.getUInt(setting.c_str()); } + for (String setting : intSettings) + { + root[setting] = preferences.getInt(setting.c_str()); + } + + for (String setting : stringSettings) + { + root[setting] = preferences.getString(setting.c_str()); + } + + for (String setting : charSettings) + { + root[setting] = preferences.getChar(setting.c_str()); + } + + for (String setting : boolSettings) + { + root[setting] = preferences.getBool(setting.c_str()); + } + + root["hostname"] = getMyHostname(); + root["ip"] = WiFi.localIP(); + root["txPower"] = WiFi.getTxPower(); + +#ifdef GIT_REV + root["gitRev"] = String(GIT_REV); +#endif +#ifdef LAST_BUILD_TIME + root["lastBuildTime"] = String(LAST_BUILD_TIME); +#endif + AsyncResponseStream *response = request->beginResponseStream("application/json"); serializeJson(root, *response); @@ -57,6 +123,46 @@ void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json) } } + for (String setting : intSettings) + { + if (settings.containsKey(setting)) + { + preferences.putInt(setting.c_str(), settings[setting].as()); + Serial.printf("Setting %s to %d\r\n", setting.c_str(), + settings[setting].as()); + } + } + + for (String setting : stringSettings) + { + if (settings.containsKey(setting)) + { + preferences.putString(setting.c_str(), settings[setting].as()); + Serial.printf("Setting %s to %s\r\n", setting.c_str(), + settings[setting].as()); + } + } + + for (String setting : charSettings) + { + if (settings.containsKey(setting)) + { + preferences.putChar(setting.c_str(), settings[setting].as()); + Serial.printf("Setting %s to %s\r\n", setting.c_str(), + settings[setting].as()); + } + } + + for (String setting : boolSettings) + { + if (settings.containsKey(setting)) + { + preferences.putBool(setting.c_str(), settings[setting].as()); + Serial.printf("Setting %s to %d\r\n", setting.c_str(), + settings[setting].as()); + } + } + request->send(200); } @@ -80,3 +186,21 @@ void onNotFound(AsyncWebServerRequest *request) request->send(404); } } + +void onApiRestart(AsyncWebServerRequest *request) { + request->send(200); + + delay(500); + + esp_restart(); +} + +/** + * @Api + * @Path("/api/full_refresh") + */ +void onApiFullRefresh(AsyncWebServerRequest *request) { + display.refresh(); + + request->send(200); +} \ No newline at end of file diff --git a/src/webserver.hpp b/src/webserver.hpp index 7ca57cc..30230de 100644 --- a/src/webserver.hpp +++ b/src/webserver.hpp @@ -9,8 +9,12 @@ void setupWebserver(); +void onApiStatus(AsyncWebServerRequest *request); + void onApiSettingsGet(AsyncWebServerRequest *request); void onApiSettingsPatch(AsyncWebServerRequest *request, JsonVariant &json); +void onApiFullRefresh(AsyncWebServerRequest *request); +void onApiRestart(AsyncWebServerRequest *request); void onIndex(AsyncWebServerRequest *request); void onNotFound(AsyncWebServerRequest *request); \ No newline at end of file