Compare commits
126 Commits
update-REA
...
30-fronten
| Author | SHA1 | Date | |
|---|---|---|---|
| 32e996e446 | |||
| b20a31ccf4 | |||
| 8a2d621c7d | |||
| 93ea5036dc | |||
| aee5a48967 | |||
| 936020df58 | |||
| e447902001 | |||
| 768b630722 | |||
| e0ff9c54bc | |||
| c0b9ef2961 | |||
| e23f8bd34c | |||
| 97e9742c75 | |||
| c687e3a977 | |||
| aa1a02ce54 | |||
| 3e888790e4 | |||
| 96759c999c | |||
| 82ed3a3c2e | |||
| a7e58853bb | |||
| 2e1bc7782b | |||
| fa0e23ee87 | |||
| 856bf34161 | |||
| 83deb1b4c2 | |||
| 2225906664 | |||
| 176fc66c17 | |||
| 79a970e70a | |||
| 19afb2f936 | |||
| 3c23fc1446 | |||
| 111ae65ebc | |||
| 1356ea06ab | |||
| e07ee46518 | |||
| cd5515ddad | |||
| 99ad8582f0 | |||
| ae875950cd | |||
| 340aea6be6 | |||
| f64bbc27fd | |||
| abb880baca | |||
| a59f00ad60 | |||
| 135ad11de9 | |||
| ff52e989a2 | |||
| fbb383abed | |||
| 869a92d998 | |||
| 696f7b956c | |||
| 0bb1b2758a | |||
| 419d85209e | |||
| 0f4f72253c | |||
| 595f3bd37f | |||
| 08fdb75297 | |||
| d28f0b1c0c | |||
| 9147bab5bb | |||
| 0820ec1fc8 | |||
| 7d3cdbabe4 | |||
| b5e3df1054 | |||
| 55718aa1ff | |||
| 981c75020f | |||
| b56e4f36b4 | |||
| 9ba6e698b9 | |||
| db136ed79a | |||
| 6a9ff9eb0a | |||
| 7327549eea | |||
| 7e532ec641 | |||
| 5013e2359d | |||
| 4512b91763 | |||
| 5891006bcd | |||
| a81952fb8a | |||
| 92f9ce3d5a | |||
| 23d5b050dc | |||
| 2a4033d3ca | |||
| 2809d3f6e7 | |||
| 8e4f2fde21 | |||
| 9357b4d243 | |||
| 231172f794 | |||
| 5625f43f81 | |||
| 502805e2e8 | |||
| 902ce980af | |||
| 834e07966a | |||
| e8beb4c8f7 | |||
| fef9e690cd | |||
| 4507275a02 | |||
| 16d5180d34 | |||
| 69e119a8a0 | |||
| 4a15b2c221 | |||
| d3674e46aa | |||
| 1fa3b3c887 | |||
| 4d295501eb | |||
| c9150eb21a | |||
| da90228ab5 | |||
| bd17197fef | |||
| d39157ba0a | |||
| 09c8f522b8 | |||
| 0ce0b51f1c | |||
| e33fefc552 | |||
| 95b3924736 | |||
| 27110b7b62 | |||
| fb36ac8ed2 | |||
| 4c7ce78201 | |||
| 10de110375 | |||
| 5c2df891d9 | |||
| e72443ff1f | |||
| 3f0eeb837a | |||
| 3bd81f01e2 | |||
| 6f155ebb55 | |||
| f1de8c6c75 | |||
| 3b349af8cf | |||
| 679495bf2b | |||
| e9bd4f72b6 | |||
| 34f9a44cdb | |||
| 2796dbcf16 | |||
| ff2a609752 | |||
| 2f0d4cc3eb | |||
| 7ccab40cd6 | |||
| 96fea9dab6 | |||
| 9059da1a70 | |||
| 0353796110 | |||
| ce02daad3a | |||
| 7778147b66 | |||
| 7712c25627 | |||
| 69b6f6e860 | |||
| 903840f982 | |||
| b477aba94c | |||
| f0c3fe4db8 | |||
| d02776eea8 | |||
| fb496b6991 | |||
| 1b683358d1 | |||
| 91af2087b2 | |||
| 7f8282315e | |||
| 8a8cb85c39 |
@@ -1,6 +1,6 @@
|
|||||||
---
|
---
|
||||||
name: Build RPi Pico firmware image
|
name: Build RPi Pico firmware image
|
||||||
on:
|
"on":
|
||||||
push:
|
push:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -11,10 +11,17 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
- name: Initialize submodules
|
- name: Initialize submodules
|
||||||
run: cd software && ./update-submodules.sh
|
run: cd software && ./update-submodules.sh
|
||||||
|
- name: Prepare venv
|
||||||
|
run: python -m venv build-venv && source build-venv/bin/activate && pip install freezefs
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cd software && ./build.sh
|
run: source build-venv/bin/activate && cd software && ./build.sh
|
||||||
- name: Upload firmware
|
- name: Upload firmware
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: firmware-RPi-Pico-W
|
name: firmware-RPi-Pico-W
|
||||||
path: software/lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/firmware.uf2
|
path: software/build/firmware-*.uf2
|
||||||
|
- name: Upload firmware w/ filesystem
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: firmware-RPi-Pico-W-with-fs
|
||||||
|
path: software/build/firmware-filesystem-*.uf2
|
||||||
|
|||||||
@@ -25,8 +25,11 @@ jobs:
|
|||||||
path: git
|
path: git
|
||||||
- name: Check python
|
- name: Check python
|
||||||
run: |
|
run: |
|
||||||
cd git/software/src &&
|
cd git/software && (
|
||||||
find . -iname '*.py' -exec ../../../flake-venv/bin/flake8 {} +
|
find src -iname '*.py' -exec ../../flake-venv/bin/flake8 {} +
|
||||||
|
find tests -iname '*.py' -exec ../../flake-venv/bin/flake8 {} +
|
||||||
|
find modules -iname '*.py' -exec ../../flake-venv/bin/flake8 {} +
|
||||||
|
)
|
||||||
Check-Bash-Shellcheck:
|
Check-Bash-Shellcheck:
|
||||||
runs-on: ubuntu-22.04-full
|
runs-on: ubuntu-22.04-full
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
24
.gitea/workflows/unit-tests-python.yaml
Normal file
24
.gitea/workflows/unit-tests-python.yaml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
---
|
||||||
|
name: Run pytests
|
||||||
|
"on":
|
||||||
|
push:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Check-Pytest:
|
||||||
|
runs-on: ubuntu-22.04-full
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
path: git
|
||||||
|
- name: Get dependencies
|
||||||
|
run: |
|
||||||
|
python -m venv test-venv
|
||||||
|
test-venv/bin/pip install -r git/software/tests/requirements.txt
|
||||||
|
- name: Run pytest
|
||||||
|
run: |
|
||||||
|
. test-venv/bin/activate &&
|
||||||
|
cd git/software &&
|
||||||
|
pytest
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -3,5 +3,9 @@ hardware/tonberry-pico/tonberry-pico-backups/
|
|||||||
*.kicad_sch-bak
|
*.kicad_sch-bak
|
||||||
*.kicad_sch.lck
|
*.kicad_sch.lck
|
||||||
software/build
|
software/build
|
||||||
|
software/typings
|
||||||
compile_commands.json
|
compile_commands.json
|
||||||
.dir-locals.el
|
.dir-locals.el
|
||||||
|
.cache
|
||||||
|
\#*#
|
||||||
|
__pycache__
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -7,3 +7,6 @@
|
|||||||
[submodule "software/lib/microdot"]
|
[submodule "software/lib/microdot"]
|
||||||
path = software/lib/microdot
|
path = software/lib/microdot
|
||||||
url = ../microdot.git
|
url = ../microdot.git
|
||||||
|
[submodule "software/tools/mklittlefs"]
|
||||||
|
path = software/tools/mklittlefs
|
||||||
|
url = https://github.com/earlephilhower/mklittlefs.git
|
||||||
|
|||||||
54
DEVELOP.md
Normal file
54
DEVELOP.md
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
# Developer notes
|
||||||
|
|
||||||
|
## How to setup python environment for mypy and pytest
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd software
|
||||||
|
python -m venv test-venv
|
||||||
|
. test-venv/bin/activate
|
||||||
|
pip install -r tests/requirements.txt
|
||||||
|
pip install -U micropython-rp2-pico_w-stubs --target typings
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 'database' schema for btree db
|
||||||
|
|
||||||
|
### Playlist storage
|
||||||
|
|
||||||
|
The playlist data is stored in the btree database in a hierarchical schema. The hierarchy levels are
|
||||||
|
separated by the '/' character. Currently, the schema is as follows: The top level for a playlist
|
||||||
|
is the 'tag' id encoded as a hexadecimal string. Beneath this, the 'playlist' key contains the
|
||||||
|
elements in the playlist. The keys used for the playlist entries must be decimal integers,
|
||||||
|
left-padded with zeros so their length is 5 (e.g. format `{:05}`).
|
||||||
|
|
||||||
|
#### Playlist modes
|
||||||
|
|
||||||
|
The 'playlistshuffle' key located under the 'tag' key can be 'no' or 'yes' and specifies whether the
|
||||||
|
playlist is in shuffle mode. Should this key be absent the default value is 'no'.
|
||||||
|
|
||||||
|
The 'playlistpersist' key located under the 'tag' key can be 'no', 'track' or 'offset'. Should this
|
||||||
|
key be absent the default value is 'track'.
|
||||||
|
|
||||||
|
* When it is 'no', the playlist position is not saved when playback stops. If shuffle mode is
|
||||||
|
active, the shuffle random seed is also not saved.
|
||||||
|
* When it is 'track', the currently playing track is saved when playback stops. If shuffle mode is
|
||||||
|
active, the shuffle random seed is also saved. Should playback reach the last track (in shuffle
|
||||||
|
mode: the last track in the permutated order), the saved position is reset and playback is
|
||||||
|
stopped. The next time the playlist is started it will start from the first track and with a new
|
||||||
|
shuffle seed if applicable.
|
||||||
|
* When it is 'offset', the operation is basically the same as in 'track' mode. The difference is
|
||||||
|
that the offset in the currently playing track is also saved and playback will resume at that
|
||||||
|
position.
|
||||||
|
|
||||||
|
The 'playlistpos' key located under the 'tag' key stores the key of the current playlist
|
||||||
|
entry. The 'playlistshuffleseed' key stores the random seed used to shuffle the playlist.
|
||||||
|
The 'playlistposoffset' key stores the offset in the current playlist entry.
|
||||||
|
|
||||||
|
#### Example
|
||||||
|
|
||||||
|
For example, a playlist with two entries 'a.mp3' and 'b.mp3' for a tag with the id '00aa11bb22'
|
||||||
|
would be stored in the following key/value pairs in the btree db:
|
||||||
|
|
||||||
|
* 00aa11bb22/playlist/00000: a.mp3
|
||||||
|
* 00aa11bb22/playlist/00001: b.mp3
|
||||||
|
* 00aa11bb22/playlistpos: 00000
|
||||||
@@ -0,0 +1,443 @@
|
|||||||
|
(footprint "AZ_Delivery_RC522"
|
||||||
|
(version 20241229)
|
||||||
|
(generator "pcbnew")
|
||||||
|
(generator_version "9.0")
|
||||||
|
(layer "F.Cu")
|
||||||
|
(descr "Through hole straight socket strip, 1x08, 2.54mm pitch, single row (from Kicad 4.0.7), script generated")
|
||||||
|
(tags "Through hole socket strip THT 1x08 2.54mm single row")
|
||||||
|
(property "Reference" "REF**"
|
||||||
|
(at 0 -2.77 0)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "a34ef152-6377-4c69-9f9a-fb0a9cbaaa0f")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1 1)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(property "Value" "AZ_Delivery_RC522"
|
||||||
|
(at -1.9 11.03 0)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "eb0c6019-b136-4812-894a-851e8bd21756")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1 1)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(property "Datasheet" ""
|
||||||
|
(at 0 0 0)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(hide yes)
|
||||||
|
(uuid "2d28acfc-38d9-483d-a1f6-5b4a7c5c5208")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1.27 1.27)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(property "Description" ""
|
||||||
|
(at 0 0 0)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(hide yes)
|
||||||
|
(uuid "d9f04e4e-c6ea-48a4-82e1-9fd680507783")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1.27 1.27)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(attr through_hole)
|
||||||
|
(fp_line
|
||||||
|
(start -1.33 1.27)
|
||||||
|
(end -1.33 19.11)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "5c816ce7-1e16-4fef-8a94-56ce4f336409")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.33 1.27)
|
||||||
|
(end 1.33 1.27)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "31cfc545-00ec-41d5-a909-28f51668cbb9")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.33 19.11)
|
||||||
|
(end 1.33 19.11)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "6fdb9624-459f-42e6-8474-8476e3e73a19")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 0 -1.33)
|
||||||
|
(end 1.33 -1.33)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "673bcfad-3a7d-4a5b-ae2e-68bfc3cb0212")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.33 -1.33)
|
||||||
|
(end 1.33 0)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "596a7cf5-b26a-4237-9ea9-a0b83ce26be7")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.33 1.27)
|
||||||
|
(end 1.33 19.11)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "dd0a885d-9fe0-46b4-a266-a488d8626717")
|
||||||
|
)
|
||||||
|
(fp_rect
|
||||||
|
(start -1.9 -9.5)
|
||||||
|
(end 58.1 30.5)
|
||||||
|
(stroke
|
||||||
|
(width 0.1524)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(fill no)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "8f986849-0666-4030-885e-2108faf1da15")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.8 -1.8)
|
||||||
|
(end 1.75 -1.8)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "446aed1d-b80f-4b30-b956-5f8297a19941")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.8 19.55)
|
||||||
|
(end -1.8 -1.8)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "22ebe2a7-8b67-47c2-bdf8-cdb4cd9951cf")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.75 -1.8)
|
||||||
|
(end 1.75 19.55)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "fe065b5b-3b98-4eb6-bb1d-7dc77992767f")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.75 19.55)
|
||||||
|
(end -1.8 19.55)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "3e9ed458-acaf-44bb-9c22-6291112f2c8b")
|
||||||
|
)
|
||||||
|
(fp_circle
|
||||||
|
(center 13.75 -7.02)
|
||||||
|
(end 16.29 -7.02)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(fill no)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "b0fc3695-90f5-41e7-8964-ebecbadeaa08")
|
||||||
|
)
|
||||||
|
(fp_circle
|
||||||
|
(center 13.75 27.98)
|
||||||
|
(end 16.29 27.98)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(fill no)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "37ebb819-cba1-4d26-8219-ba4fc5e3d318")
|
||||||
|
)
|
||||||
|
(fp_circle
|
||||||
|
(center 51.25 -2.22)
|
||||||
|
(end 53.79 -2.22)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(fill no)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "c91279d4-19e9-4d6b-82e4-3abaf6c9450a")
|
||||||
|
)
|
||||||
|
(fp_circle
|
||||||
|
(center 51.25 23.63)
|
||||||
|
(end 53.79 23.63)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(fill no)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "49613ec7-c049-4577-95b9-7914f2d0eeb3")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.27 -1.27)
|
||||||
|
(end 0.635 -1.27)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "2a668cbf-d67d-4e90-af33-7fe4d2a9e9d2")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.27 19.05)
|
||||||
|
(end -1.27 -1.27)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "063425b4-bc99-4269-a589-b598140b14bf")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 0.635 -1.27)
|
||||||
|
(end 1.27 -0.635)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "a5b3e0e3-31ba-4ee4-a261-36ed072fb6d3")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.27 -0.635)
|
||||||
|
(end 1.27 19.05)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "626e075f-5f87-420e-9637-1a7362632a52")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.27 19.05)
|
||||||
|
(end -1.27 19.05)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "92009a7e-4398-40ed-8894-3370eb975c50")
|
||||||
|
)
|
||||||
|
(fp_text user "keep out"
|
||||||
|
(at 29 9 0)
|
||||||
|
(unlocked yes)
|
||||||
|
(layer "Dwgs.User")
|
||||||
|
(uuid "1b8c2096-cda5-41b5-8771-3eac90083181")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1 1)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
(justify left bottom)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(fp_text user "Antenna"
|
||||||
|
(at 29 7.5 0)
|
||||||
|
(unlocked yes)
|
||||||
|
(layer "Dwgs.User")
|
||||||
|
(uuid "1b984db0-349b-4503-b267-9f2bd33cff0c")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1 1)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
(justify left bottom)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(fp_text user "${REFERENCE}"
|
||||||
|
(at 0 8.89 90)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "1781e910-89a3-48c8-8888-4f06a0dd2fdc")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1 1)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(pad "" np_thru_hole circle
|
||||||
|
(at 13.75 -7.02)
|
||||||
|
(size 3.2 3.2)
|
||||||
|
(drill 3.2)
|
||||||
|
(layers "F&B.Cu" "*.Mask")
|
||||||
|
(uuid "7c71dc53-c556-469e-a5bc-b4c067525bc5")
|
||||||
|
)
|
||||||
|
(pad "" np_thru_hole circle
|
||||||
|
(at 13.75 27.98)
|
||||||
|
(size 3.2 3.2)
|
||||||
|
(drill 3.2)
|
||||||
|
(layers "F&B.Cu" "*.Mask")
|
||||||
|
(uuid "23726452-e50f-490c-a39e-a24c479dbdd3")
|
||||||
|
)
|
||||||
|
(pad "" np_thru_hole circle
|
||||||
|
(at 51.25 -2.22)
|
||||||
|
(size 3.2 3.2)
|
||||||
|
(drill 3.2)
|
||||||
|
(layers "F&B.Cu" "*.Mask")
|
||||||
|
(uuid "e3c5ee74-770a-46e8-910d-a5bf1769975b")
|
||||||
|
)
|
||||||
|
(pad "" np_thru_hole circle
|
||||||
|
(at 51.25 23.63)
|
||||||
|
(size 3.2 3.2)
|
||||||
|
(drill 3.2)
|
||||||
|
(layers "F&B.Cu" "*.Mask")
|
||||||
|
(uuid "d92ad159-9fb8-4328-a573-b78abcdc471f")
|
||||||
|
)
|
||||||
|
(pad "1" thru_hole rect
|
||||||
|
(at 0 0)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "cb8ff318-d964-4ff3-93e9-18ed02ecc716")
|
||||||
|
)
|
||||||
|
(pad "2" thru_hole circle
|
||||||
|
(at 0 2.54)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "171fee8a-00a9-4f1b-bee2-5dc44fbef460")
|
||||||
|
)
|
||||||
|
(pad "3" thru_hole circle
|
||||||
|
(at 0 5.08)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "ff79db69-5af0-439a-b402-c1c0dc0f6a5d")
|
||||||
|
)
|
||||||
|
(pad "4" thru_hole circle
|
||||||
|
(at 0 7.62)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "59f1cdf4-9788-48e6-8ac6-11fe687d19a6")
|
||||||
|
)
|
||||||
|
(pad "5" thru_hole circle
|
||||||
|
(at 0 10.16)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "02a680e0-7902-4b9b-97d5-04111a6ad630")
|
||||||
|
)
|
||||||
|
(pad "6" thru_hole circle
|
||||||
|
(at 0 12.7)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "ea4d2b35-79b2-4d5d-8abe-b0d0675a2a70")
|
||||||
|
)
|
||||||
|
(pad "7" thru_hole circle
|
||||||
|
(at 0 15.24)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "c0db5362-fdbd-4784-9c33-469f195ac9af")
|
||||||
|
)
|
||||||
|
(pad "8" thru_hole circle
|
||||||
|
(at 0 17.78)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "adabdc0d-c196-4362-98fc-523b0680144d")
|
||||||
|
)
|
||||||
|
(zone
|
||||||
|
(net 0)
|
||||||
|
(net_name "")
|
||||||
|
(layers "F.Cu" "B.Cu")
|
||||||
|
(uuid "5e845900-288e-4f42-a8ac-c20c1cc0aa41")
|
||||||
|
(name "Antenna")
|
||||||
|
(hatch edge 0.5)
|
||||||
|
(connect_pads
|
||||||
|
(clearance 0)
|
||||||
|
)
|
||||||
|
(min_thickness 0.25)
|
||||||
|
(filled_areas_thickness no)
|
||||||
|
(keepout
|
||||||
|
(tracks not_allowed)
|
||||||
|
(vias not_allowed)
|
||||||
|
(pads not_allowed)
|
||||||
|
(copperpour not_allowed)
|
||||||
|
(footprints not_allowed)
|
||||||
|
)
|
||||||
|
(placement
|
||||||
|
(enabled no)
|
||||||
|
(sheetname "")
|
||||||
|
)
|
||||||
|
(fill
|
||||||
|
(thermal_gap 0.5)
|
||||||
|
(thermal_bridge_width 0.5)
|
||||||
|
)
|
||||||
|
(polygon
|
||||||
|
(pts
|
||||||
|
(xy 58.1 -9.5) (xy 15.5 -9.5) (xy 15.5 30.5) (xy 58.1 30.5)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(group ""
|
||||||
|
(uuid "72a71035-0d89-4c39-b4c6-1fefc66807fa")
|
||||||
|
(members "23726452-e50f-490c-a39e-a24c479dbdd3" "37ebb819-cba1-4d26-8219-ba4fc5e3d318"
|
||||||
|
"49613ec7-c049-4577-95b9-7914f2d0eeb3" "7c71dc53-c556-469e-a5bc-b4c067525bc5"
|
||||||
|
"8f986849-0666-4030-885e-2108faf1da15" "b0fc3695-90f5-41e7-8964-ebecbadeaa08"
|
||||||
|
"c91279d4-19e9-4d6b-82e4-3abaf6c9450a" "d92ad159-9fb8-4328-a573-b78abcdc471f"
|
||||||
|
"e3c5ee74-770a-46e8-910d-a5bf1769975b" "eb0c6019-b136-4812-894a-851e8bd21756"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(embedded_fonts no)
|
||||||
|
(model "${KICAD9_3DMODEL_DIR}/Connector_PinSocket_2.54mm.3dshapes/PinSocket_1x08_P2.54mm_Vertical.step"
|
||||||
|
(offset
|
||||||
|
(xyz 0 0 0)
|
||||||
|
)
|
||||||
|
(scale
|
||||||
|
(xyz 1 1 1)
|
||||||
|
)
|
||||||
|
(rotate
|
||||||
|
(xyz 0 0 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
355
hardware/tonberry-pico/Modules.pretty/Adafruit_bq25185.kicad_mod
Normal file
355
hardware/tonberry-pico/Modules.pretty/Adafruit_bq25185.kicad_mod
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
(footprint "Adafruit_bq25185"
|
||||||
|
(version 20241229)
|
||||||
|
(generator "pcbnew")
|
||||||
|
(generator_version "9.0")
|
||||||
|
(layer "F.Cu")
|
||||||
|
(descr "Through hole straight socket strip, 1x07, 2.54mm pitch, single row (from Kicad 4.0.7), script generated")
|
||||||
|
(tags "Through hole socket strip THT 1x07 2.54mm single row")
|
||||||
|
(property "Reference" "REF**"
|
||||||
|
(at 0 -8.255 0)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "6e575542-8fac-4808-b8c9-ded669296d0d")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1.016 1.016)
|
||||||
|
(thickness 0.1524)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(property "Value" "Adafruit_bq25185"
|
||||||
|
(at -3.81 23.495 0)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "86edab5c-7f9d-4119-b32b-b12277ace150")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1 1)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(property "Datasheet" ""
|
||||||
|
(at 0 0 0)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(hide yes)
|
||||||
|
(uuid "178de254-c544-4662-881b-f069a6a8f6a5")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1.27 1.27)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(property "Description" ""
|
||||||
|
(at 0 0 0)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(hide yes)
|
||||||
|
(uuid "66b522fa-0a20-4d7b-9c1c-01db271771b4")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1.27 1.27)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(attr through_hole)
|
||||||
|
(fp_line
|
||||||
|
(start -1.33 15.24)
|
||||||
|
(end -1.33 -2.54)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "aca72a9b-9330-4c35-bf1b-82d487a9447f")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.33 17.78)
|
||||||
|
(end -1.33 16.51)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "518da0df-6fd0-4f31-af5f-48e331dc849c")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.33 15.24)
|
||||||
|
(end -1.33 15.24)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "e4083416-4300-4fb2-b50c-b38793ec4834")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.33 15.24)
|
||||||
|
(end 1.33 -2.54)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "9b378274-151c-4b84-ae74-fca52b0e1635")
|
||||||
|
)
|
||||||
|
(fp_rect
|
||||||
|
(start -16.51 -6.985)
|
||||||
|
(end 2.54 22.225)
|
||||||
|
(stroke
|
||||||
|
(width 0.1524)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(fill no)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "07d7f04c-4960-4dfe-92c3-4d957a4244ce")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.8 -1.8)
|
||||||
|
(end -1.799999 -2.652907)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "4452e51e-0893-4b70-92fd-70f0b54ced32")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.778371 17.931627)
|
||||||
|
(end -1.8 -1.8)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "9f38d32c-752e-406c-81c2-56a23e2124ab")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.75 -1.8)
|
||||||
|
(end 1.75 -2.604051)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "355fe774-8482-4f16-ae08-c09c0defb5e5")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.75 -1.8)
|
||||||
|
(end 1.778373 17.931628)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "f10f2b1d-14a2-4182-8823-7cb2b08a980c")
|
||||||
|
)
|
||||||
|
(fp_arc
|
||||||
|
(start -1.8 -2.652908)
|
||||||
|
(mid 0.034954 -6.984758)
|
||||||
|
(end 1.75 -2.604051)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "f25457eb-8a07-4acd-907f-7cd787994a5c")
|
||||||
|
)
|
||||||
|
(fp_arc
|
||||||
|
(start 1.778373 17.931627)
|
||||||
|
(mid 0 22.224999)
|
||||||
|
(end -1.778373 17.931627)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "b284c369-20df-4fe6-a2a0-f28fcc3b58d6")
|
||||||
|
)
|
||||||
|
(fp_circle
|
||||||
|
(center -13.97 -4.42)
|
||||||
|
(end -16.485 -4.42)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(fill no)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "bd44fb04-efc3-4813-ad81-2f8b403680df")
|
||||||
|
)
|
||||||
|
(fp_circle
|
||||||
|
(center -13.97 19.71)
|
||||||
|
(end -16.485 19.71)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(fill no)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "cf8bdd37-9205-40af-8a51-17c2a30f9014")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.27 -2.501639)
|
||||||
|
(end 1.27 -2.501639)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "7afbc36b-dc93-48ce-b6e3-213a0cc77565")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.27 17.145)
|
||||||
|
(end -1.27 -2.501639)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "2c7bc528-2d0c-40c9-9e97-d56236d26fe6")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -0.635 17.78)
|
||||||
|
(end -1.27 17.145)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "a12a655f-03d7-4c2a-87ca-b0238aca19c3")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.27 -2.501639)
|
||||||
|
(end 1.27 17.78)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "7a7136ae-feda-4d2a-9391-81bb7d3312d2")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.27 17.78)
|
||||||
|
(end -0.635 17.78)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "c13c8715-e3c4-4d03-8849-e2a1e26d40a1")
|
||||||
|
)
|
||||||
|
(fp_text user "${REFERENCE}"
|
||||||
|
(at -2.54 12.7 270)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "a8c74cf2-2d56-4366-b45f-d04e448ebc72")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1 1)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(pad "" np_thru_hole circle
|
||||||
|
(at -13.97 -4.445)
|
||||||
|
(size 2.54 2.54)
|
||||||
|
(drill 2.54)
|
||||||
|
(layers "F&B.Cu" "*.Mask")
|
||||||
|
(uuid "356e8d03-c3ad-4b24-bf9f-f35de4f1636d")
|
||||||
|
)
|
||||||
|
(pad "" np_thru_hole circle
|
||||||
|
(at -13.97 19.685)
|
||||||
|
(size 2.54 2.54)
|
||||||
|
(drill 2.54)
|
||||||
|
(layers "F&B.Cu" "*.Mask")
|
||||||
|
(uuid "c342310e-eb02-4174-9d9c-eb95934aebf5")
|
||||||
|
)
|
||||||
|
(pad "" np_thru_hole circle
|
||||||
|
(at 0 -4.445)
|
||||||
|
(size 2.54 2.54)
|
||||||
|
(drill 2.54)
|
||||||
|
(layers "F&B.Cu" "*.Mask")
|
||||||
|
(uuid "6306c281-706a-4b36-9e39-5aa956be2d80")
|
||||||
|
)
|
||||||
|
(pad "" np_thru_hole circle
|
||||||
|
(at 0 19.685)
|
||||||
|
(size 2.54 2.54)
|
||||||
|
(drill 2.54)
|
||||||
|
(layers "F&B.Cu" "*.Mask")
|
||||||
|
(uuid "d01f277d-a18c-4fb7-8bce-ad82f53d5330")
|
||||||
|
)
|
||||||
|
(pad "1" thru_hole rect
|
||||||
|
(at 0 16.51 180)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "9c199252-bdac-484c-a687-6b4ef26cc3e6")
|
||||||
|
)
|
||||||
|
(pad "2" thru_hole circle
|
||||||
|
(at 0 13.97 180)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "be025cc7-4905-4574-955b-178576ccc1cc")
|
||||||
|
)
|
||||||
|
(pad "3" thru_hole circle
|
||||||
|
(at 0 11.43 180)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "e46efbed-3393-457c-95ba-f30245b6478b")
|
||||||
|
)
|
||||||
|
(pad "4" thru_hole circle
|
||||||
|
(at 0 8.89 180)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "5bb680ed-7bca-4b4a-852d-e6849574c8a6")
|
||||||
|
)
|
||||||
|
(pad "5" thru_hole circle
|
||||||
|
(at 0 6.35 180)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "59961279-9785-4a81-948d-7855e0e08516")
|
||||||
|
)
|
||||||
|
(pad "6" thru_hole circle
|
||||||
|
(at 0 3.81 180)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "eda46f52-d12e-47b7-951c-bc4005bc694e")
|
||||||
|
)
|
||||||
|
(pad "7" thru_hole circle
|
||||||
|
(at 0 1.27 180)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "2cb79b81-6688-44c2-92c7-e75f5df30e91")
|
||||||
|
)
|
||||||
|
(pad "8" thru_hole circle
|
||||||
|
(at 0 -1.27 180)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "b2d6f12f-bf69-4a9b-a3e9-164231e4f483")
|
||||||
|
)
|
||||||
|
(embedded_fonts no)
|
||||||
|
(model "${KICAD9_3DMODEL_DIR}/Connector_PinSocket_2.54mm.3dshapes/PinSocket_1x08_P2.54mm_Vertical.step"
|
||||||
|
(offset
|
||||||
|
(xyz 0 1.25 0)
|
||||||
|
)
|
||||||
|
(scale
|
||||||
|
(xyz 1 1 1)
|
||||||
|
)
|
||||||
|
(rotate
|
||||||
|
(xyz -0 -0 -0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,381 @@
|
|||||||
|
(footprint "Sparkfun_MAX98357A"
|
||||||
|
(version 20241229)
|
||||||
|
(generator "pcbnew")
|
||||||
|
(generator_version "9.0")
|
||||||
|
(layer "F.Cu")
|
||||||
|
(descr "Through hole straight socket strip, 1x07, 2.54mm pitch, single row (from Kicad 4.0.7), script generated")
|
||||||
|
(tags "Through hole socket strip THT 1x07 2.54mm single row")
|
||||||
|
(property "Reference" "REF**"
|
||||||
|
(at 0 -3.556 0)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "2656f117-ae27-4bd8-a9c3-f25e38d25a50")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1.016 1.016)
|
||||||
|
(thickness 0.1524)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(property "Value" "Sparkfun_MAX98357A"
|
||||||
|
(at 0 18.01 0)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "e7891e54-c3b2-4045-b729-ec1b29005170")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1 1)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(property "Datasheet" ""
|
||||||
|
(at 0 0 0)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(hide yes)
|
||||||
|
(uuid "aab68a41-9b58-43f5-b6ed-218d51a35c21")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1.27 1.27)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(property "Description" ""
|
||||||
|
(at 0 0 0)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(hide yes)
|
||||||
|
(uuid "d5308824-df7c-46bf-b6ad-dfc407c4cafc")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1.27 1.27)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(attr through_hole)
|
||||||
|
(fp_line
|
||||||
|
(start -1.33 -2.54)
|
||||||
|
(end 19.05 -2.54)
|
||||||
|
(stroke
|
||||||
|
(width 0.1524)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "a225c96b-f88a-46ab-8898-d5a961a69e4d")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.33 1.27)
|
||||||
|
(end -1.33 -2.54)
|
||||||
|
(stroke
|
||||||
|
(width 0.1524)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "d9e35813-ad57-481a-bd0f-e96849b81f42")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.33 1.27)
|
||||||
|
(end -1.33 16.57)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "acaab1d6-f4d6-4ae2-98ff-f3ba2e3cd533")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.33 1.27)
|
||||||
|
(end 1.33 1.27)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "c2228c31-a76f-4f6c-8dac-e47a00dd4a83")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.33 16.57)
|
||||||
|
(end 1.33 16.57)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "4067195c-1874-47b5-b0f1-8f78aa17f1fb")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.27 17.78)
|
||||||
|
(end -1.27 16.57)
|
||||||
|
(stroke
|
||||||
|
(width 0.1524)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "b5f022f0-b8d1-4bd3-b372-d667aa83852e")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 0 -1.33)
|
||||||
|
(end 1.33 -1.33)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "3672f0c4-cfd3-4ed8-a916-26b23e299b5c")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.33 -1.33)
|
||||||
|
(end 1.33 0)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "e8fb0117-396a-4d0e-92dd-8c1cd555e5c6")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.33 1.27)
|
||||||
|
(end 1.33 16.57)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "1ac1a480-cb45-4327-a825-2b9d92cf5a54")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 19.05 -2.54)
|
||||||
|
(end 19.05 17.78)
|
||||||
|
(stroke
|
||||||
|
(width 0.1524)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "51996aa9-4796-4afe-9489-c6b28405d249")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 19.05 17.78)
|
||||||
|
(end -1.27 17.78)
|
||||||
|
(stroke
|
||||||
|
(width 0.1524)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "18391d8a-34f3-4119-86e5-c63f37e37070")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.8 -1.8)
|
||||||
|
(end 1.75 -1.8)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "858f249c-fdd9-466b-b8a1-4dd799afef6b")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.8 17)
|
||||||
|
(end -1.8 -1.8)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "1d1e798f-8b02-4758-b297-16af69eea697")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.75 -1.8)
|
||||||
|
(end 1.75 17)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "43d35c9f-6401-4fe4-80f8-4316968255a0")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.75 17)
|
||||||
|
(end -1.8 17)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "35349e7d-9f0b-4de6-b84b-46bf4473917f")
|
||||||
|
)
|
||||||
|
(fp_circle
|
||||||
|
(center 15.875 0.635)
|
||||||
|
(end 18.415 0.635)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(fill no)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "dfaa1b74-8adb-43a3-83c0-f701d5acdf69")
|
||||||
|
)
|
||||||
|
(fp_circle
|
||||||
|
(center 15.875 14.605)
|
||||||
|
(end 18.415 14.605)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type default)
|
||||||
|
)
|
||||||
|
(fill no)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "b6741d2d-655e-4405-bdec-635bf83d8ad5")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.27 -1.27)
|
||||||
|
(end 0.635 -1.27)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "37e9acde-12d2-4319-a282-cdf2fdbe341a")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.27 16.51)
|
||||||
|
(end -1.27 -1.27)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "56075374-a1e4-485e-b882-8e4cce7262d5")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 0.635 -1.27)
|
||||||
|
(end 1.27 -0.635)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "b0ddd3e4-2153-4cae-8cb5-d50cbfc8f47e")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.27 -0.635)
|
||||||
|
(end 1.27 16.51)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "2c20ee51-27de-4f59-83a5-2a2f58df65fb")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.27 16.51)
|
||||||
|
(end -1.27 16.51)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "d6de62ad-e175-43b9-85c8-340c8ab61fbc")
|
||||||
|
)
|
||||||
|
(fp_rect
|
||||||
|
(start -1.3716 -2.6416)
|
||||||
|
(end 19.1516 17.8816)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(fill no)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "6931ddba-5c8b-4684-9522-4250e5a9daf6")
|
||||||
|
)
|
||||||
|
(fp_text user "${REFERENCE}"
|
||||||
|
(at 0 7.62 90)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "d0f4c06e-6fb2-4a19-9b48-8ad674e44ff7")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1 1)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(pad "" np_thru_hole circle
|
||||||
|
(at 15.875 0.635)
|
||||||
|
(size 3.2 3.2)
|
||||||
|
(drill 3.2)
|
||||||
|
(layers "F&B.Cu" "*.Mask")
|
||||||
|
(uuid "71993a8f-c48d-437a-8a3c-3e63eb969208")
|
||||||
|
)
|
||||||
|
(pad "" np_thru_hole circle
|
||||||
|
(at 15.875 14.605)
|
||||||
|
(size 3.2 3.2)
|
||||||
|
(drill 3.2)
|
||||||
|
(layers "F&B.Cu" "*.Mask")
|
||||||
|
(uuid "ed9c8b97-5d58-4937-959a-98150878726c")
|
||||||
|
)
|
||||||
|
(pad "1" thru_hole rect
|
||||||
|
(at 0 0)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "1c83ddd7-1eb1-4a63-97b8-8cad20ed8adb")
|
||||||
|
)
|
||||||
|
(pad "2" thru_hole circle
|
||||||
|
(at 0 2.54)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "4b5aedc5-52ef-4559-9513-4cfe7a9290f5")
|
||||||
|
)
|
||||||
|
(pad "3" thru_hole circle
|
||||||
|
(at 0 5.08)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "d2e33fa3-db5c-4923-b9dd-22f0a7d95420")
|
||||||
|
)
|
||||||
|
(pad "4" thru_hole circle
|
||||||
|
(at 0 7.62)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "769680bd-a4f8-488a-9497-b63b5c793581")
|
||||||
|
)
|
||||||
|
(pad "5" thru_hole circle
|
||||||
|
(at 0 10.16)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "995ea8f5-e63f-41d7-93fe-dc90eacb16c7")
|
||||||
|
)
|
||||||
|
(pad "6" thru_hole circle
|
||||||
|
(at 0 12.7)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "3060d1e5-b1f1-47ad-9a39-6410cc4f3629")
|
||||||
|
)
|
||||||
|
(pad "7" thru_hole circle
|
||||||
|
(at 0 15.24)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "8651b2dd-3cf3-4c82-aa1a-ccc59e2c1675")
|
||||||
|
)
|
||||||
|
(embedded_fonts no)
|
||||||
|
(model "${KICAD9_3DMODEL_DIR}/Connector_PinSocket_2.54mm.3dshapes/PinSocket_1x07_P2.54mm_Vertical.step"
|
||||||
|
(offset
|
||||||
|
(xyz 0 0 0)
|
||||||
|
)
|
||||||
|
(scale
|
||||||
|
(xyz 1 1 1)
|
||||||
|
)
|
||||||
|
(rotate
|
||||||
|
(xyz 0 0 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
(footprint "Sparkfun_MicroSD_Breakout"
|
||||||
|
(version 20241229)
|
||||||
|
(generator "pcbnew")
|
||||||
|
(generator_version "9.0")
|
||||||
|
(layer "F.Cu")
|
||||||
|
(descr "Through hole straight socket strip, 1x07, 2.54mm pitch, single row (from Kicad 4.0.7), script generated")
|
||||||
|
(tags "Through hole socket strip THT 1x07 2.54mm single row")
|
||||||
|
(property "Reference" "REF**"
|
||||||
|
(at 0 -3.81 0)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "2649026f-8664-4aec-9bb7-08c001d0263d")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1.016 1.016)
|
||||||
|
(thickness 0.1524)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(property "Value" "Sparkfun_MicroSD_Breakout"
|
||||||
|
(at -8.255 19.05 0)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "befe8006-9d37-47c5-9cb4-916482040426")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1 1)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(property "Datasheet" ""
|
||||||
|
(at 0 0 0)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(hide yes)
|
||||||
|
(uuid "bda792b3-ed8e-4973-9481-8397712eba90")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1.27 1.27)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(property "Description" ""
|
||||||
|
(at 0 0 0)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(hide yes)
|
||||||
|
(uuid "2b01e065-879a-4a28-815b-34368d12b6af")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1.27 1.27)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(attr through_hole)
|
||||||
|
(fp_line
|
||||||
|
(start -1.33 1.27)
|
||||||
|
(end -1.33 16.57)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "c581ca6d-5026-479b-93ae-7bad6b62ddd9")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.33 1.27)
|
||||||
|
(end 1.33 1.27)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "d49fc77d-4bea-4acf-adb5-43551cb508f1")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.33 16.57)
|
||||||
|
(end 1.33 16.57)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "fa1edbcf-d278-4f63-83db-6cdbef0fe0fb")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 0 -1.33)
|
||||||
|
(end 1.33 -1.33)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "466d8a50-67a4-4117-b2ef-fd8fa3c112f8")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.33 -1.33)
|
||||||
|
(end 1.33 0)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "cc6f4d87-5f66-4c7a-b151-a392aa8f66ed")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.33 1.27)
|
||||||
|
(end 1.33 16.57)
|
||||||
|
(stroke
|
||||||
|
(width 0.12)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "dc1b7dd4-ca53-4049-ae81-eb11216e81d9")
|
||||||
|
)
|
||||||
|
(fp_rect
|
||||||
|
(start -20.574 -2.794)
|
||||||
|
(end 1.778 18.034)
|
||||||
|
(stroke
|
||||||
|
(width 0.1524)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(fill no)
|
||||||
|
(layer "F.SilkS")
|
||||||
|
(uuid "0af85dea-fde2-4a4a-83d8-7c1dc2a9aafa")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.8 -1.8)
|
||||||
|
(end 1.75 -1.8)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "abe8217b-b617-4a57-8b1b-aaad9a1a9408")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.8 17)
|
||||||
|
(end -1.8 -1.8)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "f6453894-baf7-4501-8b5d-b696bc45df5b")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.75 -1.8)
|
||||||
|
(end 1.75 17)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "9c21be3b-ef13-48a3-a28c-f5586af79a94")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.75 17)
|
||||||
|
(end -1.8 17)
|
||||||
|
(stroke
|
||||||
|
(width 0.05)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.CrtYd")
|
||||||
|
(uuid "27ab22be-40bf-42f8-a30e-00bb317bcedd")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.27 -1.27)
|
||||||
|
(end 0.635 -1.27)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "bbaf3582-2515-4a48-ac3f-6c1eac7a9c7a")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start -1.27 16.51)
|
||||||
|
(end -1.27 -1.27)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "7ba01148-ed5c-4058-bbbe-f7586e6b0fe3")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 0.635 -1.27)
|
||||||
|
(end 1.27 -0.635)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "449b7ca7-e50d-486c-95c9-ec3b9328d1e9")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.27 -0.635)
|
||||||
|
(end 1.27 16.51)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "a8fe924f-576e-43f4-bdbd-bede8f50e4a9")
|
||||||
|
)
|
||||||
|
(fp_line
|
||||||
|
(start 1.27 16.51)
|
||||||
|
(end -1.27 16.51)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "bc826cb1-1f13-4927-b007-dbc4e7bf9119")
|
||||||
|
)
|
||||||
|
(fp_rect
|
||||||
|
(start -20.5105 -2.8575)
|
||||||
|
(end 1.7145 18.0975)
|
||||||
|
(stroke
|
||||||
|
(width 0.1)
|
||||||
|
(type solid)
|
||||||
|
)
|
||||||
|
(fill no)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "ff136a20-1e67-47d3-967e-862544e6261a")
|
||||||
|
)
|
||||||
|
(fp_text user "${REFERENCE}"
|
||||||
|
(at 0 7.62 90)
|
||||||
|
(layer "F.Fab")
|
||||||
|
(uuid "c75ce234-326d-4a91-a01c-aa463620f982")
|
||||||
|
(effects
|
||||||
|
(font
|
||||||
|
(size 1 1)
|
||||||
|
(thickness 0.15)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
(pad "1" thru_hole rect
|
||||||
|
(at 0 0)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "94205f6f-c0ef-4190-8ab9-3ca1f6dfb4f7")
|
||||||
|
)
|
||||||
|
(pad "2" thru_hole circle
|
||||||
|
(at 0 2.54)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "b734434c-14a9-4700-99dc-f466e6509db7")
|
||||||
|
)
|
||||||
|
(pad "3" thru_hole circle
|
||||||
|
(at 0 5.08)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "971175f6-b389-4741-aaaf-0fb15d3e7fff")
|
||||||
|
)
|
||||||
|
(pad "4" thru_hole circle
|
||||||
|
(at 0 7.62)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "dca213e6-9378-4e06-9621-11be8656a233")
|
||||||
|
)
|
||||||
|
(pad "5" thru_hole circle
|
||||||
|
(at 0 10.16)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "c723245c-962a-4ab8-b0e1-229741e9138e")
|
||||||
|
)
|
||||||
|
(pad "6" thru_hole circle
|
||||||
|
(at 0 12.7)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "9b547c20-1f88-4220-82b8-d72f851fb6a1")
|
||||||
|
)
|
||||||
|
(pad "7" thru_hole circle
|
||||||
|
(at 0 15.24)
|
||||||
|
(size 1.7 1.7)
|
||||||
|
(drill 1)
|
||||||
|
(layers "*.Cu" "*.Mask")
|
||||||
|
(remove_unused_layers no)
|
||||||
|
(uuid "a588ed23-dbc2-406f-bc1c-3c6e69dd99ee")
|
||||||
|
)
|
||||||
|
(embedded_fonts no)
|
||||||
|
(model "${KICAD9_3DMODEL_DIR}/Connector_PinSocket_2.54mm.3dshapes/PinSocket_1x07_P2.54mm_Vertical.step"
|
||||||
|
(offset
|
||||||
|
(xyz 0 0 0)
|
||||||
|
)
|
||||||
|
(scale
|
||||||
|
(xyz 1 1 1)
|
||||||
|
)
|
||||||
|
(rotate
|
||||||
|
(xyz 0 0 0)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
4
hardware/tonberry-pico/fp-lib-table
Normal file
4
hardware/tonberry-pico/fp-lib-table
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
(fp_lib_table
|
||||||
|
(version 7)
|
||||||
|
(lib (name "Modules")(type "KiCad")(uri "${KIPRJMOD}/Modules.pretty")(options "")(descr ""))
|
||||||
|
)
|
||||||
28
hardware/tonberry-pico/tonberry-pico.kicad_dru
Normal file
28
hardware/tonberry-pico/tonberry-pico.kicad_dru
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
(version 1)
|
||||||
|
(rule "Pad to Silkscreen"
|
||||||
|
(constraint silk_clearance (min 0.15mm))
|
||||||
|
(layer outer)
|
||||||
|
(condition "A.Type == 'pad' && (B.Type == 'text' || B.Type == 'graphic')"))
|
||||||
|
|
||||||
|
(rule "drill hole size (mechanical)"
|
||||||
|
(constraint hole_size (min 0.2mm) (max 6.3mm)))
|
||||||
|
|
||||||
|
(rule "Minimum Via Hole Size"
|
||||||
|
(constraint hole_size (min 0.2mm))
|
||||||
|
(condition "A.Type == 'via'"))
|
||||||
|
|
||||||
|
(rule "Minimum Via Diameter"
|
||||||
|
(constraint via_diameter (min 0.35mm))
|
||||||
|
(condition "A.Type == 'via'"))
|
||||||
|
|
||||||
|
(rule "PTH Hole Size"
|
||||||
|
(constraint hole_size (min 0.2mm) (max 6.35mm))
|
||||||
|
(condition "A.isPlated()"))
|
||||||
|
|
||||||
|
(rule "Minimum Non-plated Hole Size"
|
||||||
|
(constraint hole_size (min 0.5mm))
|
||||||
|
(condition "A.Type == 'pad' && !A.isPlated()"))
|
||||||
|
|
||||||
|
(rule "Pad to Track clearance"
|
||||||
|
(constraint clearance (min 0.1mm))
|
||||||
|
(condition "A.isPlated() && A.Type != 'Via' && B.Type == 'track'"))
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,9 @@
|
|||||||
"active_layer_preset": "",
|
"active_layer_preset": "",
|
||||||
"auto_track_width": true,
|
"auto_track_width": true,
|
||||||
"hidden_netclasses": [],
|
"hidden_netclasses": [],
|
||||||
"hidden_nets": [],
|
"hidden_nets": [
|
||||||
|
"GND"
|
||||||
|
],
|
||||||
"high_contrast_mode": 0,
|
"high_contrast_mode": 0,
|
||||||
"net_color_mode": 1,
|
"net_color_mode": 1,
|
||||||
"opacity": {
|
"opacity": {
|
||||||
@@ -16,17 +18,17 @@
|
|||||||
"zones": 0.6
|
"zones": 0.6
|
||||||
},
|
},
|
||||||
"selection_filter": {
|
"selection_filter": {
|
||||||
"dimensions": true,
|
"dimensions": false,
|
||||||
"footprints": true,
|
"footprints": false,
|
||||||
"graphics": true,
|
"graphics": true,
|
||||||
"keepouts": true,
|
"keepouts": false,
|
||||||
"lockedItems": false,
|
"lockedItems": false,
|
||||||
"otherItems": true,
|
"otherItems": false,
|
||||||
"pads": true,
|
"pads": false,
|
||||||
"text": true,
|
"text": true,
|
||||||
"tracks": true,
|
"tracks": true,
|
||||||
"vias": true,
|
"vias": true,
|
||||||
"zones": true
|
"zones": false
|
||||||
},
|
},
|
||||||
"visible_items": [
|
"visible_items": [
|
||||||
"vias",
|
"vias",
|
||||||
@@ -49,7 +51,7 @@
|
|||||||
"conflict_shadows",
|
"conflict_shadows",
|
||||||
"shapes"
|
"shapes"
|
||||||
],
|
],
|
||||||
"visible_layers": "00000000_00000000_0fffffff_ffffffff",
|
"visible_layers": "00000000_00000000_0ffffff7_ffffffff",
|
||||||
"zone_display_mode": 0
|
"zone_display_mode": 0
|
||||||
},
|
},
|
||||||
"git": {
|
"git": {
|
||||||
@@ -63,9 +65,42 @@
|
|||||||
"version": 5
|
"version": 5
|
||||||
},
|
},
|
||||||
"net_inspector_panel": {
|
"net_inspector_panel": {
|
||||||
"col_hidden": [],
|
"col_hidden": [
|
||||||
"col_order": [],
|
false,
|
||||||
"col_widths": [],
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
],
|
||||||
|
"col_order": [
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
4,
|
||||||
|
5,
|
||||||
|
6,
|
||||||
|
7,
|
||||||
|
8,
|
||||||
|
9
|
||||||
|
],
|
||||||
|
"col_widths": [
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
"custom_group_rules": [],
|
"custom_group_rules": [],
|
||||||
"expanded_rows": [],
|
"expanded_rows": [],
|
||||||
"filter_by_net_name": true,
|
"filter_by_net_name": true,
|
||||||
@@ -76,7 +111,7 @@
|
|||||||
"show_unconnected_nets": false,
|
"show_unconnected_nets": false,
|
||||||
"show_zero_pad_nets": false,
|
"show_zero_pad_nets": false,
|
||||||
"sort_ascending": true,
|
"sort_ascending": true,
|
||||||
"sorting_column": -1
|
"sorting_column": 0
|
||||||
},
|
},
|
||||||
"open_jobsets": [],
|
"open_jobsets": [],
|
||||||
"project": {
|
"project": {
|
||||||
|
|||||||
@@ -2,12 +2,247 @@
|
|||||||
"board": {
|
"board": {
|
||||||
"3dviewports": [],
|
"3dviewports": [],
|
||||||
"design_settings": {
|
"design_settings": {
|
||||||
"defaults": {},
|
"defaults": {
|
||||||
"diff_pair_dimensions": [],
|
"apply_defaults_to_fp_fields": false,
|
||||||
"drc_exclusions": [],
|
"apply_defaults_to_fp_shapes": false,
|
||||||
"rules": {},
|
"apply_defaults_to_fp_text": false,
|
||||||
"track_widths": [],
|
"board_outline_line_width": 0.05,
|
||||||
"via_dimensions": []
|
"copper_line_width": 0.2,
|
||||||
|
"copper_text_italic": false,
|
||||||
|
"copper_text_size_h": 1.5,
|
||||||
|
"copper_text_size_v": 1.5,
|
||||||
|
"copper_text_thickness": 0.3,
|
||||||
|
"copper_text_upright": false,
|
||||||
|
"courtyard_line_width": 0.05,
|
||||||
|
"dimension_precision": 4,
|
||||||
|
"dimension_units": 3,
|
||||||
|
"dimensions": {
|
||||||
|
"arrow_length": 1270000,
|
||||||
|
"extension_offset": 500000,
|
||||||
|
"keep_text_aligned": true,
|
||||||
|
"suppress_zeroes": true,
|
||||||
|
"text_position": 0,
|
||||||
|
"units_format": 0
|
||||||
|
},
|
||||||
|
"fab_line_width": 0.1,
|
||||||
|
"fab_text_italic": false,
|
||||||
|
"fab_text_size_h": 1.0,
|
||||||
|
"fab_text_size_v": 1.0,
|
||||||
|
"fab_text_thickness": 0.15,
|
||||||
|
"fab_text_upright": false,
|
||||||
|
"other_line_width": 0.1,
|
||||||
|
"other_text_italic": false,
|
||||||
|
"other_text_size_h": 1.0,
|
||||||
|
"other_text_size_v": 1.0,
|
||||||
|
"other_text_thickness": 0.15,
|
||||||
|
"other_text_upright": false,
|
||||||
|
"pads": {
|
||||||
|
"drill": 0.8,
|
||||||
|
"height": 1.6,
|
||||||
|
"width": 1.6
|
||||||
|
},
|
||||||
|
"silk_line_width": 0.1,
|
||||||
|
"silk_text_italic": false,
|
||||||
|
"silk_text_size_h": 1.0,
|
||||||
|
"silk_text_size_v": 1.0,
|
||||||
|
"silk_text_thickness": 0.1,
|
||||||
|
"silk_text_upright": false,
|
||||||
|
"zones": {
|
||||||
|
"min_clearance": 0.4064
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"diff_pair_dimensions": [
|
||||||
|
{
|
||||||
|
"gap": 0.0,
|
||||||
|
"via_gap": 0.0,
|
||||||
|
"width": 0.0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"drc_exclusions": [
|
||||||
|
[
|
||||||
|
"lib_footprint_mismatch|148587019|59126370|4f17c230-1bc1-45e3-8816-9cf9d13e3a5c|00000000-0000-0000-0000-000000000000",
|
||||||
|
""
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"nonmirrored_text_on_back_layer|213233000|81790000|e87ca627-5327-4f7e-ac1a-c1eadd662c0a|00000000-0000-0000-0000-000000000000",
|
||||||
|
""
|
||||||
|
]
|
||||||
|
],
|
||||||
|
"meta": {
|
||||||
|
"filename": "board_design_settings.json",
|
||||||
|
"version": 2
|
||||||
|
},
|
||||||
|
"rule_severities": {
|
||||||
|
"annular_width": "error",
|
||||||
|
"clearance": "error",
|
||||||
|
"connection_width": "warning",
|
||||||
|
"copper_edge_clearance": "error",
|
||||||
|
"copper_sliver": "warning",
|
||||||
|
"courtyards_overlap": "error",
|
||||||
|
"creepage": "error",
|
||||||
|
"diff_pair_gap_out_of_range": "error",
|
||||||
|
"diff_pair_uncoupled_length_too_long": "error",
|
||||||
|
"drill_out_of_range": "error",
|
||||||
|
"duplicate_footprints": "warning",
|
||||||
|
"extra_footprint": "warning",
|
||||||
|
"footprint": "error",
|
||||||
|
"footprint_filters_mismatch": "ignore",
|
||||||
|
"footprint_symbol_mismatch": "warning",
|
||||||
|
"footprint_type_mismatch": "ignore",
|
||||||
|
"hole_clearance": "error",
|
||||||
|
"hole_to_hole": "warning",
|
||||||
|
"holes_co_located": "warning",
|
||||||
|
"invalid_outline": "error",
|
||||||
|
"isolated_copper": "warning",
|
||||||
|
"item_on_disabled_layer": "error",
|
||||||
|
"items_not_allowed": "error",
|
||||||
|
"length_out_of_range": "error",
|
||||||
|
"lib_footprint_issues": "warning",
|
||||||
|
"lib_footprint_mismatch": "warning",
|
||||||
|
"malformed_courtyard": "error",
|
||||||
|
"microvia_drill_out_of_range": "error",
|
||||||
|
"mirrored_text_on_front_layer": "warning",
|
||||||
|
"missing_courtyard": "ignore",
|
||||||
|
"missing_footprint": "warning",
|
||||||
|
"net_conflict": "warning",
|
||||||
|
"nonmirrored_text_on_back_layer": "warning",
|
||||||
|
"npth_inside_courtyard": "ignore",
|
||||||
|
"padstack": "warning",
|
||||||
|
"pth_inside_courtyard": "ignore",
|
||||||
|
"shorting_items": "error",
|
||||||
|
"silk_edge_clearance": "warning",
|
||||||
|
"silk_over_copper": "warning",
|
||||||
|
"silk_overlap": "warning",
|
||||||
|
"skew_out_of_range": "error",
|
||||||
|
"solder_mask_bridge": "error",
|
||||||
|
"starved_thermal": "error",
|
||||||
|
"text_height": "warning",
|
||||||
|
"text_on_edge_cuts": "error",
|
||||||
|
"text_thickness": "warning",
|
||||||
|
"through_hole_pad_without_hole": "error",
|
||||||
|
"too_many_vias": "error",
|
||||||
|
"track_angle": "error",
|
||||||
|
"track_dangling": "warning",
|
||||||
|
"track_segment_length": "error",
|
||||||
|
"track_width": "error",
|
||||||
|
"tracks_crossing": "error",
|
||||||
|
"unconnected_items": "error",
|
||||||
|
"unresolved_variable": "error",
|
||||||
|
"via_dangling": "warning",
|
||||||
|
"zones_intersect": "error"
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"max_error": 0.005,
|
||||||
|
"min_clearance": 0.15,
|
||||||
|
"min_connection": 0.0,
|
||||||
|
"min_copper_edge_clearance": 0.2,
|
||||||
|
"min_groove_width": 0.0,
|
||||||
|
"min_hole_clearance": 0.35,
|
||||||
|
"min_hole_to_hole": 0.2,
|
||||||
|
"min_microvia_diameter": 0.2,
|
||||||
|
"min_microvia_drill": 0.1,
|
||||||
|
"min_resolved_spokes": 2,
|
||||||
|
"min_silk_clearance": 0.0,
|
||||||
|
"min_text_height": 1.0,
|
||||||
|
"min_text_thickness": 0.15,
|
||||||
|
"min_through_hole_diameter": 0.2,
|
||||||
|
"min_track_width": 0.15,
|
||||||
|
"min_via_annular_width": 0.15,
|
||||||
|
"min_via_diameter": 0.35,
|
||||||
|
"solder_mask_to_copper_clearance": 0.005,
|
||||||
|
"use_height_for_length_calcs": true
|
||||||
|
},
|
||||||
|
"teardrop_options": [
|
||||||
|
{
|
||||||
|
"td_onpthpad": true,
|
||||||
|
"td_onroundshapesonly": false,
|
||||||
|
"td_onsmdpad": true,
|
||||||
|
"td_ontrackend": false,
|
||||||
|
"td_onvia": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"teardrop_parameters": [
|
||||||
|
{
|
||||||
|
"td_allow_use_two_tracks": true,
|
||||||
|
"td_curve_segcount": 0,
|
||||||
|
"td_height_ratio": 1.0,
|
||||||
|
"td_length_ratio": 0.5,
|
||||||
|
"td_maxheight": 2.0,
|
||||||
|
"td_maxlen": 1.0,
|
||||||
|
"td_on_pad_in_zone": false,
|
||||||
|
"td_target_name": "td_round_shape",
|
||||||
|
"td_width_to_size_filter_ratio": 0.9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"td_allow_use_two_tracks": true,
|
||||||
|
"td_curve_segcount": 0,
|
||||||
|
"td_height_ratio": 1.0,
|
||||||
|
"td_length_ratio": 0.5,
|
||||||
|
"td_maxheight": 2.0,
|
||||||
|
"td_maxlen": 1.0,
|
||||||
|
"td_on_pad_in_zone": false,
|
||||||
|
"td_target_name": "td_rect_shape",
|
||||||
|
"td_width_to_size_filter_ratio": 0.9
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"td_allow_use_two_tracks": true,
|
||||||
|
"td_curve_segcount": 0,
|
||||||
|
"td_height_ratio": 1.0,
|
||||||
|
"td_length_ratio": 0.5,
|
||||||
|
"td_maxheight": 2.0,
|
||||||
|
"td_maxlen": 1.0,
|
||||||
|
"td_on_pad_in_zone": false,
|
||||||
|
"td_target_name": "td_track_end",
|
||||||
|
"td_width_to_size_filter_ratio": 0.9
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"track_widths": [
|
||||||
|
0.0,
|
||||||
|
0.2032,
|
||||||
|
0.4064,
|
||||||
|
0.508
|
||||||
|
],
|
||||||
|
"tuning_pattern_settings": {
|
||||||
|
"diff_pair_defaults": {
|
||||||
|
"corner_radius_percentage": 80,
|
||||||
|
"corner_style": 1,
|
||||||
|
"max_amplitude": 1.0,
|
||||||
|
"min_amplitude": 0.2,
|
||||||
|
"single_sided": false,
|
||||||
|
"spacing": 1.0
|
||||||
|
},
|
||||||
|
"diff_pair_skew_defaults": {
|
||||||
|
"corner_radius_percentage": 80,
|
||||||
|
"corner_style": 1,
|
||||||
|
"max_amplitude": 1.0,
|
||||||
|
"min_amplitude": 0.2,
|
||||||
|
"single_sided": false,
|
||||||
|
"spacing": 0.6
|
||||||
|
},
|
||||||
|
"single_track_defaults": {
|
||||||
|
"corner_radius_percentage": 80,
|
||||||
|
"corner_style": 1,
|
||||||
|
"max_amplitude": 1.0,
|
||||||
|
"min_amplitude": 0.2,
|
||||||
|
"single_sided": false,
|
||||||
|
"spacing": 0.6
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"via_dimensions": [
|
||||||
|
{
|
||||||
|
"diameter": 0.0,
|
||||||
|
"drill": 0.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"diameter": 0.6,
|
||||||
|
"drill": 0.3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"diameter": 0.8,
|
||||||
|
"drill": 0.4
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"zones_allow_external_fillets": false
|
||||||
},
|
},
|
||||||
"ipc2581": {
|
"ipc2581": {
|
||||||
"dist": "",
|
"dist": "",
|
||||||
@@ -25,7 +260,12 @@
|
|||||||
"equivalence_files": []
|
"equivalence_files": []
|
||||||
},
|
},
|
||||||
"erc": {
|
"erc": {
|
||||||
"erc_exclusions": [],
|
"erc_exclusions": [
|
||||||
|
[
|
||||||
|
"pin_to_pin|1549400|977900|f11a46f2-13fb-4d63-9b53-b032e49a375d|4b6603d6-e95b-4131-942b-96ddc8a9a606|/7226ca0f-138a-46b4-89f3-dc32dfb1f9f7|/7226ca0f-138a-46b4-89f3-dc32dfb1f9f7|/7226ca0f-138a-46b4-89f3-dc32dfb1f9f7",
|
||||||
|
"Raspberry Pi Pico W Datasheet p. 9: If the ADC is not used or ADC performance is not critical, this pin can be connected to digital ground."
|
||||||
|
]
|
||||||
|
],
|
||||||
"meta": {
|
"meta": {
|
||||||
"version": 0
|
"version": 0
|
||||||
},
|
},
|
||||||
@@ -213,9 +453,9 @@
|
|||||||
"extra_units": "error",
|
"extra_units": "error",
|
||||||
"footprint_filter": "ignore",
|
"footprint_filter": "ignore",
|
||||||
"footprint_link_issues": "warning",
|
"footprint_link_issues": "warning",
|
||||||
"four_way_junction": "ignore",
|
"four_way_junction": "warning",
|
||||||
"global_label_dangling": "warning",
|
"global_label_dangling": "warning",
|
||||||
"hier_label_mismatch": "error",
|
"hier_label_mismatch": "warning",
|
||||||
"label_dangling": "error",
|
"label_dangling": "error",
|
||||||
"label_multiple_wires": "warning",
|
"label_multiple_wires": "warning",
|
||||||
"lib_symbol_issues": "warning",
|
"lib_symbol_issues": "warning",
|
||||||
@@ -230,16 +470,17 @@
|
|||||||
"no_connect_dangling": "warning",
|
"no_connect_dangling": "warning",
|
||||||
"pin_not_connected": "error",
|
"pin_not_connected": "error",
|
||||||
"pin_not_driven": "error",
|
"pin_not_driven": "error",
|
||||||
"pin_to_pin": "warning",
|
"pin_to_pin": "error",
|
||||||
"power_pin_not_driven": "error",
|
"power_pin_not_driven": "error",
|
||||||
"same_local_global_label": "warning",
|
"same_local_global_label": "warning",
|
||||||
"similar_label_and_power": "warning",
|
"similar_label_and_power": "warning",
|
||||||
"similar_labels": "warning",
|
"similar_labels": "warning",
|
||||||
"similar_power": "warning",
|
"similar_power": "warning",
|
||||||
"simulation_model_issue": "ignore",
|
"simulation_model_issue": "ignore",
|
||||||
"single_global_label": "ignore",
|
"single_global_label": "warning",
|
||||||
"unannotated": "error",
|
"unannotated": "error",
|
||||||
"unconnected_wire_endpoint": "warning",
|
"unconnected_wire_endpoint": "warning",
|
||||||
|
"undefined_netclass": "error",
|
||||||
"unit_value_mismatch": "error",
|
"unit_value_mismatch": "error",
|
||||||
"unresolved_variable": "error",
|
"unresolved_variable": "error",
|
||||||
"wire_dangling": "error"
|
"wire_dangling": "error"
|
||||||
@@ -257,7 +498,7 @@
|
|||||||
"classes": [
|
"classes": [
|
||||||
{
|
{
|
||||||
"bus_width": 12,
|
"bus_width": 12,
|
||||||
"clearance": 0.2,
|
"clearance": 0.1778,
|
||||||
"diff_pair_gap": 0.25,
|
"diff_pair_gap": 0.25,
|
||||||
"diff_pair_via_gap": 0.25,
|
"diff_pair_via_gap": 0.25,
|
||||||
"diff_pair_width": 0.2,
|
"diff_pair_width": 0.2,
|
||||||
@@ -268,10 +509,20 @@
|
|||||||
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||||
"priority": 2147483647,
|
"priority": 2147483647,
|
||||||
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||||
"track_width": 0.2,
|
"track_width": 0.2032,
|
||||||
"via_diameter": 0.6,
|
"via_diameter": 0.8,
|
||||||
"via_drill": 0.3,
|
"via_drill": 0.4,
|
||||||
"wire_width": 6
|
"wire_width": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"clearance": 0.1778,
|
||||||
|
"name": "Power",
|
||||||
|
"pcb_color": "rgba(0, 0, 0, 0.000)",
|
||||||
|
"priority": 0,
|
||||||
|
"schematic_color": "rgba(0, 0, 0, 0.000)",
|
||||||
|
"track_width": 0.4064,
|
||||||
|
"via_diameter": 0.8,
|
||||||
|
"via_drill": 0.4
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"meta": {
|
"meta": {
|
||||||
@@ -279,14 +530,27 @@
|
|||||||
},
|
},
|
||||||
"net_colors": null,
|
"net_colors": null,
|
||||||
"netclass_assignments": null,
|
"netclass_assignments": null,
|
||||||
"netclass_patterns": []
|
"netclass_patterns": [
|
||||||
|
{
|
||||||
|
"netclass": "Power",
|
||||||
|
"pattern": "+*"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"netclass": "Power",
|
||||||
|
"pattern": "GND"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"netclass": "Power",
|
||||||
|
"pattern": "/*BAT"
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"pcbnew": {
|
"pcbnew": {
|
||||||
"last_paths": {
|
"last_paths": {
|
||||||
"gencad": "",
|
"gencad": "",
|
||||||
"idf": "",
|
"idf": "",
|
||||||
"netlist": "",
|
"netlist": "",
|
||||||
"plot": "",
|
"plot": "pcb_test/",
|
||||||
"pos_files": "",
|
"pos_files": "",
|
||||||
"specctra_dsn": "",
|
"specctra_dsn": "",
|
||||||
"step": "",
|
"step": "",
|
||||||
@@ -347,12 +611,36 @@
|
|||||||
"label": "DNP",
|
"label": "DNP",
|
||||||
"name": "${DNP}",
|
"name": "${DNP}",
|
||||||
"show": true
|
"show": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": false,
|
||||||
|
"label": "#",
|
||||||
|
"name": "${ITEM_NUMBER}",
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": false,
|
||||||
|
"label": "Description",
|
||||||
|
"name": "Description",
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": false,
|
||||||
|
"label": "Sim.Pins",
|
||||||
|
"name": "Sim.Pins",
|
||||||
|
"show": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"group_by": false,
|
||||||
|
"label": "Sim.Device",
|
||||||
|
"name": "Sim.Device",
|
||||||
|
"show": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"filter_string": "",
|
"filter_string": "",
|
||||||
"group_symbols": true,
|
"group_symbols": true,
|
||||||
"include_excluded_from_bom": false,
|
"include_excluded_from_bom": false,
|
||||||
"name": "Grouped By Value",
|
"name": "",
|
||||||
"sort_asc": true,
|
"sort_asc": true,
|
||||||
"sort_field": "Reference"
|
"sort_field": "Reference"
|
||||||
},
|
},
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -50,13 +50,13 @@ add_test(NAME generate-xml-report
|
|||||||
set_tests_properties(clean-reports PROPERTIES FIXTURES_SETUP "Report")
|
set_tests_properties(clean-reports PROPERTIES FIXTURES_SETUP "Report")
|
||||||
set_tests_properties(generate-xml-report PROPERTIES FIXTURES_CLEANUP "Report")
|
set_tests_properties(generate-xml-report PROPERTIES FIXTURES_CLEANUP "Report")
|
||||||
|
|
||||||
add_subdirectory(src/audiocore)
|
add_subdirectory(modules/audiocore)
|
||||||
|
|
||||||
add_custom_target(check-format
|
add_custom_target(check-format
|
||||||
find . -iname '*.[ch]' -exec clang-format -Werror --dry-run {} +
|
find . -iname '*.[ch]' -exec clang-format -Werror --dry-run {} +
|
||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/modules
|
||||||
)
|
)
|
||||||
add_custom_target(clang-format
|
add_custom_target(clang-format
|
||||||
find . -iname '*.[ch]' -exec clang-format -i {} +
|
find . -iname '*.[ch]' -exec clang-format -i {} +
|
||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/src
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/modules
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ FROM gitea/runner-images:ubuntu-22.04
|
|||||||
# Install gcc-arm-none-eabi
|
# Install gcc-arm-none-eabi
|
||||||
RUN apt update && \
|
RUN apt update && \
|
||||||
DEBIAN_FRONTEND=noninteractive \
|
DEBIAN_FRONTEND=noninteractive \
|
||||||
apt install -y --no-install-recommends cmake gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib \
|
apt install -y --no-install-recommends cmake gcc-arm-none-eabi libnewlib-arm-none-eabi libstdc++-arm-none-eabi-newlib cpio \
|
||||||
&& apt clean && rm -rf /var/lib/apt/lists/*
|
&& apt clean && rm -rf /var/lib/apt/lists/*
|
||||||
|
|||||||
3
software/boards/RPI_PICO_W/manifest-Rev1.py
Normal file
3
software/boards/RPI_PICO_W/manifest-Rev1.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
include("manifest.py")
|
||||||
|
|
||||||
|
module("hwconfig.py", "../../src/hwconfig_Rev1")
|
||||||
3
software/boards/RPI_PICO_W/manifest-breadboard.py
Normal file
3
software/boards/RPI_PICO_W/manifest-breadboard.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
include("manifest.py")
|
||||||
|
|
||||||
|
module("hwconfig.py", "../../src/hwconfig_breadboard")
|
||||||
@@ -5,8 +5,7 @@ require("bundle-networking")
|
|||||||
# Bluetooth
|
# Bluetooth
|
||||||
require("aioble")
|
require("aioble")
|
||||||
|
|
||||||
module("rp2_neopixel.py", "../../src")
|
# AsyncIO REPL
|
||||||
require("sdcard")
|
|
||||||
require("aiorepl")
|
require("aiorepl")
|
||||||
|
|
||||||
# Third party modules
|
# Third party modules
|
||||||
@@ -14,5 +13,14 @@ module("mfrc522.py", "../../lib/micropython-mfrc522/")
|
|||||||
module("microdot.py", "../../lib/microdot/src/microdot/")
|
module("microdot.py", "../../lib/microdot/src/microdot/")
|
||||||
|
|
||||||
# TonberryPico modules
|
# TonberryPico modules
|
||||||
module("audiocore.py", "../../src/audiocore")
|
module("audiocore.py", "../../modules/audiocore")
|
||||||
package("nfc", base_path="../../src/")
|
module("rp2_neopixel.py", "../../modules")
|
||||||
|
|
||||||
|
module("main.py", "../../src")
|
||||||
|
module("app.py", "../../src")
|
||||||
|
module("mp3player.py", "../../src")
|
||||||
|
module("webserver.py", "../../src")
|
||||||
|
package("utils", base_path="../../src")
|
||||||
|
package("nfc", base_path="../../src")
|
||||||
|
|
||||||
|
module("frozen_frontend.py", "../../build")
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ set(MICROPY_PY_BLUETOOTH ON)
|
|||||||
set(MICROPY_BLUETOOTH_BTSTACK ON)
|
set(MICROPY_BLUETOOTH_BTSTACK ON)
|
||||||
set(MICROPY_PY_BLUETOOTH_CYW43 ON)
|
set(MICROPY_PY_BLUETOOTH_CYW43 ON)
|
||||||
|
|
||||||
|
set(MICROPY_PY_BTREE ON)
|
||||||
|
|
||||||
# Board specific version of the frozen manifest
|
# Board specific version of the frozen manifest
|
||||||
set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py)
|
set(MICROPY_FROZEN_MANIFEST ${MICROPY_BOARD_DIR}/manifest.py)
|
||||||
|
|
||||||
|
|||||||
@@ -25,3 +25,12 @@ int mp_hal_is_pin_reserved(int n);
|
|||||||
#define MICROPY_HW_PIN_RESERVED(i) mp_hal_is_pin_reserved(i)
|
#define MICROPY_HW_PIN_RESERVED(i) mp_hal_is_pin_reserved(i)
|
||||||
|
|
||||||
#define MICROPY_PY_THREAD (0)
|
#define MICROPY_PY_THREAD (0)
|
||||||
|
|
||||||
|
#define TONBERRY_POWER_EN 22
|
||||||
|
|
||||||
|
#define MICROPY_BOARD_STARTUP() \
|
||||||
|
{ \
|
||||||
|
gpio_init(TONBERRY_POWER_EN); \
|
||||||
|
gpio_set_dir(TONBERRY_POWER_EN, true); \
|
||||||
|
gpio_put(TONBERRY_POWER_EN, true); \
|
||||||
|
}
|
||||||
|
|||||||
5
software/boards/tonberry_unix/manifest.py
Normal file
5
software/boards/tonberry_unix/manifest.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
include("$(PORT_DIR)/variants/manifest.py")
|
||||||
|
|
||||||
|
include("$(MPY_DIR)/extmod/asyncio")
|
||||||
|
|
||||||
|
module("microdot.py", "../../lib/microdot/src/microdot/")
|
||||||
31
software/boards/tonberry_unix/mpconfigvariant.h
Normal file
31
software/boards/tonberry_unix/mpconfigvariant.h
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of the MicroPython project, http://micropython.org/
|
||||||
|
*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2019 Damien P. George
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Set base feature level.
|
||||||
|
#define MICROPY_CONFIG_ROM_LEVEL (MICROPY_CONFIG_ROM_LEVEL_EXTRA_FEATURES)
|
||||||
|
|
||||||
|
// Enable extra Unix features.
|
||||||
|
#include "mpconfigvariant_common.h"
|
||||||
3
software/boards/tonberry_unix/mpconfigvariant.mk
Normal file
3
software/boards/tonberry_unix/mpconfigvariant.mk
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# This is the default variant when you `make` the Unix port.
|
||||||
|
|
||||||
|
FROZEN_MANIFEST ?= $(VARIANT_DIR)/manifest.py
|
||||||
126
software/boards/tonberry_unix/mpconfigvariant_common.h
Normal file
126
software/boards/tonberry_unix/mpconfigvariant_common.h
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of the MicroPython project, http://micropython.org/
|
||||||
|
*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright (c) 2022 Jim Mussared
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This file enables and configures features common to all variants
|
||||||
|
// other than "minimal".
|
||||||
|
|
||||||
|
// Send raise KeyboardInterrupt directly from the signal handler rather than
|
||||||
|
// scheduling it into the VM.
|
||||||
|
#define MICROPY_ASYNC_KBD_INTR (!MICROPY_PY_THREAD_GIL)
|
||||||
|
|
||||||
|
// Enable helpers for printing debugging information.
|
||||||
|
#ifndef MICROPY_DEBUG_PRINTERS
|
||||||
|
#define MICROPY_DEBUG_PRINTERS (1)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Enable floating point by default.
|
||||||
|
#ifndef MICROPY_FLOAT_IMPL
|
||||||
|
#define MICROPY_FLOAT_IMPL (MICROPY_FLOAT_IMPL_DOUBLE)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Don't use native _Float16 because it increases code size by a lot.
|
||||||
|
#ifndef MICROPY_FLOAT_USE_NATIVE_FLT16
|
||||||
|
#define MICROPY_FLOAT_USE_NATIVE_FLT16 (0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Enable arbitrary precision long-int by default.
|
||||||
|
#ifndef MICROPY_LONGINT_IMPL
|
||||||
|
#define MICROPY_LONGINT_IMPL (MICROPY_LONGINT_IMPL_MPZ)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Enable use of C libraries that need read/write/lseek/fsync, e.g. axtls.
|
||||||
|
#define MICROPY_STREAMS_POSIX_API (1)
|
||||||
|
|
||||||
|
// REPL conveniences.
|
||||||
|
#define MICROPY_REPL_EMACS_WORDS_MOVE (1)
|
||||||
|
#define MICROPY_REPL_EMACS_EXTRA_WORDS_MOVE (1)
|
||||||
|
#define MICROPY_USE_READLINE_HISTORY (1)
|
||||||
|
#ifndef MICROPY_READLINE_HISTORY_SIZE
|
||||||
|
#define MICROPY_READLINE_HISTORY_SIZE (50)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Seed random on import.
|
||||||
|
#define MICROPY_PY_RANDOM_SEED_INIT_FUNC (mp_random_seed_init())
|
||||||
|
|
||||||
|
// Allow exception details in low-memory conditions.
|
||||||
|
#define MICROPY_ENABLE_EMERGENCY_EXCEPTION_BUF (1)
|
||||||
|
#define MICROPY_EMERGENCY_EXCEPTION_BUF_SIZE (256)
|
||||||
|
|
||||||
|
// Allow loading of .mpy files.
|
||||||
|
#define MICROPY_PERSISTENT_CODE_LOAD (1)
|
||||||
|
|
||||||
|
// Extra memory debugging.
|
||||||
|
#define MICROPY_MALLOC_USES_ALLOCATED_SIZE (1)
|
||||||
|
#define MICROPY_MEM_STATS (1)
|
||||||
|
|
||||||
|
// Enable a small performance boost for the VM.
|
||||||
|
#define MICROPY_OPT_COMPUTED_GOTO (1)
|
||||||
|
|
||||||
|
// Return number of collected objects from gc.collect().
|
||||||
|
#define MICROPY_PY_GC_COLLECT_RETVAL (1)
|
||||||
|
|
||||||
|
// Enable detailed error messages and warnings.
|
||||||
|
#define MICROPY_ERROR_REPORTING (MICROPY_ERROR_REPORTING_DETAILED)
|
||||||
|
#define MICROPY_WARNINGS (1)
|
||||||
|
#define MICROPY_PY_STR_BYTES_CMP_WARN (1)
|
||||||
|
|
||||||
|
// Configure the "sys" module with features not usually enabled on bare-metal.
|
||||||
|
#define MICROPY_PY_SYS_ATEXIT (1)
|
||||||
|
#define MICROPY_PY_SYS_EXC_INFO (1)
|
||||||
|
|
||||||
|
// Configure the "os" module with extra unix features.
|
||||||
|
#define MICROPY_PY_OS_INCLUDEFILE "ports/unix/modos.c"
|
||||||
|
#define MICROPY_PY_OS_ERRNO (1)
|
||||||
|
#define MICROPY_PY_OS_GETENV_PUTENV_UNSETENV (1)
|
||||||
|
#define MICROPY_PY_OS_SYSTEM (1)
|
||||||
|
#define MICROPY_PY_OS_URANDOM (1)
|
||||||
|
|
||||||
|
// Enable the unix-specific "time" module.
|
||||||
|
#define MICROPY_PY_TIME (1)
|
||||||
|
#define MICROPY_PY_TIME_TIME_TIME_NS (1)
|
||||||
|
#define MICROPY_PY_TIME_CUSTOM_SLEEP (1)
|
||||||
|
#define MICROPY_PY_TIME_INCLUDEFILE "ports/unix/modtime.c"
|
||||||
|
|
||||||
|
#if MICROPY_PY_SSL
|
||||||
|
#define MICROPY_PY_HASHLIB_MD5 (1)
|
||||||
|
#define MICROPY_PY_HASHLIB_SHA1 (1)
|
||||||
|
#define MICROPY_PY_CRYPTOLIB (1)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// The "select" module is enabled by default, but disable select.select().
|
||||||
|
#define MICROPY_PY_SELECT_POSIX_OPTIMISATIONS (1)
|
||||||
|
#define MICROPY_PY_SELECT_SELECT (0)
|
||||||
|
|
||||||
|
// Enable the "websocket" module.
|
||||||
|
#define MICROPY_PY_WEBSOCKET (1)
|
||||||
|
|
||||||
|
// Enable the "machine" module, mostly for machine.mem*.
|
||||||
|
#define MICROPY_PY_MACHINE (1)
|
||||||
|
#define MICROPY_PY_MACHINE_PULSE (1)
|
||||||
|
#define MICROPY_PY_MACHINE_PIN_BASE (1)
|
||||||
|
|
||||||
|
#define MICROPY_VFS_ROM (1)
|
||||||
|
#define MICROPY_VFS_ROM_IOCTL (0)
|
||||||
@@ -6,9 +6,60 @@ set -eu
|
|||||||
|
|
||||||
( cd lib/micropython
|
( cd lib/micropython
|
||||||
make -C mpy-cross -j "$(nproc)"
|
make -C mpy-cross -j "$(nproc)"
|
||||||
make -C ports/rp2 BOARD=TONBERRY_RPI_PICO_W BOARD_DIR="$TOPDIR"/boards/RPI_PICO_W clean
|
|
||||||
make -C ports/rp2 BOARD=TONBERRY_RPI_PICO_W BOARD_DIR="$TOPDIR"/boards/RPI_PICO_W \
|
# build tonberry specific unix port of micropython
|
||||||
USER_C_MODULES="$TOPDIR"/src/micropython.cmake -j "$(nproc)"
|
make -C ports/unix VARIANT_DIR="$TOPDIR"/boards/tonberry_unix clean
|
||||||
|
make -C ports/unix VARIANT_DIR="$TOPDIR"/boards/tonberry_unix -j "$(nproc)"
|
||||||
)
|
)
|
||||||
|
|
||||||
echo "Output in lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/firmware.uf2"
|
( cd tools/mklittlefs
|
||||||
|
make -j "$(nproc)"
|
||||||
|
)
|
||||||
|
|
||||||
|
BUILDDIR=lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/
|
||||||
|
BUILDDIR_UNIX=lib/micropython/ports/unix/build-tonberry_unix/
|
||||||
|
OUTDIR=$(pwd)/build
|
||||||
|
mkdir -p "$OUTDIR"
|
||||||
|
FS_STAGE_DIR=$(mktemp -d)
|
||||||
|
mkdir "$FS_STAGE_DIR"/fs
|
||||||
|
trap 'rm -rf $FS_STAGE_DIR' EXIT
|
||||||
|
tools/mklittlefs/mklittlefs -p 256 -s 868352 -c "$FS_STAGE_DIR"/fs "$FS_STAGE_DIR"/filesystem.bin
|
||||||
|
|
||||||
|
FRONTEND_STAGE_DIR=$(mktemp -d)
|
||||||
|
trap 'rm -rf $FRONTEND_STAGE_DIR' EXIT
|
||||||
|
gzip -c frontend/index.html > "$FRONTEND_STAGE_DIR"/index.html.gz
|
||||||
|
python -m freezefs "$FRONTEND_STAGE_DIR" build/frozen_frontend.py --target=/frontend
|
||||||
|
|
||||||
|
for hwconfig in boards/RPI_PICO_W/manifest-*.py; do
|
||||||
|
hwconfig_base=$(basename "$hwconfig")
|
||||||
|
hwname=${hwconfig_base##manifest-}
|
||||||
|
hwname=${hwname%%.py}
|
||||||
|
hwconfig_abs=$(realpath "$hwconfig")
|
||||||
|
( cd lib/micropython
|
||||||
|
make -C ports/rp2 BOARD=TONBERRY_RPI_PICO_W BOARD_DIR="$TOPDIR"/boards/RPI_PICO_W clean
|
||||||
|
make -C ports/rp2 BOARD=TONBERRY_RPI_PICO_W BOARD_DIR="$TOPDIR"/boards/RPI_PICO_W \
|
||||||
|
USER_C_MODULES="$TOPDIR"/modules/micropython.cmake \
|
||||||
|
FROZEN_MANIFEST="$hwconfig_abs" -j "$(nproc)"
|
||||||
|
)
|
||||||
|
PICOTOOL=picotool
|
||||||
|
if ! command -v $PICOTOOL >/dev/null 2>&1; then
|
||||||
|
echo "system picotool not found, checking SDK build dir"
|
||||||
|
PICOTOOL=lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/_deps/picotool-build/picotool
|
||||||
|
if ! command -v $PICOTOOL >/dev/null 2>&1; then
|
||||||
|
echo "No picotool found, exiting"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
truncate -s 2M $BUILDDIR/firmware-filesystem.bin
|
||||||
|
dd if=$BUILDDIR/firmware.bin of=$BUILDDIR/firmware-filesystem.bin bs=1k
|
||||||
|
dd if="$FS_STAGE_DIR"/filesystem.bin of=$BUILDDIR/firmware-filesystem.bin bs=1k seek=1200
|
||||||
|
cp $BUILDDIR/firmware.uf2 "$OUTDIR"/firmware-"$hwname".uf2
|
||||||
|
$PICOTOOL uf2 convert $BUILDDIR/firmware-filesystem.bin "$OUTDIR"/firmware-filesystem-"$hwname".uf2
|
||||||
|
done
|
||||||
|
|
||||||
|
cp "$BUILDDIR_UNIX"/micropython "$OUTDIR"/micropython-tonberry_unix
|
||||||
|
chmod u+x "$OUTDIR"/micropython-tonberry_unix
|
||||||
|
|
||||||
|
echo "Output in" "${OUTDIR}"/firmware-*.uf2
|
||||||
|
echo "Images with filesystem in" "${OUTDIR}"/firmware-filesystem-*.uf2
|
||||||
|
echo "Unix build in" "${OUTDIR}"/micropython-tonberry_unix
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
|
TOPDIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||||
|
|
||||||
check_command()
|
check_command()
|
||||||
{
|
{
|
||||||
name=$1
|
name=$1
|
||||||
@@ -16,14 +18,15 @@ check_command lsusb
|
|||||||
check_command picotool
|
check_command picotool
|
||||||
|
|
||||||
DEVICEPATH=/dev/disk/by-label/RPI-RP2
|
DEVICEPATH=/dev/disk/by-label/RPI-RP2
|
||||||
IMAGEPATH=lib/micropython/ports/rp2/build-TONBERRY_RPI_PICO_W/firmware.uf2
|
IMAGEPATH=${TOPDIR}/build
|
||||||
|
REVISION=Rev1
|
||||||
|
|
||||||
flash_via_mountpoint()
|
flash_via_mountpoint()
|
||||||
{
|
{
|
||||||
while [ ! -e "$DEVICEPATH" ] ; do sleep 1; echo 'Waiting for RP2...'; done
|
while [ ! -e "$DEVICEPATH" ] ; do sleep 1; echo 'Waiting for RP2...'; done
|
||||||
|
|
||||||
udisksctl mount -b "$DEVICEPATH"
|
udisksctl mount -b "$DEVICEPATH"
|
||||||
cp "$IMAGEPATH" "$(findmnt "$DEVICEPATH" -n -o TARGET)"
|
cp "$IMAGEFILE" "$(findmnt "$DEVICEPATH" -n -o TARGET)"
|
||||||
}
|
}
|
||||||
|
|
||||||
PID="2e8a"
|
PID="2e8a"
|
||||||
@@ -40,7 +43,7 @@ flash_via_picotool()
|
|||||||
local device="${bus_device[1]//[!0-9]/}"
|
local device="${bus_device[1]//[!0-9]/}"
|
||||||
echo "Found RP2 with serial $serial on Bus $bus Device $device"
|
echo "Found RP2 with serial $serial on Bus $bus Device $device"
|
||||||
|
|
||||||
picotool load --bus "$bus" --address "$device" "$IMAGEPATH"
|
picotool load --update --bus "$bus" --address "$device" "$IMAGEFILE"
|
||||||
}
|
}
|
||||||
|
|
||||||
FLASH_VIA_MOUNTPOINT=0
|
FLASH_VIA_MOUNTPOINT=0
|
||||||
@@ -52,11 +55,12 @@ usage()
|
|||||||
echo
|
echo
|
||||||
echo " -m, --via-mountpoint Mount first found RP2 and flash image by"
|
echo " -m, --via-mountpoint Mount first found RP2 and flash image by"
|
||||||
echo " copying to mountpoint."
|
echo " copying to mountpoint."
|
||||||
|
echo " -r, --revision <rev> Hardware revision to flash. Default is Rev1"
|
||||||
echo " -h, --help Print this text and exit."
|
echo " -h, --help Print this text and exit."
|
||||||
exit 2
|
exit 2
|
||||||
}
|
}
|
||||||
|
|
||||||
PARSED_ARGUMENTS=$(getopt -a -n "$0" -o mh --long via-mountpoint,help -- "$@")
|
PARSED_ARGUMENTS=$(getopt -a -n "$0" -o mhr: --long via-mountpoint,revision:,help -- "$@")
|
||||||
# shellcheck disable=SC2181
|
# shellcheck disable=SC2181
|
||||||
# Indirect getopt return value checking is okay here
|
# Indirect getopt return value checking is okay here
|
||||||
if [ "$?" != "0" ]; then
|
if [ "$?" != "0" ]; then
|
||||||
@@ -68,6 +72,7 @@ while :
|
|||||||
do
|
do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
-m | --via-mountpoint) FLASH_VIA_MOUNTPOINT=1 ; shift ;;
|
-m | --via-mountpoint) FLASH_VIA_MOUNTPOINT=1 ; shift ;;
|
||||||
|
-r | --revision) REVISION=$2 ; shift 2 ;;
|
||||||
-h | --help) usage ;;
|
-h | --help) usage ;;
|
||||||
--) shift; break ;;
|
--) shift; break ;;
|
||||||
*) echo "Unexpected option: $1"
|
*) echo "Unexpected option: $1"
|
||||||
@@ -80,6 +85,8 @@ if [ $# -gt 0 ]; then
|
|||||||
usage
|
usage
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
IMAGEFILE="$IMAGEPATH"/firmware-$REVISION.uf2
|
||||||
|
|
||||||
if [ "$FLASH_VIA_MOUNTPOINT" -eq 0 ]; then
|
if [ "$FLASH_VIA_MOUNTPOINT" -eq 0 ]; then
|
||||||
flash_via_picotool
|
flash_via_picotool
|
||||||
else
|
else
|
||||||
|
|||||||
249
software/frontend/index.html
Normal file
249
software/frontend/index.html
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Device Admin</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 700px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
nav button {
|
||||||
|
margin: 5px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Screens */
|
||||||
|
.screen {
|
||||||
|
display: none;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.screen.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Config editor UI */
|
||||||
|
label {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 12px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
.nested {
|
||||||
|
margin-left: 20px;
|
||||||
|
border-left: 2px solid #ddd;
|
||||||
|
padding-left: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>Device Admin</h1>
|
||||||
|
|
||||||
|
<nav>
|
||||||
|
<button onclick="showScreen('menu')">🏠 Main Menu</button>
|
||||||
|
<button onclick="showScreen('config')">⚙️ Config Editor</button>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- MAIN MENU -->
|
||||||
|
<div id="screen-menu" class="screen active">
|
||||||
|
<h2>Main Menu</h2>
|
||||||
|
<p>Select a tool:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><button onclick="showScreen('config')">Open Config Editor</button></li>
|
||||||
|
<!-- More screens can be added later -->
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CONFIG EDITOR SCREEN -->
|
||||||
|
<div id="screen-config" class="screen">
|
||||||
|
<h2>Configuration Editor</h2>
|
||||||
|
<div id="config-container">Loading…</div>
|
||||||
|
<button id="save-btn" disabled>Save</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* -----------------------------
|
||||||
|
Screen switching
|
||||||
|
----------------------------- */
|
||||||
|
function showScreen(name) {
|
||||||
|
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
||||||
|
document.getElementById('screen-' + name).classList.add('active');
|
||||||
|
|
||||||
|
if (name === "config") {
|
||||||
|
loadConfig(); // refresh most up-to-date config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* -----------------------------
|
||||||
|
CONFIG EDITOR LOGIC
|
||||||
|
----------------------------- */
|
||||||
|
async function loadConfig() {
|
||||||
|
const container = document.getElementById('config-container');
|
||||||
|
container.innerHTML = "Loading…";
|
||||||
|
document.getElementById('save-btn').disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/v1/config');
|
||||||
|
const config = await res.json();
|
||||||
|
renderConfigForm(config);
|
||||||
|
} catch (err) {
|
||||||
|
container.innerHTML = "Failed to load config: " + err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderConfigForm(config) {
|
||||||
|
const container = document.getElementById('config-container');
|
||||||
|
container.innerHTML = "";
|
||||||
|
container.appendChild(renderObject(config, "root"));
|
||||||
|
document.getElementById('save-btn').disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config_names = {
|
||||||
|
'root.IDLE_TIMEOUT_SECS': 'Idle Timeout (seconds)',
|
||||||
|
'root.BUTTON_MAP': 'Button map',
|
||||||
|
'root.BUTTON_MAP.NEXT': 'Next track',
|
||||||
|
'root.BUTTON_MAP.PREV': 'Previous track',
|
||||||
|
'root.BUTTON_MAP.VOLUP': 'Volume up',
|
||||||
|
'root.BUTTON_MAP.VOLDOWN': 'Volume down',
|
||||||
|
'root.BUTTON_MAP.PLAY_PAUSE': 'Play/Pause',
|
||||||
|
'root.TAG_TIMEOUT_SECS': 'Tag removal timeout (seconds)',
|
||||||
|
'root.TAGMODE': 'Tag mode',
|
||||||
|
'root.LED_COUNT': 'Length of WS2182 (Neopixel) LED chain'
|
||||||
|
};
|
||||||
|
const config_input_override = {
|
||||||
|
'root.TAGMODE': {
|
||||||
|
'element': 'select',
|
||||||
|
'values': {
|
||||||
|
'tagremains': 'Play until tag is removed',
|
||||||
|
'tagstartstop': 'Present tag once to start, present again to stop playback'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'root.IDLE_TIMEOUT_SECS': {
|
||||||
|
'input-type': 'number'
|
||||||
|
},
|
||||||
|
'root.TAG_TIMEOUT_SECS': {
|
||||||
|
'input-type': 'number'
|
||||||
|
},
|
||||||
|
'root.LED_COUNT': {
|
||||||
|
'input-type': 'number'
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderObject(obj, path) {
|
||||||
|
const wrapper = document.createElement('div');
|
||||||
|
|
||||||
|
Object.entries(obj).forEach(([key, value]) => {
|
||||||
|
const currentPath = path + '.' + key;
|
||||||
|
const label = document.createElement('label');
|
||||||
|
if (currentPath in config_names) {
|
||||||
|
label.textContent = config_names[currentPath];
|
||||||
|
} else {
|
||||||
|
label.textContent = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value !== null && typeof value === 'object') {
|
||||||
|
wrapper.appendChild(label);
|
||||||
|
const nested = document.createElement('div');
|
||||||
|
nested.className = "nested";
|
||||||
|
nested.appendChild(renderObject(value, currentPath));
|
||||||
|
wrapper.appendChild(nested);
|
||||||
|
} else {
|
||||||
|
wrapper.appendChild(label);
|
||||||
|
if (currentPath in config_input_override && 'element' in config_input_override[currentPath]) {
|
||||||
|
const override = config_input_override[currentPath];
|
||||||
|
if (override['element'] === 'select') {
|
||||||
|
const input = document.createElement('select');
|
||||||
|
input.dataset.path = currentPath;
|
||||||
|
|
||||||
|
for (const val in override.values) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = val;
|
||||||
|
option.textContent = override.values[val];
|
||||||
|
if (val === value) {
|
||||||
|
option.selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.appendChild(option);
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapper.appendChild(input);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
if (currentPath in config_input_override && 'input-type' in config_input_override[currentPath]) {
|
||||||
|
input.type = config_input_override[currentPath]['input-type'];
|
||||||
|
}
|
||||||
|
input.value = value === null ? "" : value;
|
||||||
|
input.dataset.path = currentPath;
|
||||||
|
|
||||||
|
wrapper.appendChild(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeConfig(rootObj) {
|
||||||
|
const inputs = document.querySelectorAll("input[data-path], select[data-path]");
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
const path = input.dataset.path.split('.').slice(1); // remove "root"
|
||||||
|
let current = rootObj;
|
||||||
|
|
||||||
|
for (let i = 0; i < path.length - 1; i++) {
|
||||||
|
current = current[path[i]];
|
||||||
|
}
|
||||||
|
|
||||||
|
let val = input.value.trim();
|
||||||
|
if (val === "") val = null;
|
||||||
|
else if (!isNaN(val)) val = Number(val);
|
||||||
|
|
||||||
|
current[path[path.length - 1]] = val;
|
||||||
|
});
|
||||||
|
|
||||||
|
return rootObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('save-btn').addEventListener('click', async () => {
|
||||||
|
const res = await fetch('/api/v1/config');
|
||||||
|
const original = await res.json();
|
||||||
|
const updated = serializeConfig(original);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const saveRes = await fetch('/api/v1/config', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(updated, null, 2)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!saveRes.ok) {
|
||||||
|
alert("Failed to save config: " + await saveRes.text());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
alert("Configuration saved successfully!");
|
||||||
|
} catch (err) {
|
||||||
|
alert("Error saving configuration: " + err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load main menu by default
|
||||||
|
showScreen('menu');
|
||||||
|
</script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Submodule software/lib/micropython updated: e4422b860e...4ecb4099cf
@@ -40,7 +40,7 @@ void __time_critical_func(core1_main)(void)
|
|||||||
{
|
{
|
||||||
uint32_t ret = 0;
|
uint32_t ret = 0;
|
||||||
bool running = true, playing = false;
|
bool running = true, playing = false;
|
||||||
if (!i2s_init(shared_context.out_pin, shared_context.sideset_base)) {
|
if (!i2s_init(shared_context.out_pin, shared_context.sideset_base, shared_context.sideset_dclk_first)) {
|
||||||
ret = MP_EIO;
|
ret = MP_EIO;
|
||||||
goto out;
|
goto out;
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,7 @@ struct audiocore_shared_context {
|
|||||||
|
|
||||||
// Set by module.c before core1 is launched and then never changed, can be read without lock
|
// Set by module.c before core1 is launched and then never changed, can be read without lock
|
||||||
int out_pin, sideset_base, samplerate;
|
int out_pin, sideset_base, samplerate;
|
||||||
|
bool sideset_dclk_first;
|
||||||
|
|
||||||
// Must hold lock. The indices 0..MP3_BUFFER_PREAREA-1 may only be read and written on core1 (no
|
// Must hold lock. The indices 0..MP3_BUFFER_PREAREA-1 may only be read and written on core1 (no
|
||||||
// lock needed) The buffer is aligned to, and MP3_BUFFER_PREAREA is a multiple of, the machine
|
// lock needed) The buffer is aligned to, and MP3_BUFFER_PREAREA is a multiple of, the machine
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
import _audiocore
|
import _audiocore
|
||||||
from asyncio import ThreadSafeFlag
|
from asyncio import ThreadSafeFlag
|
||||||
|
from utils import get_pin_index
|
||||||
|
|
||||||
|
|
||||||
class Audiocore:
|
class Audiocore:
|
||||||
def __init__(self, pin, sideset):
|
def __init__(self, din, dclk, lrclk):
|
||||||
|
# PIO requires sideset pins to be adjacent
|
||||||
|
assert get_pin_index(lrclk) == get_pin_index(dclk)+1 or get_pin_index(lrclk) == get_pin_index(dclk)-1
|
||||||
self.notify = ThreadSafeFlag()
|
self.notify = ThreadSafeFlag()
|
||||||
self._audiocore = _audiocore.Audiocore(pin, sideset, self._interrupt)
|
self._audiocore = _audiocore.Audiocore(din, dclk, lrclk, self._interrupt)
|
||||||
|
|
||||||
def __del__(self):
|
def deinit(self):
|
||||||
self._audiocore.deinit()
|
self._audiocore.deinit()
|
||||||
|
|
||||||
def _interrupt(self, _):
|
def _interrupt(self, _):
|
||||||
@@ -35,3 +41,17 @@ class Audiocore:
|
|||||||
if pos >= len(buffer):
|
if pos >= len(buffer):
|
||||||
return (pos, buf_space, underruns)
|
return (pos, buf_space, underruns)
|
||||||
await self.notify.wait()
|
await self.notify.wait()
|
||||||
|
|
||||||
|
|
||||||
|
class AudioContext:
|
||||||
|
def __init__(self, din, dclk, lrclk):
|
||||||
|
self.din = din
|
||||||
|
self.dclk = dclk
|
||||||
|
self.lrclk = lrclk
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self._audiocore = Audiocore(self.din, self.dclk, self.lrclk)
|
||||||
|
return self._audiocore
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
self._audiocore.deinit()
|
||||||
@@ -91,19 +91,21 @@ void i2s_stop(void)
|
|||||||
{
|
{
|
||||||
if (!i2s_context.playback_active)
|
if (!i2s_context.playback_active)
|
||||||
return;
|
return;
|
||||||
bool have_data = false;
|
while (true) {
|
||||||
do {
|
|
||||||
const long flags = save_and_disable_interrupts();
|
const long flags = save_and_disable_interrupts();
|
||||||
const int next_buf = (i2s_context.cur_playing + 1) % AUDIO_BUFS;
|
const int next_buf = (i2s_context.cur_playing + 1) % AUDIO_BUFS;
|
||||||
have_data = i2s_context.has_data[next_buf];
|
const bool have_data = i2s_context.has_data[next_buf];
|
||||||
restore_interrupts(flags);
|
if (!have_data) {
|
||||||
if (have_data)
|
|
||||||
__wfi();
|
|
||||||
} while (have_data);
|
|
||||||
const long flags = save_and_disable_interrupts();
|
|
||||||
i2s_context.playback_active = false;
|
i2s_context.playback_active = false;
|
||||||
shared_context.underruns = 0;
|
shared_context.underruns = 0;
|
||||||
restore_interrupts(flags);
|
restore_interrupts(flags);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
__wfi();
|
||||||
|
restore_interrupts(flags);
|
||||||
|
__nop(); // Ensure at least two instructions between enable interrupts and subsequent disable
|
||||||
|
__nop();
|
||||||
|
}
|
||||||
// Workaround rp2040 E13
|
// Workaround rp2040 E13
|
||||||
dma_channel_set_irq1_enabled(i2s_context.dma_ch, false);
|
dma_channel_set_irq1_enabled(i2s_context.dma_ch, false);
|
||||||
dma_channel_abort(i2s_context.dma_ch);
|
dma_channel_abort(i2s_context.dma_ch);
|
||||||
@@ -113,17 +115,18 @@ void i2s_stop(void)
|
|||||||
pio_sm_clear_fifos(audiocore_pio, i2s_context.pio_sm);
|
pio_sm_clear_fifos(audiocore_pio, i2s_context.pio_sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool i2s_init(int out_pin, int sideset_base)
|
bool i2s_init(int out_pin, int sideset_base, bool dclk_first)
|
||||||
{
|
{
|
||||||
memset(i2s_context.dma_buf, 0, sizeof(i2s_context.dma_buf[0][0]) * AUDIO_BUFS * I2S_DMA_BUF_SIZE);
|
memset(i2s_context.dma_buf, 0, sizeof(i2s_context.dma_buf[0][0]) * AUDIO_BUFS * I2S_DMA_BUF_SIZE);
|
||||||
if (!pio_can_add_program(audiocore_pio, (const pio_program_t *)&i2s_max98357_program))
|
const pio_program_t *program = dclk_first ? &i2s_max98357_program : &i2s_max98357_lrclk_program;
|
||||||
|
if (!pio_can_add_program(audiocore_pio, program))
|
||||||
return false;
|
return false;
|
||||||
i2s_context.pio_sm = pio_claim_unused_sm(audiocore_pio, false);
|
i2s_context.pio_sm = pio_claim_unused_sm(audiocore_pio, false);
|
||||||
i2s_context.out_pin = out_pin;
|
i2s_context.out_pin = out_pin;
|
||||||
i2s_context.sideset_base = sideset_base;
|
i2s_context.sideset_base = sideset_base;
|
||||||
if (i2s_context.pio_sm == -1)
|
if (i2s_context.pio_sm == -1)
|
||||||
return false;
|
return false;
|
||||||
i2s_context.pio_program_offset = pio_add_program(audiocore_pio, (const pio_program_t *)&i2s_max98357_program);
|
i2s_context.pio_program_offset = pio_add_program(audiocore_pio, program);
|
||||||
|
|
||||||
i2s_context.dma_ch = dma_claim_unused_channel(false);
|
i2s_context.dma_ch = dma_claim_unused_channel(false);
|
||||||
if (i2s_context.dma_ch == -1)
|
if (i2s_context.dma_ch == -1)
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
#define I2S_DMA_BUF_SIZE (1152)
|
#define I2S_DMA_BUF_SIZE (1152)
|
||||||
|
|
||||||
bool i2s_init(int out_pin, int sideset_base);
|
bool i2s_init(int out_pin, int sideset_base, bool dclk_first);
|
||||||
void i2s_deinit(void);
|
void i2s_deinit(void);
|
||||||
|
|
||||||
void i2s_play(int samplerate);
|
void i2s_play(int samplerate);
|
||||||
91
software/modules/audiocore/i2s_max98357.pio
Normal file
91
software/modules/audiocore/i2s_max98357.pio
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
.program i2s_max98357
|
||||||
|
.side_set 2
|
||||||
|
|
||||||
|
.lang_opt python sideset_init = pico.PIO.OUT_LOW
|
||||||
|
.lang_opt python out_init = pico.PIO.OUT_LOW
|
||||||
|
.lang_opt python out_shiftdir = pcio.PIO.SHIFT_LEFT
|
||||||
|
.lang_opt python autopull = True
|
||||||
|
|
||||||
|
// data - DOUT
|
||||||
|
// sideset - 2-LRCLK, 1-BCLK
|
||||||
|
|
||||||
|
set x,15 side 0
|
||||||
|
nop side 1
|
||||||
|
startup_loop:
|
||||||
|
nop side 2
|
||||||
|
jmp x-- startup_loop side 3
|
||||||
|
nop side 0
|
||||||
|
set x, 14 side 1
|
||||||
|
|
||||||
|
left_loop:
|
||||||
|
.wrap_target
|
||||||
|
out pins, 1 side 0
|
||||||
|
jmp x-- left_loop side 1
|
||||||
|
out pins, 1 side 2
|
||||||
|
set x, 14 side 3
|
||||||
|
right_loop:
|
||||||
|
out pins, 1 side 2
|
||||||
|
jmp x-- right_loop side 3
|
||||||
|
out pins, 1 side 0
|
||||||
|
set x, 14 side 1
|
||||||
|
.wrap
|
||||||
|
|
||||||
|
.program i2s_max98357_lrclk
|
||||||
|
.side_set 2
|
||||||
|
|
||||||
|
.lang_opt python sideset_init = pico.PIO.OUT_LOW
|
||||||
|
.lang_opt python out_init = pico.PIO.OUT_LOW
|
||||||
|
.lang_opt python out_shiftdir = pcio.PIO.SHIFT_LEFT
|
||||||
|
.lang_opt python autopull = True
|
||||||
|
|
||||||
|
// data - DOUT
|
||||||
|
// sideset - 2-BCLK, 1-LRCLK
|
||||||
|
|
||||||
|
set x,15 side 0
|
||||||
|
nop side 2
|
||||||
|
startup_loop:
|
||||||
|
nop side 1
|
||||||
|
jmp x-- startup_loop side 3
|
||||||
|
nop side 0
|
||||||
|
set x, 14 side 2
|
||||||
|
|
||||||
|
left_loop:
|
||||||
|
.wrap_target
|
||||||
|
out pins, 1 side 0
|
||||||
|
jmp x-- left_loop side 2
|
||||||
|
out pins, 1 side 1
|
||||||
|
set x, 14 side 3
|
||||||
|
right_loop:
|
||||||
|
out pins, 1 side 1
|
||||||
|
jmp x-- right_loop side 3
|
||||||
|
out pins, 1 side 0
|
||||||
|
set x, 14 side 2
|
||||||
|
.wrap
|
||||||
|
|
||||||
|
% c-sdk {
|
||||||
|
#include "hardware/clocks.h"
|
||||||
|
|
||||||
|
static inline void i2s_max98357_program_init(PIO pio, uint sm, uint offset, uint pin, uint sideset, uint samplerate) {
|
||||||
|
pio_gpio_init(pio, pin);
|
||||||
|
pio_gpio_init(pio, sideset);
|
||||||
|
pio_gpio_init(pio, sideset+1);
|
||||||
|
pio_sm_set_consecutive_pindirs(pio, sm, pin, 1, true);
|
||||||
|
pio_sm_set_consecutive_pindirs(pio, sm, sideset, 2, true);
|
||||||
|
|
||||||
|
pio_sm_config c = i2s_max98357_program_get_default_config(offset);
|
||||||
|
sm_config_set_out_pins(&c, pin, 1);
|
||||||
|
sm_config_set_sideset_pins(&c, sideset);
|
||||||
|
sm_config_set_out_shift(&c, false, true, 32);
|
||||||
|
sm_config_set_fifo_join(&c, PIO_FIFO_JOIN_TX);
|
||||||
|
|
||||||
|
const unsigned i2s_freq = samplerate * 2 * 16 * 2;
|
||||||
|
const float div = clock_get_hz(clk_sys) / (float)i2s_freq;
|
||||||
|
sm_config_set_clkdiv(&c, div);
|
||||||
|
|
||||||
|
pio_sm_init(pio, sm, offset, &c);
|
||||||
|
//pio_sm_set_enabled(pio, sm, true);
|
||||||
|
}
|
||||||
|
%}
|
||||||
@@ -50,10 +50,14 @@ static uint32_t get_fifo_read_value_blocking(struct audiocore_obj *obj)
|
|||||||
const long flags = save_and_disable_interrupts();
|
const long flags = save_and_disable_interrupts();
|
||||||
const uint32_t value = obj->fifo_read_value;
|
const uint32_t value = obj->fifo_read_value;
|
||||||
obj->fifo_read_value = 0;
|
obj->fifo_read_value = 0;
|
||||||
|
if (value & AUDIOCORE_FIFO_DATA_FLAG) {
|
||||||
restore_interrupts(flags);
|
restore_interrupts(flags);
|
||||||
if (value & AUDIOCORE_FIFO_DATA_FLAG)
|
|
||||||
return value & ~AUDIOCORE_FIFO_DATA_FLAG;
|
return value & ~AUDIOCORE_FIFO_DATA_FLAG;
|
||||||
|
}
|
||||||
__wfi();
|
__wfi();
|
||||||
|
restore_interrupts(flags);
|
||||||
|
__nop(); // Ensure at least two instructions between enable interrupts and subsequent disable
|
||||||
|
__nop();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +94,9 @@ static mp_obj_t audiocore_put(mp_obj_t self_in, mp_obj_t buffer)
|
|||||||
(void)self;
|
(void)self;
|
||||||
mp_buffer_info_t bufinfo;
|
mp_buffer_info_t bufinfo;
|
||||||
if (!mp_get_buffer(buffer, &bufinfo, MP_BUFFER_READ))
|
if (!mp_get_buffer(buffer, &bufinfo, MP_BUFFER_READ))
|
||||||
mp_raise_ValueError("not a read buffer");
|
mp_raise_ValueError(MP_ERROR_TEXT("not a read buffer"));
|
||||||
if (bufinfo.typecode != 'b' && bufinfo.typecode != 'B')
|
if (bufinfo.typecode != 'b' && bufinfo.typecode != 'B')
|
||||||
mp_raise_ValueError("unsupported buffer type");
|
mp_raise_ValueError(MP_ERROR_TEXT("unsupported buffer type"));
|
||||||
unsigned to_copy = bufinfo.len;
|
unsigned to_copy = bufinfo.len;
|
||||||
|
|
||||||
const uint32_t flags = spin_lock_blocking(shared_context.lock);
|
const uint32_t flags = spin_lock_blocking(shared_context.lock);
|
||||||
@@ -144,7 +148,7 @@ static mp_obj_t audiocore_set_volume(mp_obj_t self_in, mp_obj_t volume_obj)
|
|||||||
struct audiocore_obj *self = MP_OBJ_TO_PTR(self_in);
|
struct audiocore_obj *self = MP_OBJ_TO_PTR(self_in);
|
||||||
const int volume = mp_obj_get_int(volume_obj);
|
const int volume = mp_obj_get_int(volume_obj);
|
||||||
if (volume < 0 || volume > 255)
|
if (volume < 0 || volume > 255)
|
||||||
mp_raise_ValueError("volume out of range");
|
mp_raise_ValueError(MP_ERROR_TEXT("volume out of range"));
|
||||||
multicore_fifo_push_blocking(AUDIOCORE_CMD_SET_VOLUME);
|
multicore_fifo_push_blocking(AUDIOCORE_CMD_SET_VOLUME);
|
||||||
multicore_fifo_push_blocking(AUDIOCORE_MAX_VOLUME * volume / 255);
|
multicore_fifo_push_blocking(AUDIOCORE_MAX_VOLUME * volume / 255);
|
||||||
wake_core1();
|
wake_core1();
|
||||||
@@ -166,10 +170,11 @@ static MP_DEFINE_CONST_FUN_OBJ_2(audiocore_set_volume_obj, audiocore_set_volume)
|
|||||||
*/
|
*/
|
||||||
static void audiocore_init(struct audiocore_obj *obj, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args)
|
static void audiocore_init(struct audiocore_obj *obj, size_t n_args, const mp_obj_t *pos_args, mp_map_t *kw_args)
|
||||||
{
|
{
|
||||||
enum { ARG_pin, ARG_sideset, ARG_handler };
|
enum { ARG_pin, ARG_dclk, ARG_lrclk, ARG_handler };
|
||||||
static const mp_arg_t allowed_args[] = {
|
static const mp_arg_t allowed_args[] = {
|
||||||
{MP_QSTR_pin, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
|
{MP_QSTR_pin, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
|
||||||
{MP_QSTR_sideset, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
|
{MP_QSTR_dclk, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
|
||||||
|
{MP_QSTR_lrclk, MP_ARG_REQUIRED | MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
|
||||||
{MP_QSTR_handler, MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
|
{MP_QSTR_handler, MP_ARG_OBJ, {.u_obj = MP_OBJ_NULL}},
|
||||||
};
|
};
|
||||||
if (initialized)
|
if (initialized)
|
||||||
@@ -184,7 +189,8 @@ static void audiocore_init(struct audiocore_obj *obj, size_t n_args, const mp_ob
|
|||||||
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
|
mp_arg_val_t args[MP_ARRAY_SIZE(allowed_args)];
|
||||||
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
|
mp_arg_parse_all(n_args, pos_args, kw_args, MP_ARRAY_SIZE(allowed_args), allowed_args, args);
|
||||||
const mp_hal_pin_obj_t pin = mp_hal_get_pin_obj(args[ARG_pin].u_obj);
|
const mp_hal_pin_obj_t pin = mp_hal_get_pin_obj(args[ARG_pin].u_obj);
|
||||||
const mp_hal_pin_obj_t sideset_pin = mp_hal_get_pin_obj(args[ARG_sideset].u_obj);
|
const mp_hal_pin_obj_t dclk_pin = mp_hal_get_pin_obj(args[ARG_dclk].u_obj);
|
||||||
|
const mp_hal_pin_obj_t lrclk_pin = mp_hal_get_pin_obj(args[ARG_lrclk].u_obj);
|
||||||
if (args[ARG_handler].u_obj != MP_OBJ_NULL) {
|
if (args[ARG_handler].u_obj != MP_OBJ_NULL) {
|
||||||
obj->irq_obj = mp_irq_new(&audiocore_irq_methods, MP_OBJ_FROM_PTR(obj));
|
obj->irq_obj = mp_irq_new(&audiocore_irq_methods, MP_OBJ_FROM_PTR(obj));
|
||||||
obj->irq_obj->handler = args[ARG_handler].u_obj;
|
obj->irq_obj->handler = args[ARG_handler].u_obj;
|
||||||
@@ -199,7 +205,14 @@ static void audiocore_init(struct audiocore_obj *obj, size_t n_args, const mp_ob
|
|||||||
memset(shared_context.mp3_buffer, 0, MP3_BUFFER_PREAREA + MP3_BUFFER_SIZE);
|
memset(shared_context.mp3_buffer, 0, MP3_BUFFER_PREAREA + MP3_BUFFER_SIZE);
|
||||||
multicore_reset_core1();
|
multicore_reset_core1();
|
||||||
shared_context.out_pin = pin;
|
shared_context.out_pin = pin;
|
||||||
shared_context.sideset_base = sideset_pin;
|
// PIO requires sideset pins to be adjacent, but we support both dclk first and lrclk first
|
||||||
|
if (lrclk_pin == dclk_pin + 1) {
|
||||||
|
shared_context.sideset_base = dclk_pin;
|
||||||
|
shared_context.sideset_dclk_first = true;
|
||||||
|
} else {
|
||||||
|
shared_context.sideset_base = lrclk_pin;
|
||||||
|
shared_context.sideset_dclk_first = false;
|
||||||
|
}
|
||||||
initialized = true;
|
initialized = true;
|
||||||
multicore_launch_core1(&core1_main);
|
multicore_launch_core1(&core1_main);
|
||||||
uint32_t result = get_fifo_read_value_blocking(obj);
|
uint32_t result = get_fifo_read_value_blocking(obj);
|
||||||
@@ -15,7 +15,7 @@ static unsigned multicore_fifo_push_last;
|
|||||||
|
|
||||||
static unsigned (*multicore_fifo_pop_blocking_cb)(void);
|
static unsigned (*multicore_fifo_pop_blocking_cb)(void);
|
||||||
|
|
||||||
bool i2s_init(int out_pin, int sideset_base)
|
bool i2s_init(int out_pin, int sideset_base, bool dclk_first)
|
||||||
{
|
{
|
||||||
TEST_ASSERT_FALSE(i2s_initialized);
|
TEST_ASSERT_FALSE(i2s_initialized);
|
||||||
if (i2s_init_return)
|
if (i2s_init_return)
|
||||||
@@ -66,9 +66,9 @@ static mp_obj_t sdcard_readblocks(mp_obj_t self_obj, mp_obj_t block_obj, mp_obj_
|
|||||||
const int start_block = mp_obj_get_int(block_obj);
|
const int start_block = mp_obj_get_int(block_obj);
|
||||||
mp_buffer_info_t bufinfo;
|
mp_buffer_info_t bufinfo;
|
||||||
if (!mp_get_buffer(buf_obj, &bufinfo, MP_BUFFER_WRITE))
|
if (!mp_get_buffer(buf_obj, &bufinfo, MP_BUFFER_WRITE))
|
||||||
mp_raise_ValueError("Not a write buffer");
|
mp_raise_ValueError(MP_ERROR_TEXT("Not a write buffer"));
|
||||||
if (bufinfo.len % SD_SECTOR_SIZE != 0)
|
if (bufinfo.len % SD_SECTOR_SIZE != 0)
|
||||||
mp_raise_ValueError("Buffer length is invalid");
|
mp_raise_ValueError(MP_ERROR_TEXT("Buffer length is invalid"));
|
||||||
const int nblocks = bufinfo.len / SD_SECTOR_SIZE;
|
const int nblocks = bufinfo.len / SD_SECTOR_SIZE;
|
||||||
for (int block = 0; block < nblocks; block++) {
|
for (int block = 0; block < nblocks; block++) {
|
||||||
// TODO: Implement CMD18 read multiple blocks
|
// TODO: Implement CMD18 read multiple blocks
|
||||||
@@ -79,6 +79,25 @@ static mp_obj_t sdcard_readblocks(mp_obj_t self_obj, mp_obj_t block_obj, mp_obj_
|
|||||||
}
|
}
|
||||||
static MP_DEFINE_CONST_FUN_OBJ_3(sdcard_readblocks_obj, sdcard_readblocks);
|
static MP_DEFINE_CONST_FUN_OBJ_3(sdcard_readblocks_obj, sdcard_readblocks);
|
||||||
|
|
||||||
|
static mp_obj_t sdcard_writeblocks(mp_obj_t self_obj, mp_obj_t block_obj, mp_obj_t buf_obj)
|
||||||
|
{
|
||||||
|
struct sdcard_obj *self = MP_OBJ_TO_PTR(self_obj);
|
||||||
|
const int start_block = mp_obj_get_int(block_obj);
|
||||||
|
mp_buffer_info_t bufinfo;
|
||||||
|
if (!mp_get_buffer(buf_obj, &bufinfo, MP_BUFFER_READ))
|
||||||
|
mp_raise_ValueError(MP_ERROR_TEXT("Not a read buffer"));
|
||||||
|
if (bufinfo.len % SD_SECTOR_SIZE != 0)
|
||||||
|
mp_raise_ValueError(MP_ERROR_TEXT("Buffer length is invalid"));
|
||||||
|
const int nblocks = bufinfo.len / SD_SECTOR_SIZE;
|
||||||
|
for (int block = 0; block < nblocks; block++) {
|
||||||
|
// TODO: Implement CMD25 write multiple blocks
|
||||||
|
if (!sd_writeblock(&self->sd_context, start_block + block, bufinfo.buf + block * SD_SECTOR_SIZE))
|
||||||
|
mp_raise_OSError(MP_EIO);
|
||||||
|
}
|
||||||
|
return mp_const_none;
|
||||||
|
}
|
||||||
|
static MP_DEFINE_CONST_FUN_OBJ_3(sdcard_writeblocks_obj, sdcard_writeblocks);
|
||||||
|
|
||||||
static mp_obj_t sdcard_ioctl(mp_obj_t self_obj, mp_obj_t op_obj, mp_obj_t arg_obj)
|
static mp_obj_t sdcard_ioctl(mp_obj_t self_obj, mp_obj_t op_obj, mp_obj_t arg_obj)
|
||||||
{
|
{
|
||||||
struct sdcard_obj *self = MP_OBJ_TO_PTR(self_obj);
|
struct sdcard_obj *self = MP_OBJ_TO_PTR(self_obj);
|
||||||
@@ -96,8 +115,10 @@ static MP_DEFINE_CONST_FUN_OBJ_3(sdcard_ioctl_obj, sdcard_ioctl);
|
|||||||
|
|
||||||
static const mp_rom_map_elem_t sdcard_locals_dict_table[] = {
|
static const mp_rom_map_elem_t sdcard_locals_dict_table[] = {
|
||||||
{MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&sdcard_deinit_obj)},
|
{MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&sdcard_deinit_obj)},
|
||||||
|
{MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&sdcard_deinit_obj)},
|
||||||
{MP_ROM_QSTR(MP_QSTR_ioctl), MP_ROM_PTR(&sdcard_ioctl_obj)},
|
{MP_ROM_QSTR(MP_QSTR_ioctl), MP_ROM_PTR(&sdcard_ioctl_obj)},
|
||||||
{MP_ROM_QSTR(MP_QSTR_readblocks), MP_ROM_PTR(&sdcard_readblocks_obj)},
|
{MP_ROM_QSTR(MP_QSTR_readblocks), MP_ROM_PTR(&sdcard_readblocks_obj)},
|
||||||
|
{MP_ROM_QSTR(MP_QSTR_writeblocks), MP_ROM_PTR(&sdcard_writeblocks_obj)},
|
||||||
};
|
};
|
||||||
static MP_DEFINE_CONST_DICT(sdcard_locals_dict, sdcard_locals_dict_table);
|
static MP_DEFINE_CONST_DICT(sdcard_locals_dict, sdcard_locals_dict_table);
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ static bool sd_acmd(const uint8_t cmd, const uint32_t arg, unsigned resplen, uin
|
|||||||
static bool sd_early_init(void)
|
static bool sd_early_init(void)
|
||||||
{
|
{
|
||||||
uint8_t buf;
|
uint8_t buf;
|
||||||
for (int i = 0; i < 5; ++i) {
|
for (int i = 0; i < 500; ++i) {
|
||||||
if (sd_cmd(0, 0, 1, &buf)) {
|
if (sd_cmd(0, 0, 1, &buf)) {
|
||||||
#ifdef SD_DEBUG
|
#ifdef SD_DEBUG
|
||||||
printf("CMD0 resp %02hhx\n", buf);
|
printf("CMD0 resp %02hhx\n", buf);
|
||||||
@@ -63,7 +63,7 @@ static bool sd_send_op_cond(void)
|
|||||||
{
|
{
|
||||||
uint8_t buf;
|
uint8_t buf;
|
||||||
bool use_acmd = true;
|
bool use_acmd = true;
|
||||||
for (int timeout = 0; timeout < 500; ++timeout) {
|
for (int timeout = 0; timeout < 50000; ++timeout) {
|
||||||
bool result = false;
|
bool result = false;
|
||||||
if (use_acmd)
|
if (use_acmd)
|
||||||
result = sd_acmd(41, 0x40000000, 1, &buf);
|
result = sd_acmd(41, 0x40000000, 1, &buf);
|
||||||
@@ -74,6 +74,7 @@ static bool sd_send_op_cond(void)
|
|||||||
#ifdef SD_DEBUG
|
#ifdef SD_DEBUG
|
||||||
printf("sd_init: card does not understand ACMD41, try CMD1...\n");
|
printf("sd_init: card does not understand ACMD41, try CMD1...\n");
|
||||||
#endif
|
#endif
|
||||||
|
use_acmd = false;
|
||||||
continue;
|
continue;
|
||||||
} else if (buf != 0x01) {
|
} else if (buf != 0x01) {
|
||||||
printf("sd_init: send_op_cond failed\n");
|
printf("sd_init: send_op_cond failed\n");
|
||||||
@@ -129,7 +130,10 @@ static void sd_dump_cid [[maybe_unused]] (void)
|
|||||||
static bool sd_read_csd(struct sd_context *sd_context)
|
static bool sd_read_csd(struct sd_context *sd_context)
|
||||||
{
|
{
|
||||||
uint8_t buf[16];
|
uint8_t buf[16];
|
||||||
if (sd_cmd_read(9, 0, 16, buf)) {
|
if (!sd_cmd_read(9, 0, 16, buf)) {
|
||||||
|
printf("Failed to read CSD\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
const uint8_t crc = sd_crc7(15, buf);
|
const uint8_t crc = sd_crc7(15, buf);
|
||||||
const uint8_t card_crc = buf[15] >> 1;
|
const uint8_t card_crc = buf[15] >> 1;
|
||||||
if (card_crc != crc) {
|
if (card_crc != crc) {
|
||||||
@@ -143,6 +147,7 @@ static bool sd_read_csd(struct sd_context *sd_context)
|
|||||||
unsigned blocksize [[maybe_unused]] = 0;
|
unsigned blocksize [[maybe_unused]] = 0;
|
||||||
unsigned blocks = 0;
|
unsigned blocks = 0;
|
||||||
unsigned version [[maybe_unused]] = 0;
|
unsigned version [[maybe_unused]] = 0;
|
||||||
|
unsigned max_speed [[maybe_unused]] = 0;
|
||||||
switch (csd_ver) {
|
switch (csd_ver) {
|
||||||
case 0: {
|
case 0: {
|
||||||
if (sd_context->sdhc_sdxc) {
|
if (sd_context->sdhc_sdxc) {
|
||||||
@@ -159,6 +164,7 @@ static bool sd_read_csd(struct sd_context *sd_context)
|
|||||||
const unsigned c_size = (buf[6] & 0x3) << 10 | (buf[7] << 2) | (buf[8] & 0xc0) >> 6;
|
const unsigned c_size = (buf[6] & 0x3) << 10 | (buf[7] << 2) | (buf[8] & 0xc0) >> 6;
|
||||||
blocks = (c_size + 1) * (1 << (c_size_mult + 2));
|
blocks = (c_size + 1) * (1 << (c_size_mult + 2));
|
||||||
version = 1;
|
version = 1;
|
||||||
|
max_speed = buf[3];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 1: {
|
case 1: {
|
||||||
@@ -166,6 +172,7 @@ static bool sd_read_csd(struct sd_context *sd_context)
|
|||||||
const unsigned c_size = (buf[7] & 0x3f) << 16 | buf[8] << 8 | buf[9];
|
const unsigned c_size = (buf[7] & 0x3f) << 16 | buf[8] << 8 | buf[9];
|
||||||
blocks = (c_size + 1) * 1024;
|
blocks = (c_size + 1) * 1024;
|
||||||
version = 2;
|
version = 2;
|
||||||
|
max_speed = buf[3];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 2: {
|
case 2: {
|
||||||
@@ -174,11 +181,11 @@ static bool sd_read_csd(struct sd_context *sd_context)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sd_context->blocks = blocks;
|
sd_context->blocks = blocks;
|
||||||
|
sd_context->blocksize = blocksize;
|
||||||
#ifdef SD_DEBUG
|
#ifdef SD_DEBUG
|
||||||
printf("CSD version %u.0, blocksize %u, blocks %u, capacity %llu MiB\n", version, blocksize, blocks,
|
printf("CSD version %u.0, blocksize %u, blocks %u, capacity %llu MiB, max speed %u\n", version, blocksize, blocks,
|
||||||
((uint64_t)blocksize * blocks) / (1024 * 1024));
|
((uint64_t)blocksize * blocks) / (1024 * 1024), max_speed);
|
||||||
#endif
|
#endif
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,40 +196,60 @@ bool sd_init(struct sd_context *sd_context, int mosi, int miso, int sck, int ss,
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!sd_early_init()) {
|
if (!sd_early_init()) {
|
||||||
return false;
|
goto out_spi;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sd_check_interface_condition()) {
|
if (!sd_check_interface_condition()) {
|
||||||
return false;
|
goto out_spi;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t ocr;
|
uint32_t ocr;
|
||||||
if (!sd_read_ocr(&ocr)) {
|
if (!sd_read_ocr(&ocr)) {
|
||||||
printf("sd_init: read OCR failed\n");
|
printf("sd_init: read OCR failed\n");
|
||||||
return false;
|
goto out_spi;
|
||||||
}
|
}
|
||||||
if ((ocr & 0x00380000) != 0x00380000) {
|
if ((ocr & 0x00380000) != 0x00380000) {
|
||||||
printf("sd_init: unsupported card voltage range\n");
|
printf("sd_init: unsupported card voltage range\n");
|
||||||
return false;
|
goto out_spi;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sd_send_op_cond())
|
if (!sd_send_op_cond())
|
||||||
return false;
|
goto out_spi;
|
||||||
|
|
||||||
sd_spi_set_bitrate(rate);
|
sd_spi_set_bitrate(rate);
|
||||||
|
|
||||||
if (!sd_read_ocr(&ocr)) {
|
if (!sd_read_ocr(&ocr)) {
|
||||||
printf("sd_init: read OCR failed\n");
|
printf("sd_init: read OCR failed\n");
|
||||||
return false;
|
goto out_spi;
|
||||||
}
|
}
|
||||||
if (!(ocr & (1 << 31))) {
|
if (!(ocr & (1 << 31))) {
|
||||||
printf("sd_init: card not powered up but !idle?\n");
|
printf("sd_init: card not powered up but !idle?\n");
|
||||||
return false;
|
goto out_spi;
|
||||||
}
|
}
|
||||||
sd_context->sdhc_sdxc = (ocr & (1 << 30));
|
sd_context->sdhc_sdxc = (ocr & (1 << 30));
|
||||||
|
|
||||||
if (!sd_read_csd(sd_context)) {
|
if (!sd_read_csd(sd_context)) {
|
||||||
return false;
|
goto out_spi;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sd_context->blocksize != SD_SECTOR_SIZE) {
|
||||||
|
if (sd_context->blocksize != 1024 && sd_context->blocksize != 2048) {
|
||||||
|
printf("sd_init: Unsupported block size %u\n", sd_context->blocksize);
|
||||||
|
goto out_spi;
|
||||||
|
}
|
||||||
|
// Attempt SET_BLOCKLEN command
|
||||||
|
uint8_t resp[1];
|
||||||
|
if (!sd_cmd(16, SD_SECTOR_SIZE, 1, resp)) {
|
||||||
|
printf("sd_init: SET_BLOCKLEN failed\n");
|
||||||
|
goto out_spi;
|
||||||
|
}
|
||||||
|
// Successfully set blocksize to SD_SECTOR_SIZE, adjust context
|
||||||
|
sd_context->blocks *= sd_context->blocksize / SD_SECTOR_SIZE;
|
||||||
|
#ifdef SD_DEBUG
|
||||||
|
printf("Adjusted blocksize from %u to 512, card now has %u blocks\n", sd_context->blocksize,
|
||||||
|
sd_context->blocks);
|
||||||
|
#endif
|
||||||
|
sd_context->blocksize = SD_SECTOR_SIZE;
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef SD_DEBUG
|
#ifdef SD_DEBUG
|
||||||
@@ -231,6 +258,10 @@ bool sd_init(struct sd_context *sd_context, int mosi, int miso, int sck, int ss,
|
|||||||
|
|
||||||
sd_context->initialized = true;
|
sd_context->initialized = true;
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
out_spi:
|
||||||
|
sd_spi_deinit();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sd_deinit(struct sd_context *sd_context)
|
bool sd_deinit(struct sd_context *sd_context)
|
||||||
@@ -246,14 +277,24 @@ bool sd_readblock(struct sd_context *sd_context, size_t sector_num, uint8_t buff
|
|||||||
if (!sd_context->initialized || sector_num >= sd_context->blocks)
|
if (!sd_context->initialized || sector_num >= sd_context->blocks)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return sd_cmd_read(17, sector_num, SD_SECTOR_SIZE, buffer);
|
uint32_t addr = sector_num;
|
||||||
|
if (!sd_context->sdhc_sdxc) {
|
||||||
|
// SDSC cards used byte addressing
|
||||||
|
addr *= SD_SECTOR_SIZE;
|
||||||
|
}
|
||||||
|
return sd_cmd_read(17, addr, SD_SECTOR_SIZE, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sd_readblock_start(struct sd_context *sd_context, size_t sector_num, uint8_t buffer[static SD_SECTOR_SIZE])
|
bool sd_readblock_start(struct sd_context *sd_context, size_t sector_num, uint8_t buffer[static SD_SECTOR_SIZE])
|
||||||
{
|
{
|
||||||
if (!sd_context->initialized || sector_num >= sd_context->blocks)
|
if (!sd_context->initialized || sector_num >= sd_context->blocks)
|
||||||
return false;
|
return false;
|
||||||
return sd_cmd_read_start(17, sector_num, SD_SECTOR_SIZE, buffer);
|
uint32_t addr = sector_num;
|
||||||
|
if (!sd_context->sdhc_sdxc) {
|
||||||
|
// SDSC cards used byte addressing
|
||||||
|
addr *= SD_SECTOR_SIZE;
|
||||||
|
}
|
||||||
|
return sd_cmd_read_start(17, addr, SD_SECTOR_SIZE, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sd_readblock_complete(struct sd_context *sd_context)
|
bool sd_readblock_complete(struct sd_context *sd_context)
|
||||||
@@ -265,3 +306,16 @@ bool sd_readblock_complete(struct sd_context *sd_context)
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool sd_readblock_is_complete(struct sd_context *sd_context) { return sd_cmd_read_is_complete(); }
|
bool sd_readblock_is_complete(struct sd_context *sd_context) { return sd_cmd_read_is_complete(); }
|
||||||
|
|
||||||
|
bool sd_writeblock(struct sd_context *sd_context, size_t sector_num, uint8_t buffer[const static SD_SECTOR_SIZE])
|
||||||
|
{
|
||||||
|
if (!sd_context->initialized || sector_num >= sd_context->blocks)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
uint32_t addr = sector_num;
|
||||||
|
if (!sd_context->sdhc_sdxc) {
|
||||||
|
// SDSC cards used byte addressing
|
||||||
|
addr *= SD_SECTOR_SIZE;
|
||||||
|
}
|
||||||
|
return sd_cmd_write(24, addr, SD_SECTOR_SIZE, buffer);
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
struct sd_context {
|
struct sd_context {
|
||||||
size_t blocks;
|
size_t blocks;
|
||||||
|
size_t blocksize;
|
||||||
bool initialized;
|
bool initialized;
|
||||||
bool old_card;
|
bool old_card;
|
||||||
bool sdhc_sdxc;
|
bool sdhc_sdxc;
|
||||||
@@ -21,3 +22,5 @@ bool sd_readblock(struct sd_context *context, size_t sector_num, uint8_t buffer[
|
|||||||
bool sd_readblock_start(struct sd_context *context, size_t sector_num, uint8_t buffer[static SD_SECTOR_SIZE]);
|
bool sd_readblock_start(struct sd_context *context, size_t sector_num, uint8_t buffer[static SD_SECTOR_SIZE]);
|
||||||
bool sd_readblock_complete(struct sd_context *context);
|
bool sd_readblock_complete(struct sd_context *context);
|
||||||
bool sd_readblock_is_complete(struct sd_context *context);
|
bool sd_readblock_is_complete(struct sd_context *context);
|
||||||
|
|
||||||
|
bool sd_writeblock(struct sd_context *context, size_t sector_num, uint8_t buffer[const static SD_SECTOR_SIZE]);
|
||||||
@@ -12,13 +12,15 @@
|
|||||||
#include <pico/time.h>
|
#include <pico/time.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
|
typedef enum { DMA_READ_TOKEN, DMA_READ, DMA_IDLE } sd_dma_state;
|
||||||
|
|
||||||
struct sd_dma_context {
|
struct sd_dma_context {
|
||||||
uint8_t *read_buf;
|
uint8_t *read_buf;
|
||||||
size_t len;
|
size_t len;
|
||||||
uint8_t crc_buf[2];
|
uint8_t crc_buf[2];
|
||||||
uint8_t read_token_buf;
|
uint8_t read_token_buf;
|
||||||
uint8_t wrdata;
|
uint8_t wrdata;
|
||||||
_Atomic enum { DMA_READ_TOKEN, DMA_READ, DMA_IDLE } state;
|
_Atomic sd_dma_state state;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct sd_spi_context {
|
struct sd_spi_context {
|
||||||
@@ -111,8 +113,18 @@ static void __time_critical_func(sd_spi_dma_isr)(void)
|
|||||||
|
|
||||||
void sd_spi_wait_complete(void)
|
void sd_spi_wait_complete(void)
|
||||||
{
|
{
|
||||||
while (sd_spi_context.sd_dma_context.state != DMA_IDLE)
|
while (true) {
|
||||||
|
const long flags = save_and_disable_interrupts();
|
||||||
|
const sd_dma_state state = sd_spi_context.sd_dma_context.state;
|
||||||
|
if (state == DMA_IDLE) {
|
||||||
|
restore_interrupts(flags);
|
||||||
|
return;
|
||||||
|
}
|
||||||
__wfi();
|
__wfi();
|
||||||
|
restore_interrupts(flags);
|
||||||
|
__nop(); // Ensure at least two instructions between enable interrupts and subsequent disable
|
||||||
|
__nop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sd_cmd_read_is_complete(void) { return sd_spi_context.sd_dma_context.state == DMA_IDLE; }
|
bool sd_cmd_read_is_complete(void) { return sd_spi_context.sd_dma_context.state == DMA_IDLE; }
|
||||||
@@ -208,7 +220,78 @@ bool sd_cmd_read_complete(void)
|
|||||||
sd_spi_wait_complete();
|
sd_spi_wait_complete();
|
||||||
gpio_put(sd_spi_context.ss, true);
|
gpio_put(sd_spi_context.ss, true);
|
||||||
sd_spi_read_blocking(0xff, &buf, 1);
|
sd_spi_read_blocking(0xff, &buf, 1);
|
||||||
return (sd_spi_context.sd_dma_context.read_token_buf == 0xfe);
|
if (sd_spi_context.sd_dma_context.read_token_buf != 0xfe) {
|
||||||
|
#ifdef SD_DEBUG
|
||||||
|
printf("read failed: invalid read token %02hhx\n", sd_spi_context.sd_dma_context.read_token_buf);
|
||||||
|
#endif
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#ifdef SD_READ_CRC_CHECK
|
||||||
|
const uint16_t expect_crc = sd_crc16(sd_spi_context.sd_dma_context.len, sd_spi_context.sd_dma_context.read_buf);
|
||||||
|
const uint16_t act_crc = sd_spi_context.sd_dma_context.crc_buf[0] << 8 | sd_spi_context.sd_dma_context.crc_buf[1];
|
||||||
|
if (act_crc != expect_crc) {
|
||||||
|
#ifdef SD_DEBUG
|
||||||
|
printf("read CRC fail: got %04hx, expected %04hx\n", act_crc, expect_crc);
|
||||||
|
#endif
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool sd_cmd_write(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[const static datalen])
|
||||||
|
{
|
||||||
|
uint8_t buf[2];
|
||||||
|
const uint16_t crc = sd_crc16(datalen, data);
|
||||||
|
sd_spi_cmd_send(cmd, arg);
|
||||||
|
// Read up to 8 garbage bytes (0xff), followed by R1 (MSB is zero)
|
||||||
|
bool got_r1 = false;
|
||||||
|
for (int timeout = 0; timeout < 8; ++timeout) {
|
||||||
|
sd_spi_read_blocking(0xff, buf, 1);
|
||||||
|
if (!(buf[0] & 0x80)) {
|
||||||
|
got_r1 = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!got_r1 || buf[0] != 0x00)
|
||||||
|
goto abort;
|
||||||
|
buf[0] = 0xfe;
|
||||||
|
sd_spi_write_blocking(buf, 1);
|
||||||
|
sd_spi_write_blocking(data, datalen);
|
||||||
|
buf[0] = crc >> 8;
|
||||||
|
buf[1] = crc;
|
||||||
|
sd_spi_write_blocking(buf, 2);
|
||||||
|
sd_spi_read_blocking(0xff, buf, 1);
|
||||||
|
if ((buf[0] & 0x1f) != 0x5) {
|
||||||
|
#ifdef SD_DEBUG
|
||||||
|
printf("Write fail: %2hhx\n", buf[0]);
|
||||||
|
#endif
|
||||||
|
goto abort;
|
||||||
|
}
|
||||||
|
|
||||||
|
int timeout = 0;
|
||||||
|
bool got_done = false;
|
||||||
|
for (timeout = 0; timeout < 131072; ++timeout) {
|
||||||
|
sd_spi_read_blocking(0xff, buf, 1);
|
||||||
|
if (buf[0] != 0x0) {
|
||||||
|
got_done = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#ifdef SD_DEBUG
|
||||||
|
printf("dbg write end: %d, %2hhx\n", timeout, buf[0]);
|
||||||
|
#endif
|
||||||
|
if (!got_done)
|
||||||
|
goto abort;
|
||||||
|
|
||||||
|
gpio_put(sd_spi_context.ss, true);
|
||||||
|
sd_spi_read_blocking(0xff, buf, 1);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
abort:
|
||||||
|
gpio_put(sd_spi_context.ss, true);
|
||||||
|
sd_spi_read_blocking(0xff, buf, 1);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool sd_spi_init(int mosi, int miso, int sck, int ss)
|
bool sd_spi_init(int mosi, int miso, int sck, int ss)
|
||||||
@@ -243,6 +326,7 @@ bool sd_spi_init(int mosi, int miso, int sck, int ss)
|
|||||||
channel_config_set_transfer_data_size(&sd_spi_context.spi_dma_rd_cfg, DMA_SIZE_8);
|
channel_config_set_transfer_data_size(&sd_spi_context.spi_dma_rd_cfg, DMA_SIZE_8);
|
||||||
sd_spi_context.spi_dma_rd_crc_cfg = dma_channel_get_default_config(sd_spi_context.spi_dma_rd_crc);
|
sd_spi_context.spi_dma_rd_crc_cfg = dma_channel_get_default_config(sd_spi_context.spi_dma_rd_crc);
|
||||||
channel_config_set_read_increment(&sd_spi_context.spi_dma_rd_crc_cfg, false);
|
channel_config_set_read_increment(&sd_spi_context.spi_dma_rd_crc_cfg, false);
|
||||||
|
channel_config_set_write_increment(&sd_spi_context.spi_dma_rd_crc_cfg, true);
|
||||||
channel_config_set_dreq(&sd_spi_context.spi_dma_rd_crc_cfg, pio_get_dreq(SD_PIO, sd_spi_context.spi_sm, false));
|
channel_config_set_dreq(&sd_spi_context.spi_dma_rd_crc_cfg, pio_get_dreq(SD_PIO, sd_spi_context.spi_sm, false));
|
||||||
channel_config_set_transfer_data_size(&sd_spi_context.spi_dma_rd_crc_cfg, DMA_SIZE_8);
|
channel_config_set_transfer_data_size(&sd_spi_context.spi_dma_rd_crc_cfg, DMA_SIZE_8);
|
||||||
sd_spi_context.spi_dma_wr_cfg = dma_channel_get_default_config(sd_spi_context.spi_dma_wr);
|
sd_spi_context.spi_dma_wr_cfg = dma_channel_get_default_config(sd_spi_context.spi_dma_wr);
|
||||||
@@ -25,3 +25,5 @@ void sd_spi_dbg_clk(const int div, const int frac);
|
|||||||
bool sd_cmd_read_start(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[static datalen]);
|
bool sd_cmd_read_start(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[static datalen]);
|
||||||
bool sd_cmd_read_complete(void);
|
bool sd_cmd_read_complete(void);
|
||||||
bool sd_cmd_read_is_complete(void);
|
bool sd_cmd_read_is_complete(void);
|
||||||
|
|
||||||
|
bool sd_cmd_write(uint8_t cmd, uint32_t arg, unsigned datalen, uint8_t data[const static datalen]);
|
||||||
@@ -52,10 +52,12 @@ static inline void sd_spi_pio_program_init(PIO pio, uint sm, uint offset, uint m
|
|||||||
sm_config_set_out_shift(&c, false, true, 8);
|
sm_config_set_out_shift(&c, false, true, 8);
|
||||||
sm_config_set_in_shift(&c, false, true, 8);
|
sm_config_set_in_shift(&c, false, true, 8);
|
||||||
|
|
||||||
|
// high speed SPI needs to bypass the input synchronizers on the MISO pin
|
||||||
|
hw_set_bits(&pio->input_sync_bypass, 1u << miso);
|
||||||
|
|
||||||
const unsigned pio_freq = bitrate*4;
|
const unsigned pio_freq = bitrate*4;
|
||||||
const float div = clock_get_hz(clk_sys) / (float)pio_freq;
|
const float div = clock_get_hz(clk_sys) / (float)pio_freq;
|
||||||
// for some reason, small clkdiv values (even integer ones) cause issues
|
sm_config_set_clkdiv(&c, div);
|
||||||
sm_config_set_clkdiv(&c, div < 2.5f ? 2.5f : div);
|
|
||||||
pio_sm_init(pio, sm, offset, &c);
|
pio_sm_init(pio, sm, offset, &c);
|
||||||
}
|
}
|
||||||
%}
|
%}
|
||||||
30
software/modules/rp2_sd/sd_util.h
Normal file
30
software/modules/rp2_sd/sd_util.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
inline static uint8_t sd_crc7(size_t len, const uint8_t data[const static len])
|
||||||
|
{
|
||||||
|
const uint8_t poly = 0b1001;
|
||||||
|
uint8_t crc = 0;
|
||||||
|
for (size_t pos = 0; pos < len; ++pos) {
|
||||||
|
crc ^= data[pos];
|
||||||
|
for (int bit = 0; bit < 8; ++bit) {
|
||||||
|
crc = (crc << 1) ^ ((crc & 0x80) ? (poly << 1) : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc >> 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static uint16_t sd_crc16(size_t len, const uint8_t data[const static len])
|
||||||
|
{
|
||||||
|
const uint16_t poly = 0b1000000100001;
|
||||||
|
uint16_t crc = 0;
|
||||||
|
for (size_t pos = 0; pos < len; ++pos) {
|
||||||
|
crc ^= data[pos] << 8;
|
||||||
|
for (int bit = 0; bit < 8; ++bit) {
|
||||||
|
crc = (crc << 1) ^ ((crc & 0x8000) ? poly : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crc;
|
||||||
|
}
|
||||||
9
software/mypy.ini
Normal file
9
software/mypy.ini
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
[mypy]
|
||||||
|
platform = linux
|
||||||
|
mypy_path = $MYPY_CONFIG_FILE_DIR/src:$MYPY_CONFIG_FILE_DIR/typings
|
||||||
|
custom_typeshed_dir = $MYPY_CONFIG_FILE_DIR/typings
|
||||||
|
follow_imports = silent
|
||||||
|
exclude = "typings[\\/].*"
|
||||||
|
follow_imports_for_stubs = true
|
||||||
|
no_site_packages = true
|
||||||
|
check_untyped_defs = true
|
||||||
3
software/pytest.ini
Normal file
3
software/pytest.ini
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[pytest]
|
||||||
|
pythonpath = tests/mocks src
|
||||||
|
testpaths = tests
|
||||||
1
software/requirements.txt
Normal file
1
software/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
freezefs
|
||||||
192
software/src/app.py
Normal file
192
software/src/app.py
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
from collections import namedtuple
|
||||||
|
import time
|
||||||
|
from utils import TimerManager
|
||||||
|
|
||||||
|
|
||||||
|
Dependencies = namedtuple('Dependencies', ('mp3player', 'nfcreader', 'buttons', 'playlistdb', 'hwconfig', 'leds',
|
||||||
|
'config'))
|
||||||
|
|
||||||
|
# Should be ~ 6dB steps
|
||||||
|
VOLUME_CURVE = [1, 2, 4, 8, 16, 32, 63, 126, 251]
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerApp:
|
||||||
|
class TagStateMachine:
|
||||||
|
def __init__(self, parent, timer_manager, timeout=5000):
|
||||||
|
self.parent = parent
|
||||||
|
self.timer_manager = timer_manager
|
||||||
|
self.current_tag = None
|
||||||
|
self.current_tag_time = time.ticks_ms()
|
||||||
|
self.timeout = timeout
|
||||||
|
|
||||||
|
def onTagChange(self, new_tag):
|
||||||
|
if new_tag is not None:
|
||||||
|
self.timer_manager.cancel(self.onTagRemoveDelay)
|
||||||
|
if new_tag == self.current_tag:
|
||||||
|
return
|
||||||
|
# Change playlist on new tag
|
||||||
|
if new_tag is not None:
|
||||||
|
self.current_tag_time = time.ticks_ms()
|
||||||
|
self.current_tag = new_tag
|
||||||
|
self.parent.onNewTag(new_tag)
|
||||||
|
else:
|
||||||
|
self.timer_manager.schedule(time.ticks_ms() + self.timeout, self.onTagRemoveDelay)
|
||||||
|
|
||||||
|
def onTagRemoveDelay(self):
|
||||||
|
if self.current_tag is not None:
|
||||||
|
self.current_tag = None
|
||||||
|
self.parent.onTagRemoved()
|
||||||
|
|
||||||
|
def __init__(self, deps: Dependencies):
|
||||||
|
self.timer_manager = TimerManager()
|
||||||
|
self.config = deps.config(self)
|
||||||
|
self.tag_timeout_ms = self.config.get_tag_timeout() * 1000
|
||||||
|
self.idle_timeout_ms = self.config.get_idle_timeout() * 1000
|
||||||
|
self.tag_state_machine = self.TagStateMachine(self, self.timer_manager, self.tag_timeout_ms)
|
||||||
|
self.player = deps.mp3player(self)
|
||||||
|
self.nfc = deps.nfcreader(self.tag_state_machine)
|
||||||
|
self.playlist_db = deps.playlistdb(self)
|
||||||
|
self.hwconfig = deps.hwconfig(self)
|
||||||
|
self.leds = deps.leds(self)
|
||||||
|
self.tag_mode = self.config.get_tagmode()
|
||||||
|
self.playing_tag = None
|
||||||
|
self.playlist = None
|
||||||
|
self.buttons = deps.buttons(self) if deps.buttons is not None else None
|
||||||
|
self.mp3file = None
|
||||||
|
self.volume_pos = 3
|
||||||
|
self.paused = False
|
||||||
|
self.playing = False
|
||||||
|
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
|
||||||
|
self._onIdle()
|
||||||
|
|
||||||
|
def __del__(self):
|
||||||
|
if self.mp3file is not None:
|
||||||
|
self.mp3file.close()
|
||||||
|
self.mp3file = None
|
||||||
|
|
||||||
|
def onNewTag(self, new_tag):
|
||||||
|
"""
|
||||||
|
Callback (typically called by TagStateMachine) to signal that a new tag has been presented.
|
||||||
|
"""
|
||||||
|
uid_str = b''.join('{:02x}'.format(x).encode() for x in new_tag)
|
||||||
|
if self.tag_mode == 'tagremains' or (self.tag_mode == 'tagstartstop' and new_tag != self.playing_tag):
|
||||||
|
self._set_playlist(uid_str)
|
||||||
|
self.playing_tag = new_tag
|
||||||
|
elif self.tag_mode == 'tagstartstop':
|
||||||
|
print('Tag presented again, stopping playback')
|
||||||
|
self._unset_playlist()
|
||||||
|
self.playing_tag = None
|
||||||
|
|
||||||
|
def onTagRemoved(self):
|
||||||
|
"""
|
||||||
|
Callback (typically called by TagStateMachine) to signal that a tag has been removed.
|
||||||
|
"""
|
||||||
|
if self.tag_mode == 'tagremains':
|
||||||
|
print('Tag gone, stopping playback')
|
||||||
|
self._unset_playlist()
|
||||||
|
|
||||||
|
def onButtonPressed(self, what):
|
||||||
|
assert self.buttons is not None
|
||||||
|
if what == self.buttons.VOLUP:
|
||||||
|
self.volume_pos = min(self.volume_pos + 1, len(VOLUME_CURVE) - 1)
|
||||||
|
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
|
||||||
|
elif what == self.buttons.VOLDOWN:
|
||||||
|
self.volume_pos = max(self.volume_pos - 1, 0)
|
||||||
|
self.player.set_volume(VOLUME_CURVE[self.volume_pos])
|
||||||
|
elif what == self.buttons.NEXT:
|
||||||
|
self._play_next()
|
||||||
|
elif what == self.buttons.PREV:
|
||||||
|
self._play_prev()
|
||||||
|
elif what == self.buttons.PLAY_PAUSE:
|
||||||
|
self._pause_toggle()
|
||||||
|
|
||||||
|
def onPlaybackDone(self):
|
||||||
|
assert self.mp3file is not None
|
||||||
|
self.mp3file.close()
|
||||||
|
self.mp3file = None
|
||||||
|
self._play_next()
|
||||||
|
|
||||||
|
def onIdleTimeout(self):
|
||||||
|
if self.hwconfig.get_on_battery():
|
||||||
|
self.hwconfig.power_off()
|
||||||
|
else:
|
||||||
|
# Check again in a minute
|
||||||
|
self.timer_manager.schedule(time.ticks_ms() + self.idle_timeout_ms, self.onIdleTimeout)
|
||||||
|
|
||||||
|
def reset_idle_timeout(self):
|
||||||
|
if not self.playing:
|
||||||
|
self.timer_manager.schedule(time.ticks_ms() + self.idle_timeout_ms, self.onIdleTimeout)
|
||||||
|
|
||||||
|
def is_playing(self) -> bool:
|
||||||
|
return self.playing
|
||||||
|
|
||||||
|
def _set_playlist(self, tag: bytes):
|
||||||
|
if self.playlist is not None:
|
||||||
|
pos = self.player.stop()
|
||||||
|
if pos is not None:
|
||||||
|
self.playlist.setPlaybackOffset(pos)
|
||||||
|
self.playlist = self.playlist_db.getPlaylistForTag(tag)
|
||||||
|
self._play(self.playlist.getCurrentPath() if self.playlist is not None else None,
|
||||||
|
self.playlist.getPlaybackOffset() if self.playlist is not None else 0)
|
||||||
|
|
||||||
|
def _unset_playlist(self):
|
||||||
|
if self.playlist is not None:
|
||||||
|
pos = self.player.stop()
|
||||||
|
self._onIdle()
|
||||||
|
if pos is not None:
|
||||||
|
self.playlist.setPlaybackOffset(pos)
|
||||||
|
self.playlist = None
|
||||||
|
|
||||||
|
def _play_next(self):
|
||||||
|
if self.playlist is None:
|
||||||
|
return
|
||||||
|
filename = self.playlist.getNextPath()
|
||||||
|
self._play(filename)
|
||||||
|
if filename is None:
|
||||||
|
self.playlist = None
|
||||||
|
self.playing_tag = None
|
||||||
|
|
||||||
|
def _play_prev(self):
|
||||||
|
if self.playlist is None:
|
||||||
|
return
|
||||||
|
filename = self.playlist.getPrevPath()
|
||||||
|
self._play(filename)
|
||||||
|
if filename is None:
|
||||||
|
self.playlist = None
|
||||||
|
self.playing_tag = None
|
||||||
|
|
||||||
|
def _play(self, filename: bytes | None, offset=0):
|
||||||
|
if self.mp3file is not None:
|
||||||
|
self.player.stop()
|
||||||
|
self.mp3file.close()
|
||||||
|
self.mp3file = None
|
||||||
|
self._onIdle()
|
||||||
|
if filename is not None:
|
||||||
|
print(f'Playing {filename!r}')
|
||||||
|
self.mp3file = open(filename, 'rb')
|
||||||
|
self.player.play(self.mp3file, offset)
|
||||||
|
self.paused = False
|
||||||
|
self._onActive()
|
||||||
|
|
||||||
|
def _pause_toggle(self):
|
||||||
|
if self.playlist is None:
|
||||||
|
return
|
||||||
|
if self.paused:
|
||||||
|
self._play(self.playlist.getCurrentPath(), self.pause_offset)
|
||||||
|
else:
|
||||||
|
self.pause_offset = self.player.stop()
|
||||||
|
self.paused = True
|
||||||
|
self._onIdle()
|
||||||
|
|
||||||
|
def _onIdle(self):
|
||||||
|
self.timer_manager.schedule(time.ticks_ms() + self.idle_timeout_ms, self.onIdleTimeout)
|
||||||
|
self.leds.set_state(self.leds.IDLE)
|
||||||
|
self.playing = False
|
||||||
|
|
||||||
|
def _onActive(self):
|
||||||
|
self.timer_manager.cancel(self.onIdleTimeout)
|
||||||
|
self.leds.set_state(self.leds.PLAYING)
|
||||||
|
self.playing = True
|
||||||
75
software/src/hwconfig_Rev1/hwconfig.py
Normal file
75
software/src/hwconfig_Rev1/hwconfig.py
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
import machine
|
||||||
|
from machine import Pin
|
||||||
|
|
||||||
|
# SD Card SPI
|
||||||
|
SD_DI = Pin.board.GP3
|
||||||
|
SD_DO = Pin.board.GP4
|
||||||
|
SD_SCK = Pin.board.GP2
|
||||||
|
SD_CS = Pin.board.GP5
|
||||||
|
SD_CLOCKRATE = 25000000
|
||||||
|
|
||||||
|
# MAX98357
|
||||||
|
I2S_LRCLK = Pin.board.GP6
|
||||||
|
I2S_DCLK = Pin.board.GP7
|
||||||
|
I2S_DIN = Pin.board.GP8
|
||||||
|
I2S_SD = Pin.board.GP9
|
||||||
|
|
||||||
|
# RC522
|
||||||
|
RC522_SPIID = 1
|
||||||
|
RC522_RST = Pin.board.GP14
|
||||||
|
RC522_IRQ = Pin.board.GP15
|
||||||
|
RC522_MOSI = Pin.board.GP11
|
||||||
|
RC522_MISO = Pin.board.GP12
|
||||||
|
RC522_SCK = Pin.board.GP10
|
||||||
|
RC522_SS = Pin.board.GP13
|
||||||
|
|
||||||
|
# WS2812
|
||||||
|
LED_DIN = Pin.board.GP16
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
BUTTONS = [Pin.board.GP17,
|
||||||
|
Pin.board.GP18,
|
||||||
|
Pin.board.GP19,
|
||||||
|
Pin.board.GP20,
|
||||||
|
Pin.board.GP21,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Power
|
||||||
|
POWER_EN = Pin.board.GP22
|
||||||
|
VBAT_ADC = Pin.board.GP26
|
||||||
|
VBUS_DET = Pin.board.WL_GPIO2
|
||||||
|
|
||||||
|
|
||||||
|
def board_init():
|
||||||
|
# POWER_EN turned on in MICROPY_BOARD_STARTUP
|
||||||
|
# TODO: Implement soft power off
|
||||||
|
|
||||||
|
# SD_DO / MISO input doesn't need any special configuration
|
||||||
|
# Set 8 mA drive strength for SCK and MOSI
|
||||||
|
machine.mem32[0x4001c004 + 2*4] = 0x60 # SCK
|
||||||
|
machine.mem32[0x4001c004 + 3*4] = 0x60 # MOSI
|
||||||
|
# SD_CS doesn't need any special configuration
|
||||||
|
|
||||||
|
# Permanently enable amplifier
|
||||||
|
# TODO: Implement amplifier power management
|
||||||
|
I2S_SD.init(mode=Pin.OPEN_DRAIN)
|
||||||
|
I2S_SD.value(1)
|
||||||
|
|
||||||
|
|
||||||
|
def get_battery_voltage():
|
||||||
|
adc = machine.ADC(VBAT_ADC) # create ADC object on ADC pin
|
||||||
|
battv = adc.read_u16()/65535.0*3.3*2
|
||||||
|
return battv
|
||||||
|
|
||||||
|
|
||||||
|
def power_off():
|
||||||
|
POWER_EN.init(mode=Pin.OUT)
|
||||||
|
POWER_EN.value(0)
|
||||||
|
|
||||||
|
|
||||||
|
def get_on_battery():
|
||||||
|
vbus = VBUS_DET.value()
|
||||||
|
return not vbus
|
||||||
57
software/src/hwconfig_breadboard/hwconfig.py
Normal file
57
software/src/hwconfig_breadboard/hwconfig.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
from machine import Pin
|
||||||
|
|
||||||
|
# SD Card SPI
|
||||||
|
SD_DI = Pin.board.GP3
|
||||||
|
SD_DO = Pin.board.GP4
|
||||||
|
SD_SCK = Pin.board.GP2
|
||||||
|
SD_CS = Pin.board.GP5
|
||||||
|
SD_CLOCKRATE = 15000000
|
||||||
|
|
||||||
|
# MAX98357
|
||||||
|
I2S_LRCLK = Pin.board.GP7
|
||||||
|
I2S_DCLK = Pin.board.GP6
|
||||||
|
I2S_DIN = Pin.board.GP8
|
||||||
|
I2S_SD = None
|
||||||
|
|
||||||
|
# RC522
|
||||||
|
RC522_SPIID = 1
|
||||||
|
RC522_RST = Pin.board.GP9
|
||||||
|
RC522_IRQ = Pin.board.GP14
|
||||||
|
RC522_MOSI = Pin.board.GP11
|
||||||
|
RC522_MISO = Pin.board.GP12
|
||||||
|
RC522_SCK = Pin.board.GP10
|
||||||
|
RC522_SS = Pin.board.GP13
|
||||||
|
|
||||||
|
# WS2812
|
||||||
|
LED_DIN = Pin.board.GP16
|
||||||
|
|
||||||
|
# Buttons
|
||||||
|
BUTTONS = [Pin.board.GP17,
|
||||||
|
Pin.board.GP18,
|
||||||
|
Pin.board.GP19,
|
||||||
|
]
|
||||||
|
|
||||||
|
# Power
|
||||||
|
POWER_EN = None
|
||||||
|
VBAT_ADC = Pin.board.GP26
|
||||||
|
|
||||||
|
|
||||||
|
def board_init():
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_battery_voltage():
|
||||||
|
# Not supported on breadboard
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def power_off():
|
||||||
|
# Not supported on breadboard
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def get_on_battery():
|
||||||
|
return False
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
# SPDX-License-Identifier: MIT
|
|
||||||
# Copyright (c) 2024 Matthias Blankertz <matthias@blankertz.org>
|
|
||||||
|
|
||||||
# Run with mpremote.py run src/led_test.py
|
|
||||||
|
|
||||||
from machine import Pin
|
|
||||||
from math import pi, sin, pow
|
|
||||||
from micropython import const
|
|
||||||
from rp2_neopixel import NeoPixel
|
|
||||||
from time import sleep, ticks_ms
|
|
||||||
import asyncio
|
|
||||||
|
|
||||||
pin = Pin.board.GP16
|
|
||||||
leds = const(10)
|
|
||||||
brightness = 0.5
|
|
||||||
|
|
||||||
np = NeoPixel(pin, leds)
|
|
||||||
|
|
||||||
# test fill and write
|
|
||||||
|
|
||||||
print("LEDs should now turn red")
|
|
||||||
np.fill((255, 0, 0))
|
|
||||||
np.write()
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
print("LEDs should now turn green")
|
|
||||||
np.fill((0, 255, 0))
|
|
||||||
np.write()
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
print("LEDs should now turn blue")
|
|
||||||
np.fill((0, 0, 255))
|
|
||||||
np.write()
|
|
||||||
sleep(1)
|
|
||||||
|
|
||||||
|
|
||||||
# test async
|
|
||||||
def gamma(value, X=2.2):
|
|
||||||
return min(max(int(brightness * pow(value / 255.0, X) * 255.0 + 0.5), 0), 255)
|
|
||||||
|
|
||||||
|
|
||||||
async def rainbow(np, period=10):
|
|
||||||
count = 0.0
|
|
||||||
while True:
|
|
||||||
for i in range(leds):
|
|
||||||
ofs = (count + i) % leds
|
|
||||||
np[i] = (gamma((sin(ofs / leds * 2 * pi) + 1) * 127),
|
|
||||||
gamma((sin(ofs / leds * 2 * pi + 2/3*pi) + 1) * 127),
|
|
||||||
gamma((sin(ofs / leds * 2 * pi + 4/3*pi) + 1) * 127))
|
|
||||||
count += 0.2
|
|
||||||
before = ticks_ms()
|
|
||||||
await np.async_write()
|
|
||||||
now = ticks_ms()
|
|
||||||
if before + 20 > now:
|
|
||||||
await asyncio.sleep_ms(20 - (now - before))
|
|
||||||
|
|
||||||
print("LEDs should now start rainbowing")
|
|
||||||
asyncio.run(rainbow(np))
|
|
||||||
136
software/src/main.py
Normal file
136
software/src/main.py
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2024-2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
import aiorepl # type: ignore
|
||||||
|
import asyncio
|
||||||
|
from errno import ENOENT
|
||||||
|
import machine
|
||||||
|
import micropython
|
||||||
|
import network
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import ubinascii
|
||||||
|
|
||||||
|
# Own modules
|
||||||
|
import app
|
||||||
|
from audiocore import AudioContext
|
||||||
|
import frozen_frontend # noqa: F401
|
||||||
|
from mfrc522 import MFRC522
|
||||||
|
from mp3player import MP3Player
|
||||||
|
from nfc import Nfc
|
||||||
|
from rp2_neopixel import NeoPixel
|
||||||
|
from utils import BTreeFileManager, Buttons, SDContext, TimerManager, LedManager, Configuration
|
||||||
|
from webserver import start_webserver
|
||||||
|
|
||||||
|
try:
|
||||||
|
import hwconfig
|
||||||
|
except ImportError:
|
||||||
|
print("Fatal: No hwconfig.py found")
|
||||||
|
raise
|
||||||
|
|
||||||
|
micropython.alloc_emergency_exception_buf(100)
|
||||||
|
|
||||||
|
# Machine setup
|
||||||
|
hwconfig.board_init()
|
||||||
|
|
||||||
|
# high prio for proc 1
|
||||||
|
machine.mem32[0x40030000 + 0x00] = 0x10
|
||||||
|
|
||||||
|
|
||||||
|
def setup_wifi():
|
||||||
|
network.hostname("TonberryPico")
|
||||||
|
wlan = network.WLAN(network.WLAN.IF_AP)
|
||||||
|
wlan.config(ssid=f"TonberryPicoAP_{machine.unique_id().hex()}", security=wlan.SEC_OPEN)
|
||||||
|
wlan.active(True)
|
||||||
|
|
||||||
|
# disable power management
|
||||||
|
wlan.config(pm=network.WLAN.PM_NONE)
|
||||||
|
|
||||||
|
mac = ubinascii.hexlify(network.WLAN().config('mac'), ':').decode()
|
||||||
|
print(f" mac: {mac}")
|
||||||
|
print(f" channel: {wlan.config('channel')}")
|
||||||
|
print(f" essid: {wlan.config('essid')}")
|
||||||
|
print(f" txpower: {wlan.config('txpower')}")
|
||||||
|
print(f"ifconfig: {wlan.ifconfig()}")
|
||||||
|
|
||||||
|
|
||||||
|
async def wdt_task(wdt):
|
||||||
|
# TODO: more checking of app health
|
||||||
|
# Right now this only protects against the asyncio executor crashing completely
|
||||||
|
while True:
|
||||||
|
await asyncio.sleep_ms(500)
|
||||||
|
wdt.feed()
|
||||||
|
|
||||||
|
DB_PATH = '/sd/tonberry.db'
|
||||||
|
|
||||||
|
config = Configuration()
|
||||||
|
|
||||||
|
|
||||||
|
def run():
|
||||||
|
asyncio.new_event_loop()
|
||||||
|
# Setup LEDs
|
||||||
|
np = NeoPixel(hwconfig.LED_DIN, config.get_led_count(), sm=1)
|
||||||
|
|
||||||
|
# Wifi with default config
|
||||||
|
setup_wifi()
|
||||||
|
|
||||||
|
# Setup MP3 player
|
||||||
|
with SDContext(mosi=hwconfig.SD_DI, miso=hwconfig.SD_DO, sck=hwconfig.SD_SCK, ss=hwconfig.SD_CS,
|
||||||
|
baudrate=hwconfig.SD_CLOCKRATE):
|
||||||
|
# Temporary hack: build database from folders if no database exists
|
||||||
|
# Can be removed once playlists can be created via API
|
||||||
|
try:
|
||||||
|
_ = os.stat(DB_PATH)
|
||||||
|
except OSError as ex:
|
||||||
|
if ex.errno == ENOENT:
|
||||||
|
print("No playlist DB found, trying to build DB from tag dirs")
|
||||||
|
builddb()
|
||||||
|
|
||||||
|
with BTreeFileManager(DB_PATH) as playlistdb, \
|
||||||
|
AudioContext(hwconfig.I2S_DIN, hwconfig.I2S_DCLK, hwconfig.I2S_LRCLK) as audioctx:
|
||||||
|
|
||||||
|
# Setup NFC
|
||||||
|
reader = MFRC522(spi_id=hwconfig.RC522_SPIID, sck=hwconfig.RC522_SCK, miso=hwconfig.RC522_MISO,
|
||||||
|
mosi=hwconfig.RC522_MOSI, cs=hwconfig.RC522_SS, rst=hwconfig.RC522_RST, tocard_retries=20)
|
||||||
|
|
||||||
|
# Setup app
|
||||||
|
deps = app.Dependencies(mp3player=lambda the_app: MP3Player(audioctx, the_app),
|
||||||
|
nfcreader=lambda the_app: Nfc(reader, the_app),
|
||||||
|
buttons=lambda the_app: Buttons(the_app, config, hwconfig),
|
||||||
|
playlistdb=lambda _: playlistdb,
|
||||||
|
hwconfig=lambda _: hwconfig,
|
||||||
|
leds=lambda _: LedManager(np),
|
||||||
|
config=lambda _: config)
|
||||||
|
the_app = app.PlayerApp(deps)
|
||||||
|
|
||||||
|
start_webserver(config, the_app)
|
||||||
|
# Start
|
||||||
|
wdt = machine.WDT(timeout=1000)
|
||||||
|
asyncio.create_task(aiorepl.task({'timer_manager': TimerManager(),
|
||||||
|
'app': the_app}))
|
||||||
|
asyncio.create_task(wdt_task(wdt))
|
||||||
|
asyncio.get_event_loop().run_forever()
|
||||||
|
|
||||||
|
|
||||||
|
def builddb():
|
||||||
|
"""
|
||||||
|
For testing, build a playlist db based on the previous tag directory format.
|
||||||
|
Can be removed once uploading files / playlist via the web api is possible.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
os.unlink(DB_PATH)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
with BTreeFileManager(DB_PATH) as db:
|
||||||
|
for name, type_, _, _ in os.ilistdir(b'/sd'):
|
||||||
|
if type_ != 0x4000:
|
||||||
|
continue
|
||||||
|
fl = [b'/sd/' + name + b'/' + x for x in os.listdir(b'/sd/' + name) if x.endswith(b'.mp3')]
|
||||||
|
db.createPlaylistForTag(name, fl)
|
||||||
|
os.sync()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
time.sleep(1)
|
||||||
|
if machine.Pin(hwconfig.BUTTONS[0], machine.Pin.IN, machine.Pin.PULL_UP).value() != 0:
|
||||||
|
run()
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
from mfrc522 import MFRC522
|
|
||||||
import asyncio
|
|
||||||
import time
|
|
||||||
|
|
||||||
delay_sum = 0
|
|
||||||
delay_count = 0
|
|
||||||
max_delay = 0
|
|
||||||
|
|
||||||
|
|
||||||
async def latency_test():
|
|
||||||
global delay_sum
|
|
||||||
global delay_count
|
|
||||||
global max_delay
|
|
||||||
global min_delay
|
|
||||||
min_delay = 0xffffffff
|
|
||||||
await asyncio.sleep_ms(1)
|
|
||||||
while True:
|
|
||||||
for _ in range(2000):
|
|
||||||
before = time.ticks_us()
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
after = time.ticks_us()
|
|
||||||
delay = after - before
|
|
||||||
delay_sum += delay
|
|
||||||
delay_count += 1
|
|
||||||
if delay > max_delay:
|
|
||||||
max_delay = delay
|
|
||||||
if delay < min_delay:
|
|
||||||
min_delay = delay
|
|
||||||
await asyncio.sleep_ms(1)
|
|
||||||
print(f"delay (min / max / avg) [µs]: ({min_delay} / {max_delay} / {delay/delay_sum})")
|
|
||||||
|
|
||||||
|
|
||||||
def uid_to_string(uid: list):
|
|
||||||
return '0x' + ''.join(f'{i:02x}' for i in uid)
|
|
||||||
|
|
||||||
|
|
||||||
async def get_tag_uid(reader: MFRC522, poll_interval_ms: int = 50) -> list:
|
|
||||||
'''
|
|
||||||
The maximum measured delay with poll_interval_ms=50 and a reader with tocard_retries=5 is
|
|
||||||
15.9 ms:
|
|
||||||
delay (min / max / avg) [µs]: (360 / 15945 / 1.892923e-06)
|
|
||||||
|
|
||||||
The maximum measured delay dropped to 11.6 ms by setting tocard_retries=1:
|
|
||||||
delay (min / max / avg) [µs]: (368 / 11696 / 6.204211e-06)
|
|
||||||
'''
|
|
||||||
while True:
|
|
||||||
reader.init()
|
|
||||||
|
|
||||||
# For now we omit the tag type
|
|
||||||
(stat, _) = reader.request(reader.REQIDL)
|
|
||||||
if stat == reader.OK:
|
|
||||||
(stat, uid) = reader.SelectTagSN()
|
|
||||||
if stat == reader.OK:
|
|
||||||
print(f"uid={uid_to_string(uid)}")
|
|
||||||
|
|
||||||
await asyncio.sleep_ms(poll_interval_ms)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
reader = MFRC522(spi_id=1, sck=10, miso=12, mosi=11, cs=13, rst=9, tocard_retries=1)
|
|
||||||
|
|
||||||
print("")
|
|
||||||
print("Please place card on reader")
|
|
||||||
print("")
|
|
||||||
|
|
||||||
asyncio.create_task(get_tag_uid(reader))
|
|
||||||
asyncio.create_task(latency_test())
|
|
||||||
|
|
||||||
asyncio.get_event_loop().run_forever()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
import rp2
|
|
||||||
import network
|
|
||||||
import ubinascii
|
|
||||||
from microdot import Microdot
|
|
||||||
|
|
||||||
rp2.country('DE')
|
|
||||||
|
|
||||||
wlan = network.WLAN(network.AP_IF)
|
|
||||||
wlan.config(ssid='TonberryPico', security=network.WLAN.SEC_OPEN)
|
|
||||||
# Important: we cannot change the ip in station mode, otherwise dhcp won't work!
|
|
||||||
# wlan.ipconfig(addr4='10.0.0.1')
|
|
||||||
wlan.active(True) # loads the firmware
|
|
||||||
while wlan.active() is False:
|
|
||||||
pass
|
|
||||||
wlan.config(pm=network.WLAN.PM_NONE)
|
|
||||||
|
|
||||||
mac = ubinascii.hexlify(network.WLAN().config('mac'), ':').decode()
|
|
||||||
print(f" mac: {mac}")
|
|
||||||
print(f" channel: {wlan.config('channel')}")
|
|
||||||
print(f" essid: {wlan.config('essid')}")
|
|
||||||
print(f" txpower: {wlan.config('txpower')}")
|
|
||||||
print(f"ifconfig: {wlan.ifconfig()}")
|
|
||||||
|
|
||||||
app = Microdot()
|
|
||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
|
||||||
async def index(request):
|
|
||||||
print("wohoo, a guest :)")
|
|
||||||
print(f" app: {request.app}")
|
|
||||||
print(f" client: {request.client_addr}")
|
|
||||||
print(f" method: {request.method}")
|
|
||||||
print(f" url: {request.url}")
|
|
||||||
print(f" headers: {request.headers}")
|
|
||||||
print(f" cookies: {request.cookies}")
|
|
||||||
return "TonberryPico says 'Hello World!'"
|
|
||||||
|
|
||||||
app.run(port=80)
|
|
||||||
@@ -3,118 +3,79 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
from array import array
|
from array import array
|
||||||
|
from utils import TimerManager
|
||||||
|
try:
|
||||||
|
from typing import TYPE_CHECKING # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import typing
|
||||||
|
|
||||||
|
class PlayerCallback(typing.Protocol):
|
||||||
|
def onPlaybackDone(self) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
class MP3Player:
|
class MP3Player:
|
||||||
def __init__(self, audiocore):
|
def __init__(self, audiocore, cb: PlayerCallback):
|
||||||
self.audiocore = audiocore
|
self.audiocore = audiocore
|
||||||
self.commands = []
|
|
||||||
self.command_event = asyncio.Event()
|
|
||||||
self.playlist = []
|
|
||||||
self.mp3task = None
|
self.mp3task = None
|
||||||
|
self.volume = 128
|
||||||
|
self.cb = cb
|
||||||
|
self.pos = 0
|
||||||
|
|
||||||
def set_playlist(self, mp3files):
|
def play(self, stream, offset=0):
|
||||||
"""
|
"""
|
||||||
Set a new playlist and start playing from the first entry.
|
Play from byte stream.
|
||||||
For convenience a single file name can also be passed.
|
If offset > 0, discard the first offset bytes
|
||||||
"""
|
"""
|
||||||
if type(mp3files) is bytes:
|
if self.mp3task is not None:
|
||||||
self.playlist = [mp3files]
|
self.mp3task.cancel()
|
||||||
else:
|
self.mp3task = None
|
||||||
self.playlist = mp3files
|
if offset > 0:
|
||||||
self._send_command('newplaylist')
|
stream.seek(offset, 1)
|
||||||
|
self.pos = offset
|
||||||
def play_next(self):
|
self.mp3task = asyncio.create_task(self._play_task(stream))
|
||||||
"""
|
|
||||||
Skip to the next track in the playlist. Reaching the end of the playlist stops playback.
|
|
||||||
"""
|
|
||||||
self._send_command('next')
|
|
||||||
|
|
||||||
def play_prev(self):
|
|
||||||
"""
|
|
||||||
Skip to the previous track in the playlist.
|
|
||||||
"""
|
|
||||||
self._send_command('prev')
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""
|
"""
|
||||||
Stop playback, remembering the current position in the playlist (but not inside a track).
|
Stop playback
|
||||||
"""
|
"""
|
||||||
self._send_command('stop')
|
if self.mp3task is not None:
|
||||||
|
self.mp3task.cancel()
|
||||||
def play(self):
|
self.mp3task = None
|
||||||
"""
|
return self.pos
|
||||||
Start playback.
|
return None
|
||||||
"""
|
|
||||||
self._send_command('play')
|
|
||||||
|
|
||||||
def set_volume(self, volume: int):
|
def set_volume(self, volume: int):
|
||||||
"""
|
"""
|
||||||
Set volume (0..255).
|
Set volume (0..255).
|
||||||
"""
|
"""
|
||||||
|
self.volume = volume
|
||||||
self.audiocore.set_volume(volume)
|
self.audiocore.set_volume(volume)
|
||||||
|
|
||||||
def _send_command(self, command: str):
|
def get_volume(self) -> int:
|
||||||
self.commands.append(command)
|
return self.volume
|
||||||
self.command_event.set()
|
|
||||||
|
|
||||||
async def _play_task(self, mp3path):
|
async def _play_task(self, stream):
|
||||||
known_underruns = 0
|
known_underruns = 0
|
||||||
|
send_done = False
|
||||||
data = array('b', range(512))
|
data = array('b', range(512))
|
||||||
try:
|
try:
|
||||||
print(b'Playing ' + mp3path)
|
|
||||||
with open(mp3path, 'rb') as mp3file:
|
|
||||||
while True:
|
while True:
|
||||||
bytes_read = mp3file.readinto(data)
|
bytes_read = stream.readinto(data)
|
||||||
if bytes_read == 0:
|
if bytes_read == 0:
|
||||||
# End of file
|
# End of file
|
||||||
break
|
break
|
||||||
_, _, underruns = await self.audiocore.async_put(data[:bytes_read])
|
_, _, underruns = await self.audiocore.async_put(data[:bytes_read])
|
||||||
|
self.pos += bytes_read
|
||||||
if underruns > known_underruns:
|
if underruns > known_underruns:
|
||||||
print(f"{underruns:x}")
|
print(f"{underruns:x}")
|
||||||
known_underruns = underruns
|
known_underruns = underruns
|
||||||
# Intentionally do not use _send_command, we don't want to set command_event yet
|
# Call onPlaybackDone after flush
|
||||||
self.commands.append('done')
|
send_done = True
|
||||||
finally:
|
finally:
|
||||||
self.audiocore.flush()
|
self.audiocore.flush()
|
||||||
self.command_event.set()
|
if send_done:
|
||||||
|
# Only call onPlaybackDone if exit due to end of stream
|
||||||
def _play(self, mp3path):
|
# Use timer with time 0 to call callback "immediately" but from a different task
|
||||||
if self.mp3task is not None:
|
TimerManager().schedule(0, self.cb.onPlaybackDone)
|
||||||
self.mp3task.cancel()
|
|
||||||
self.mp3task = None
|
|
||||||
if mp3path is not None:
|
|
||||||
self.mp3task = asyncio.create_task(self._play_task(mp3path))
|
|
||||||
|
|
||||||
async def task(self):
|
|
||||||
playlist_pos = 0
|
|
||||||
while True:
|
|
||||||
await self.command_event.wait()
|
|
||||||
self.command_event.clear()
|
|
||||||
change_play = False
|
|
||||||
while len(self.commands) > 0:
|
|
||||||
command = self.commands.pop()
|
|
||||||
if command == 'next' or command == 'done':
|
|
||||||
if playlist_pos + 1 < len(self.playlist):
|
|
||||||
playlist_pos += 1
|
|
||||||
change_play = True
|
|
||||||
else:
|
|
||||||
# reaching the end of the playlist stops playback
|
|
||||||
self._play(None)
|
|
||||||
elif command == 'prev':
|
|
||||||
if playlist_pos > 0:
|
|
||||||
playlist_pos -= 1
|
|
||||||
change_play = True
|
|
||||||
elif command == 'stop':
|
|
||||||
self._play(None)
|
|
||||||
elif command == 'play':
|
|
||||||
if self.mp3task is None:
|
|
||||||
change_play = True
|
|
||||||
elif command == 'newplaylist':
|
|
||||||
if len(self.playlist) > 0:
|
|
||||||
playlist_pos = 0
|
|
||||||
change_play = True
|
|
||||||
else:
|
|
||||||
self._play(None)
|
|
||||||
if change_play:
|
|
||||||
self._play(self.playlist[playlist_pos])
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
'''
|
'''
|
||||||
SPDX-License-Identifier: MIT
|
SPDX-License-Identifier: MIT
|
||||||
Copyright (c) 2025 Stefan Kratochwil (Kratochwil-LA@gmx.de)
|
Copyright (c) 2025 Stefan Kratochwil (Kratochwil-LA@gmx.de)
|
||||||
|
Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
'''
|
'''
|
||||||
from nfc.nfc import Nfc
|
from nfc.nfc import Nfc
|
||||||
|
|
||||||
__all__ = ['Nfc']
|
__all__ = (Nfc)
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
'''
|
'''
|
||||||
SPDX-License-Identifier: MIT
|
SPDX-License-Identifier: MIT
|
||||||
Copyright (c) 2025 Stefan Kratochwil (Kratochwil-LA@gmx.de)
|
Copyright (c) 2025 Stefan Kratochwil (Kratochwil-LA@gmx.de)
|
||||||
|
Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
|
from utils import safe_callback
|
||||||
|
|
||||||
from mfrc522 import MFRC522
|
from mfrc522 import MFRC522
|
||||||
|
try:
|
||||||
|
from typing import TYPE_CHECKING # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import typing
|
||||||
|
|
||||||
|
class TagCallback(typing.Protocol):
|
||||||
|
def onTagChange(self, uid: list[int]) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
class Nfc:
|
class Nfc:
|
||||||
@@ -28,10 +39,11 @@ class Nfc:
|
|||||||
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
'''
|
'''
|
||||||
def __init__(self, reader: MFRC522):
|
def __init__(self, reader: MFRC522, cb: TagCallback | None = None):
|
||||||
self.reader = reader
|
self.reader = reader
|
||||||
self.last_uid = None
|
self.last_uid: list[int] | None = None
|
||||||
self.last_uid_timestamp = None
|
self.last_uid_timestamp: int | None = None
|
||||||
|
self.cb = cb
|
||||||
self.task = asyncio.create_task(self._reader_poll_task())
|
self.task = asyncio.create_task(self._reader_poll_task())
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@@ -41,20 +53,30 @@ class Nfc:
|
|||||||
'''
|
'''
|
||||||
return '0x' + ''.join(f'{i:02x}' for i in uid)
|
return '0x' + ''.join(f'{i:02x}' for i in uid)
|
||||||
|
|
||||||
async def _reader_poll_task(self, poll_interval_ms: int = 50):
|
def _read_tag_sn(self) -> list[int] | None:
|
||||||
'''
|
|
||||||
Periodically polls the nfc reader. Stores tag uid and timestamp if a new tag was found.
|
|
||||||
'''
|
|
||||||
while True:
|
|
||||||
self.reader.init()
|
|
||||||
|
|
||||||
# For now we omit the tag type
|
|
||||||
(stat, _) = self.reader.request(self.reader.REQIDL)
|
(stat, _) = self.reader.request(self.reader.REQIDL)
|
||||||
if stat == self.reader.OK:
|
if stat == self.reader.OK:
|
||||||
(stat, uid) = self.reader.SelectTagSN()
|
(stat, uid) = self.reader.SelectTagSN()
|
||||||
if stat == self.reader.OK:
|
if stat == self.reader.OK:
|
||||||
|
return uid
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _reader_poll_task(self, poll_interval_ms: int = 50):
|
||||||
|
'''
|
||||||
|
Periodically polls the nfc reader. Stores tag uid and timestamp if a new tag was found.
|
||||||
|
'''
|
||||||
|
last_callback_uid = None
|
||||||
|
while True:
|
||||||
|
self.reader.init()
|
||||||
|
|
||||||
|
# For now we omit the tag type
|
||||||
|
uid = self._read_tag_sn()
|
||||||
|
if uid is not None:
|
||||||
self.last_uid = uid
|
self.last_uid = uid
|
||||||
self.last_uid_timestamp = time.ticks_us()
|
self.last_uid_timestamp = time.ticks_us()
|
||||||
|
if self.cb is not None and last_callback_uid != uid:
|
||||||
|
safe_callback(lambda: self.cb.onTagChange(uid), "nfc tag change")
|
||||||
|
last_callback_uid = uid
|
||||||
|
|
||||||
await asyncio.sleep_ms(poll_interval_ms)
|
await asyncio.sleep_ms(poll_interval_ms)
|
||||||
|
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <stddef.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
inline static uint8_t sd_crc7(size_t len, const uint8_t data[const static len])
|
|
||||||
{
|
|
||||||
const uint8_t poly = 0b1001;
|
|
||||||
uint8_t crc = 0;
|
|
||||||
for (size_t pos = 0; pos < len; ++pos) {
|
|
||||||
crc ^= data[pos];
|
|
||||||
for (int bit = 0; bit < 8; ++bit) {
|
|
||||||
crc = (crc << 1) ^ ((crc & 0x80) ? (poly << 1) : 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return crc >> 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* inline static uint16_t sd_crc16(size_t len, const uint8_t data[const static len]) */
|
|
||||||
/* { */
|
|
||||||
/* const uint16_t poly = 0b1000000100001; */
|
|
||||||
/* uint16_t crc = 0; */
|
|
||||||
/* for (size_t pos = 0; pos < len; ++pos) { */
|
|
||||||
/* crc ^= data[pos] << 8; */
|
|
||||||
/* for (int bit = 0; bit < 8; ++bit) { */
|
|
||||||
/* crc = (crc << 1) ^ ((crc & 0x8000) ? poly : 0); */
|
|
||||||
/* } */
|
|
||||||
/* } */
|
|
||||||
/* return crc; */
|
|
||||||
/* } */
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
# SPDX-License-Identifier: MIT
|
|
||||||
# Copyright (c) 2024-2025 Matthias Blankertz <matthias@blankertz.org>
|
|
||||||
|
|
||||||
import aiorepl
|
|
||||||
import asyncio
|
|
||||||
import machine
|
|
||||||
import micropython
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
from machine import Pin
|
|
||||||
from math import pi, sin, pow
|
|
||||||
from micropython import const
|
|
||||||
|
|
||||||
# Own modules
|
|
||||||
from audiocore import Audiocore
|
|
||||||
from mp3player import MP3Player
|
|
||||||
from rp2_neopixel import NeoPixel
|
|
||||||
from rp2_sd import SDCard
|
|
||||||
|
|
||||||
micropython.alloc_emergency_exception_buf(100)
|
|
||||||
|
|
||||||
leds = const(10)
|
|
||||||
brightness = 0.5
|
|
||||||
|
|
||||||
|
|
||||||
def gamma(value, X=2.2):
|
|
||||||
return min(max(int(brightness * pow(value / 255.0, X) * 255.0 + 0.5), 0), 255)
|
|
||||||
|
|
||||||
|
|
||||||
async def rainbow(np, period=10):
|
|
||||||
count = 0.0
|
|
||||||
while True:
|
|
||||||
for i in range(leds):
|
|
||||||
ofs = (count + i) % leds
|
|
||||||
np[i] = (gamma((sin(ofs / leds * 2 * pi) + 1) * 127),
|
|
||||||
gamma((sin(ofs / leds * 2 * pi + 2/3*pi) + 1) * 127),
|
|
||||||
gamma((sin(ofs / leds * 2 * pi + 4/3*pi) + 1) * 127))
|
|
||||||
count += 0.2
|
|
||||||
before = time.ticks_ms()
|
|
||||||
await np.async_write()
|
|
||||||
now = time.ticks_ms()
|
|
||||||
if before + 20 > now:
|
|
||||||
await asyncio.sleep_ms(20 - (now - before))
|
|
||||||
|
|
||||||
|
|
||||||
# Set 8 mA drive strength and fast slew rate
|
|
||||||
machine.mem32[0x4001c004 + 6*4] = 0x67
|
|
||||||
machine.mem32[0x4001c004 + 7*4] = 0x67
|
|
||||||
machine.mem32[0x4001c004 + 8*4] = 0x67
|
|
||||||
|
|
||||||
|
|
||||||
def list_sd():
|
|
||||||
try:
|
|
||||||
sd = SDCard(mosi=Pin(3), miso=Pin(4), sck=Pin(2), ss=Pin(5), baudrate=15000000)
|
|
||||||
except OSError:
|
|
||||||
for i in range(leds):
|
|
||||||
np[i] = (255, 0, 0)
|
|
||||||
np.write()
|
|
||||||
return
|
|
||||||
try:
|
|
||||||
os.mount(sd, '/sd')
|
|
||||||
print(os.listdir(b'/sd'))
|
|
||||||
except OSError as ex:
|
|
||||||
print(f"{ex}")
|
|
||||||
|
|
||||||
|
|
||||||
delay_sum = 0
|
|
||||||
delay_count = 0
|
|
||||||
max_delay = 0
|
|
||||||
|
|
||||||
|
|
||||||
async def latency_test():
|
|
||||||
global delay_sum
|
|
||||||
global delay_count
|
|
||||||
global max_delay
|
|
||||||
await asyncio.sleep_ms(1)
|
|
||||||
while True:
|
|
||||||
for _ in range(2000):
|
|
||||||
before = time.ticks_us()
|
|
||||||
await asyncio.sleep(0)
|
|
||||||
after = time.ticks_us()
|
|
||||||
delay = after - before
|
|
||||||
delay_sum += delay
|
|
||||||
delay_count += 1
|
|
||||||
if delay > max_delay:
|
|
||||||
max_delay = delay
|
|
||||||
await asyncio.sleep_ms(1)
|
|
||||||
print(f"Max delay {max_delay} us, average {delay/delay_sum} us")
|
|
||||||
|
|
||||||
pin = Pin.board.GP16
|
|
||||||
np = NeoPixel(pin, leds)
|
|
||||||
|
|
||||||
# Test SD card
|
|
||||||
list_sd()
|
|
||||||
|
|
||||||
# Test NeoPixel
|
|
||||||
asyncio.create_task(rainbow(np))
|
|
||||||
|
|
||||||
# Test audio
|
|
||||||
audioctx = Audiocore(Pin(8), Pin(6))
|
|
||||||
|
|
||||||
player = MP3Player(audioctx)
|
|
||||||
|
|
||||||
# high prio for proc 1
|
|
||||||
machine.mem32[0x40030000 + 0x00] = 0x10
|
|
||||||
|
|
||||||
testfiles = [b'/sd/' + name for name in os.listdir(b'/sd') if name.endswith(b'mp3')]
|
|
||||||
player.set_playlist(testfiles)
|
|
||||||
asyncio.create_task(player.task())
|
|
||||||
|
|
||||||
|
|
||||||
asyncio.create_task(aiorepl.task({'player': player}))
|
|
||||||
asyncio.get_event_loop().run_forever()
|
|
||||||
53
software/src/tonberry.schema.json
Normal file
53
software/src/tonberry.schema.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||||
|
"definitions": {
|
||||||
|
"PlaybackPosition": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"position_seconds": { "type": "number" },
|
||||||
|
"device_uptime": { "type": "number" }
|
||||||
|
},
|
||||||
|
"required": ["position_seconds"]
|
||||||
|
},
|
||||||
|
"AudioFile": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string", "format": "uuid" },
|
||||||
|
"filename": { "type": "string" },
|
||||||
|
"size_bytes": { "type": "integer" },
|
||||||
|
"duration_seconds": { "type": "number" },
|
||||||
|
"last_played_uptime": { "type": "number" },
|
||||||
|
"playback_position": { "$ref": "#/definitions/PlaybackPosition" }
|
||||||
|
},
|
||||||
|
"required": ["id", "filename"]
|
||||||
|
},
|
||||||
|
"Playlist": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": { "type": "string", "format": "uuid" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"audio_files": {
|
||||||
|
"type": "array",
|
||||||
|
"items": { "$ref": "#/definitions/AudioFile" }
|
||||||
|
},
|
||||||
|
"current_track_index": { "type": "integer", "minimum": 0 },
|
||||||
|
"last_played_uptime": { "type": "number" },
|
||||||
|
"playback_position": { "$ref": "#/definitions/PlaybackPosition" }
|
||||||
|
},
|
||||||
|
"required": ["id", "name", "audio_files"]
|
||||||
|
},
|
||||||
|
"NfcTag": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"uid": { "type": "string" },
|
||||||
|
"name": { "type": "string" },
|
||||||
|
"linked_type": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["audio_file", "playlist"]
|
||||||
|
},
|
||||||
|
"linked_id": { "type": "string", "format": "uuid" }
|
||||||
|
},
|
||||||
|
"required": ["uid", "linked_type", "linked_id"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
software/src/utils/__init__.py
Normal file
15
software/src/utils/__init__.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
from utils.helpers import safe_callback
|
||||||
|
from utils.buttons import Buttons
|
||||||
|
from utils.config import Configuration
|
||||||
|
from utils.leds import LedManager
|
||||||
|
from utils.mbrpartition import MBRPartition
|
||||||
|
from utils.pinindex import get_pin_index
|
||||||
|
from utils.playlistdb import BTreeDB, BTreeFileManager
|
||||||
|
from utils.sdcontext import SDContext
|
||||||
|
from utils.timer import TimerManager
|
||||||
|
|
||||||
|
__all__ = ["BTreeDB", "BTreeFileManager", "Buttons", "Configuration", "get_pin_index", "LedManager", "MBRPartition",
|
||||||
|
"safe_callback", "SDContext", "TimerManager"]
|
||||||
78
software/src/utils/buttons.py
Normal file
78
software/src/utils/buttons.py
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import machine
|
||||||
|
import micropython
|
||||||
|
import time
|
||||||
|
from utils import safe_callback
|
||||||
|
try:
|
||||||
|
from typing import TYPE_CHECKING # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
import typing
|
||||||
|
|
||||||
|
class ButtonCallback(typing.Protocol):
|
||||||
|
def onButtonPressed(self, what: int) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
|
class Buttons:
|
||||||
|
VOLUP = micropython.const(1)
|
||||||
|
VOLDOWN = micropython.const(2)
|
||||||
|
NEXT = micropython.const(3)
|
||||||
|
PREV = micropython.const(4)
|
||||||
|
PLAY_PAUSE = micropython.const(5)
|
||||||
|
KEYMAP = {VOLUP: 'VOLUP',
|
||||||
|
VOLDOWN: 'VOLDOWN',
|
||||||
|
NEXT: 'NEXT',
|
||||||
|
PREV: 'PREV',
|
||||||
|
PLAY_PAUSE: 'PLAY_PAUSE'}
|
||||||
|
|
||||||
|
def __init__(self, cb: "ButtonCallback", config, hwconfig):
|
||||||
|
self.button_map = config.get_button_map()
|
||||||
|
self.hw_buttons = hwconfig.BUTTONS
|
||||||
|
self.cb = cb
|
||||||
|
self.buttons = dict()
|
||||||
|
for key_id, key_name in self.KEYMAP.items():
|
||||||
|
pin = self._get_pin(key_name)
|
||||||
|
if pin is None:
|
||||||
|
continue
|
||||||
|
self.buttons[pin] = key_id
|
||||||
|
self.int_flag = asyncio.ThreadSafeFlag()
|
||||||
|
self.pressed: list[int] = []
|
||||||
|
self.last: dict[int, int] = {}
|
||||||
|
for button in self.buttons.keys():
|
||||||
|
button.irq(handler=self._interrupt, trigger=machine.Pin.IRQ_FALLING | machine.Pin.IRQ_RISING)
|
||||||
|
asyncio.create_task(self.task())
|
||||||
|
|
||||||
|
def _get_pin(self, key):
|
||||||
|
key_id = self.button_map.get(key, None)
|
||||||
|
if key_id is None:
|
||||||
|
return None
|
||||||
|
if key_id < 0 or key_id >= len(self.hw_buttons):
|
||||||
|
return None
|
||||||
|
pin = self.hw_buttons[key_id]
|
||||||
|
if pin is not None:
|
||||||
|
pin.init(machine.Pin.IN, machine.Pin.PULL_UP)
|
||||||
|
return pin
|
||||||
|
|
||||||
|
def _interrupt(self, button):
|
||||||
|
keycode = self.buttons[button]
|
||||||
|
last = self.last.get(keycode, 0)
|
||||||
|
now = time.ticks_ms()
|
||||||
|
self.last[keycode] = now
|
||||||
|
if now - last < 10:
|
||||||
|
# debounce, discard
|
||||||
|
return
|
||||||
|
if button.value() == 0:
|
||||||
|
# print(f'B{keycode} {now}')
|
||||||
|
self.pressed.append(keycode)
|
||||||
|
self.int_flag.set()
|
||||||
|
|
||||||
|
async def task(self):
|
||||||
|
while True:
|
||||||
|
await self.int_flag.wait()
|
||||||
|
while len(self.pressed) > 0:
|
||||||
|
what = self.pressed.pop()
|
||||||
|
safe_callback(lambda: self.cb.onButtonPressed(what), "button callback")
|
||||||
109
software/src/utils/config.py
Normal file
109
software/src/utils/config.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
from errno import ENOENT
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
try:
|
||||||
|
from typing import TYPE_CHECKING, Mapping, Any
|
||||||
|
except ImportError:
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
|
||||||
|
|
||||||
|
class Configuration:
|
||||||
|
DEFAULT_CONFIG = {
|
||||||
|
'LED_COUNT': 1,
|
||||||
|
'IDLE_TIMEOUT_SECS': 60,
|
||||||
|
'TAG_TIMEOUT_SECS': 5,
|
||||||
|
'BUTTON_MAP': {
|
||||||
|
'PLAY_PAUSE': 4,
|
||||||
|
'VOLUP': 0,
|
||||||
|
'VOLDOWN': 2,
|
||||||
|
'PREV': None,
|
||||||
|
'NEXT': 1,
|
||||||
|
},
|
||||||
|
'TAGMODE': 'tagremains'
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, config_path='/config.json'):
|
||||||
|
self.config_path = config_path
|
||||||
|
try:
|
||||||
|
with open(self.config_path, 'r') as conf_file:
|
||||||
|
self.config = json.load(conf_file)
|
||||||
|
self._merge_configs(self.DEFAULT_CONFIG, self.config)
|
||||||
|
except OSError as ex:
|
||||||
|
if ex.errno == ENOENT:
|
||||||
|
self.config = Configuration.DEFAULT_CONFIG
|
||||||
|
self._save()
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
except ValueError as ex:
|
||||||
|
print(f"Warning: Could not load configuration {self.config_path}:\n{ex}")
|
||||||
|
self._move_config_to_backup()
|
||||||
|
self.config = Configuration.DEFAULT_CONFIG
|
||||||
|
|
||||||
|
def _move_config_to_backup(self):
|
||||||
|
# Remove old backup
|
||||||
|
try:
|
||||||
|
os.remove(self.config_path + '.bup')
|
||||||
|
os.rename(self.config_path, self.config_path + '.bup')
|
||||||
|
except OSError as ex:
|
||||||
|
if ex.errno != ENOENT:
|
||||||
|
raise
|
||||||
|
os.sync()
|
||||||
|
|
||||||
|
def _merge_configs(self, default, config):
|
||||||
|
for k in default.keys():
|
||||||
|
if k not in config:
|
||||||
|
if isinstance(default[k], dict):
|
||||||
|
config[k] = default[k].copy()
|
||||||
|
else:
|
||||||
|
config[k] = default[k]
|
||||||
|
elif isinstance(default[k], dict):
|
||||||
|
self._merge_configs(default[k], config[k])
|
||||||
|
|
||||||
|
def _save(self):
|
||||||
|
with open(self.config_path + '.new', 'w') as conf_file:
|
||||||
|
json.dump(self.config, conf_file)
|
||||||
|
self._move_config_to_backup()
|
||||||
|
os.rename(self.config_path + '.new', self.config_path)
|
||||||
|
os.sync()
|
||||||
|
|
||||||
|
def _get(self, key):
|
||||||
|
return self.config[key]
|
||||||
|
|
||||||
|
def get_led_count(self) -> int:
|
||||||
|
return self._get('LED_COUNT')
|
||||||
|
|
||||||
|
def get_idle_timeout(self) -> int:
|
||||||
|
return self._get('IDLE_TIMEOUT_SECS')
|
||||||
|
|
||||||
|
def get_tag_timeout(self) -> int:
|
||||||
|
return self._get('TAG_TIMEOUT_SECS')
|
||||||
|
|
||||||
|
def get_button_map(self) -> Mapping[str, int | None]:
|
||||||
|
return self._get('BUTTON_MAP')
|
||||||
|
|
||||||
|
def get_tagmode(self) -> str:
|
||||||
|
return self._get('TAGMODE')
|
||||||
|
|
||||||
|
# For the web API
|
||||||
|
def get_config(self) -> Mapping[str, Any]:
|
||||||
|
return self.config
|
||||||
|
|
||||||
|
def _validate(self, default, config, path=''):
|
||||||
|
for k in config.keys():
|
||||||
|
if k not in default:
|
||||||
|
raise ValueError(f'Invalid config key {path}/{k}')
|
||||||
|
if isinstance(default[k], dict):
|
||||||
|
if not isinstance(config[k], dict):
|
||||||
|
raise ValueError(f'Invalid config: Value of {path}/{k} must be mapping')
|
||||||
|
self._validate(default[k], config[k], f'{path}/{k}')
|
||||||
|
|
||||||
|
def set_config(self, config):
|
||||||
|
self._validate(self.DEFAULT_CONFIG, config)
|
||||||
|
if 'TAGMODE' in config and config['TAGMODE'] not in ['tagremains', 'tagstartstop']:
|
||||||
|
raise ValueError("Invalid TAGMODE: Must be 'tagremains' or 'tagstartstop'")
|
||||||
|
self._merge_configs(self.config, config)
|
||||||
|
self.config = config
|
||||||
|
self._save()
|
||||||
12
software/src/utils/helpers.py
Normal file
12
software/src/utils/helpers.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def safe_callback(func, name="callback"):
|
||||||
|
try:
|
||||||
|
func()
|
||||||
|
except Exception as ex:
|
||||||
|
print(f"Uncaught exception in {name}")
|
||||||
|
sys.print_exception(ex)
|
||||||
58
software/src/utils/leds.py
Normal file
58
software/src/utils/leds.py
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from math import sin, pi
|
||||||
|
from micropython import const
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
class LedManager:
|
||||||
|
IDLE = const(0)
|
||||||
|
PLAYING = const(1)
|
||||||
|
|
||||||
|
def __init__(self, np):
|
||||||
|
self.led_state = LedManager.IDLE
|
||||||
|
self.np = np
|
||||||
|
self.brightness = 0.1
|
||||||
|
self.leds = len(self.np)
|
||||||
|
asyncio.create_task(self.run())
|
||||||
|
|
||||||
|
def set_state(self, state):
|
||||||
|
assert state in [LedManager.IDLE, LedManager.PLAYING]
|
||||||
|
self.led_state = state
|
||||||
|
|
||||||
|
def _gamma(self, value, X=2.2):
|
||||||
|
result = min(max(int(self.brightness * pow(value / 255.0, X) * 255.0 + 0.5), 0), 255)
|
||||||
|
if value > 0:
|
||||||
|
result = max(1, result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _rainbow(self, time):
|
||||||
|
for i in range(self.leds):
|
||||||
|
ofs = (time * self.leds + i) % self.leds
|
||||||
|
self.np[i] = (self._gamma((sin(ofs / self.leds * 2 * pi) + 1) * 127),
|
||||||
|
self._gamma((sin(ofs / self.leds * 2 * pi + 2/3*pi) + 1) * 127),
|
||||||
|
self._gamma((sin(ofs / self.leds * 2 * pi + 4/3*pi) + 1) * 127))
|
||||||
|
|
||||||
|
def _pulse(self, time, color, speed):
|
||||||
|
scaled_sin = max(1, abs(sin(time / speed * 2 * pi)) * 255)
|
||||||
|
val = (self._gamma(color[0]*scaled_sin),
|
||||||
|
self._gamma(color[1]*scaled_sin),
|
||||||
|
self._gamma(color[2]*scaled_sin))
|
||||||
|
for i in range(self.leds):
|
||||||
|
self.np[i] = val
|
||||||
|
|
||||||
|
async def run(self):
|
||||||
|
time_ = 0.0
|
||||||
|
while True:
|
||||||
|
if self.led_state == LedManager.IDLE:
|
||||||
|
self._pulse(time_, (0, 1, 0), 3)
|
||||||
|
elif self.led_state == LedManager.PLAYING:
|
||||||
|
self._rainbow(time_)
|
||||||
|
time_ += 0.02
|
||||||
|
before = time.ticks_ms()
|
||||||
|
await self.np.async_write()
|
||||||
|
now = time.ticks_ms()
|
||||||
|
if before + 20 > now:
|
||||||
|
await asyncio.sleep_ms(20 - (now - before))
|
||||||
46
software/src/utils/mbrpartition.py
Normal file
46
software/src/utils/mbrpartition.py
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
from array import array
|
||||||
|
import struct
|
||||||
|
|
||||||
|
|
||||||
|
class MBRPartition:
|
||||||
|
def __init__(self, bdev, partno):
|
||||||
|
assert partno >= 0 and partno < 4
|
||||||
|
self.bdev = bdev
|
||||||
|
bdev_len = bdev.ioctl(4, None)
|
||||||
|
bdev_bs = bdev.ioctl(5, None)
|
||||||
|
assert bdev_bs == 512
|
||||||
|
mbr = array('B', 512*b'0')
|
||||||
|
bdev.readblocks(0, mbr)
|
||||||
|
if mbr[510] != 0x55 or mbr[511] != 0xaa:
|
||||||
|
raise ValueError("Not a valid MBR")
|
||||||
|
partofs = 0x1be + partno*16
|
||||||
|
(boot_ind, _, _, _,
|
||||||
|
parttype, _, _, _,
|
||||||
|
lba_start, lba_len) = struct.unpack_from('<BBBBBBBBLL', mbr, partofs)
|
||||||
|
print(f'Partition {partno} bi {boot_ind} type {parttype} start {lba_start} len {lba_len}')
|
||||||
|
if (boot_ind != 0x00 and boot_ind != 0x80) or parttype == 0x00:
|
||||||
|
raise ValueError("Not a valid partition")
|
||||||
|
self.offset = lba_start
|
||||||
|
self.size = lba_len
|
||||||
|
assert lba_start + lba_len <= bdev_len
|
||||||
|
|
||||||
|
def ioctl(self, op, arg):
|
||||||
|
if op == 4:
|
||||||
|
return self.size
|
||||||
|
elif op == 5:
|
||||||
|
return 512
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def readblocks(self, block, buf):
|
||||||
|
if block >= self.size:
|
||||||
|
raise ValueError("Block out of range")
|
||||||
|
return self.bdev.readblocks(block+self.offset, buf)
|
||||||
|
|
||||||
|
def writeblocks(self, block, buf):
|
||||||
|
if block >= self.size:
|
||||||
|
raise ValueError("Block out of range")
|
||||||
|
return self.bdev.writeblocks(block+self.offset, buf)
|
||||||
43
software/src/utils/pinindex.py
Normal file
43
software/src/utils/pinindex.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
from machine import Pin
|
||||||
|
|
||||||
|
pins = [Pin.board.GP0,
|
||||||
|
Pin.board.GP1,
|
||||||
|
Pin.board.GP2,
|
||||||
|
Pin.board.GP3,
|
||||||
|
Pin.board.GP4,
|
||||||
|
Pin.board.GP5,
|
||||||
|
Pin.board.GP6,
|
||||||
|
Pin.board.GP7,
|
||||||
|
Pin.board.GP8,
|
||||||
|
Pin.board.GP9,
|
||||||
|
Pin.board.GP10,
|
||||||
|
Pin.board.GP11,
|
||||||
|
Pin.board.GP12,
|
||||||
|
Pin.board.GP13,
|
||||||
|
Pin.board.GP14,
|
||||||
|
Pin.board.GP15,
|
||||||
|
Pin.board.GP16,
|
||||||
|
Pin.board.GP17,
|
||||||
|
Pin.board.GP18,
|
||||||
|
Pin.board.GP19,
|
||||||
|
Pin.board.GP20,
|
||||||
|
Pin.board.GP21,
|
||||||
|
Pin.board.GP22,
|
||||||
|
None, # 23
|
||||||
|
None, # 24
|
||||||
|
None, # 25
|
||||||
|
Pin.board.GP26,
|
||||||
|
Pin.board.GP27,
|
||||||
|
Pin.board.GP28,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_pin_index(pin: Pin) -> int:
|
||||||
|
"""
|
||||||
|
Get the pin index back from a pin object.
|
||||||
|
Unfortunately, micropython has no built-in function for this.
|
||||||
|
"""
|
||||||
|
return pins.index(pin)
|
||||||
388
software/src/utils/playlistdb.py
Normal file
388
software/src/utils/playlistdb.py
Normal file
@@ -0,0 +1,388 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
import btree
|
||||||
|
import random
|
||||||
|
import time
|
||||||
|
try:
|
||||||
|
import typing
|
||||||
|
from typing import TYPE_CHECKING, Iterable # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
TYPE_CHECKING = False
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
class IPlaylist(typing.Protocol):
|
||||||
|
def getPaths(self) -> Iterable[bytes]: ...
|
||||||
|
def getCurrentPath(self) -> bytes: ...
|
||||||
|
def getNextPath(self) -> bytes | None: ...
|
||||||
|
|
||||||
|
class IPlaylistDB(typing.Protocol):
|
||||||
|
def getPlaylistForTag(self, tag: bytes) -> IPlaylist: ...
|
||||||
|
else:
|
||||||
|
class IPlaylistDB(object):
|
||||||
|
...
|
||||||
|
|
||||||
|
class IPlaylist(object):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class BTreeDB(IPlaylistDB):
|
||||||
|
SHUFFLE_NO = b'no'
|
||||||
|
SHUFFLE_YES = b'yes'
|
||||||
|
PERSIST_NO = b'no'
|
||||||
|
PERSIST_TRACK = b'track'
|
||||||
|
PERSIST_OFFSET = b'offset'
|
||||||
|
|
||||||
|
class Playlist(IPlaylist):
|
||||||
|
def __init__(self, parent: "BTreeDB", tag: bytes, pos: int, persist, shuffle):
|
||||||
|
self.parent = parent
|
||||||
|
self.tag = tag
|
||||||
|
self.pos = pos
|
||||||
|
self.persist = persist
|
||||||
|
self.shuffle = shuffle
|
||||||
|
self.length = self.parent._getPlaylistLength(self.tag)
|
||||||
|
self._shuffle()
|
||||||
|
|
||||||
|
def _getPlaylistPos(self):
|
||||||
|
"""
|
||||||
|
Gets the position to pass to parent._getPlaylistEntry etc.
|
||||||
|
"""
|
||||||
|
if self.shuffle == BTreeDB.SHUFFLE_YES:
|
||||||
|
return self.shuffle_order[self.pos]
|
||||||
|
else:
|
||||||
|
return self.pos
|
||||||
|
|
||||||
|
def _shuffle(self, reshuffle=False):
|
||||||
|
if self.shuffle == BTreeDB.SHUFFLE_NO:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.shuffle_seed = None
|
||||||
|
# Try to get seed from DB if persisted
|
||||||
|
if self.persist != BTreeDB.PERSIST_NO and not reshuffle:
|
||||||
|
self.shuffle_seed = self.parent._getPlaylistShuffleSeed(self.tag)
|
||||||
|
if self.shuffle_seed is None:
|
||||||
|
# Either not persisted or could not read from db
|
||||||
|
self.shuffle_seed = time.ticks_cpu()
|
||||||
|
if self.persist != BTreeDB.PERSIST_NO:
|
||||||
|
self.parent._setPlaylistShuffleSeed(self.tag, self.shuffle_seed)
|
||||||
|
# TODO: Find an algorithm for shuffling that does not use O(n) memory for playlist of length n
|
||||||
|
random.seed(self.shuffle_seed)
|
||||||
|
entries = list(range(0, self.length))
|
||||||
|
# We don't have random.shuffle in micropython, so emulate it with random.choice
|
||||||
|
self.shuffle_order = []
|
||||||
|
while len(entries) > 0:
|
||||||
|
chosen = random.choice(entries)
|
||||||
|
self.shuffle_order.append(chosen)
|
||||||
|
entries.remove(chosen)
|
||||||
|
|
||||||
|
def getPaths(self):
|
||||||
|
"""
|
||||||
|
Get entire playlist in storage order
|
||||||
|
"""
|
||||||
|
return self.parent._getPlaylistValueIterator(self.tag)
|
||||||
|
|
||||||
|
def getCurrentPath(self):
|
||||||
|
"""
|
||||||
|
Get path of file that should be played.
|
||||||
|
"""
|
||||||
|
return self.parent._getPlaylistEntry(self.tag, self._getPlaylistPos())
|
||||||
|
|
||||||
|
def getNextPath(self):
|
||||||
|
"""
|
||||||
|
Select next track and return path.
|
||||||
|
"""
|
||||||
|
if self.pos + 1 >= self.length:
|
||||||
|
self.pos = 0
|
||||||
|
if self.persist != BTreeDB.PERSIST_NO:
|
||||||
|
self.parent._setPlaylistPos(self.tag, self.pos)
|
||||||
|
self.setPlaybackOffset(0)
|
||||||
|
self._shuffle(True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
self.pos += 1
|
||||||
|
if self.persist != BTreeDB.PERSIST_NO:
|
||||||
|
self.parent._setPlaylistPos(self.tag, self.pos)
|
||||||
|
self.setPlaybackOffset(0)
|
||||||
|
return self.getCurrentPath()
|
||||||
|
|
||||||
|
def getPrevPath(self):
|
||||||
|
"""
|
||||||
|
Select prev track and return path.
|
||||||
|
"""
|
||||||
|
if self.pos > 0:
|
||||||
|
self.pos -= 1
|
||||||
|
if self.persist != BTreeDB.PERSIST_NO:
|
||||||
|
self.parent._setPlaylistPos(self.tag, self.pos)
|
||||||
|
self.setPlaybackOffset(0)
|
||||||
|
return self.getCurrentPath()
|
||||||
|
|
||||||
|
def setPlaybackOffset(self, offset):
|
||||||
|
"""
|
||||||
|
Store the current position in the track for PERSIST_OFFSET mode
|
||||||
|
"""
|
||||||
|
if self.persist != BTreeDB.PERSIST_OFFSET:
|
||||||
|
return
|
||||||
|
self.parent._setPlaylistPosOffset(self.tag, offset)
|
||||||
|
|
||||||
|
def getPlaybackOffset(self):
|
||||||
|
"""
|
||||||
|
Get the current position in the track for PERSIST_OFFSET mode
|
||||||
|
"""
|
||||||
|
if self.persist != BTreeDB.PERSIST_OFFSET:
|
||||||
|
return 0
|
||||||
|
return self.parent._getPlaylistPosOffset(self.tag)
|
||||||
|
|
||||||
|
def __init__(self, db: btree.BTree, flush_func: typing.Callable | None = None):
|
||||||
|
self.db = db
|
||||||
|
self.flush_func = flush_func
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _keyPlaylistPos(tag):
|
||||||
|
return b''.join([tag, b'/playlistpos'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _keyPlaylistPosOffset(tag):
|
||||||
|
return b''.join([tag, b'/playlistposoffset'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _keyPlaylistShuffle(tag):
|
||||||
|
return b''.join([tag, b'/playlistshuffle'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _keyPlaylistShuffleSeed(tag):
|
||||||
|
return b''.join([tag, b'/playlistshuffleseed'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _keyPlaylistPersist(tag):
|
||||||
|
return b''.join([tag, b'/playlistpersist'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _keyPlaylistEntry(tag, pos):
|
||||||
|
return b''.join([tag, b'/playlist/', '{:05}'.format(pos).encode()])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _keyPlaylistStart(tag):
|
||||||
|
return b''.join([tag, b'/playlist/'])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _keyPlaylistStartEnd(tag):
|
||||||
|
return (b''.join([tag, b'/playlist/']),
|
||||||
|
b''.join([tag, b'/playlist0']))
|
||||||
|
|
||||||
|
def _flush(self):
|
||||||
|
"""
|
||||||
|
Flush the database and call the flush_func if it was provided.
|
||||||
|
"""
|
||||||
|
self.db.flush()
|
||||||
|
if self.flush_func is not None:
|
||||||
|
self.flush_func()
|
||||||
|
|
||||||
|
def _getPlaylistValueIterator(self, tag: bytes):
|
||||||
|
start, end = self._keyPlaylistStartEnd(tag)
|
||||||
|
return self.db.values(start, end)
|
||||||
|
|
||||||
|
def _getPlaylistEntry(self, tag: bytes, pos: int) -> bytes:
|
||||||
|
return self.db[self._keyPlaylistEntry(tag, pos)]
|
||||||
|
|
||||||
|
def _setPlaylistPos(self, tag: bytes, pos: int, flush=True):
|
||||||
|
self.db[self._keyPlaylistPos(tag)] = str(pos).encode()
|
||||||
|
if flush:
|
||||||
|
self._flush()
|
||||||
|
|
||||||
|
def _setPlaylistPosOffset(self, tag: bytes, offset: int, flush=True):
|
||||||
|
self.db[self._keyPlaylistPosOffset(tag)] = str(offset).encode()
|
||||||
|
if flush:
|
||||||
|
self._flush()
|
||||||
|
|
||||||
|
def _getPlaylistPosOffset(self, tag: bytes) -> int:
|
||||||
|
return int(self.db.get(self._keyPlaylistPosOffset(tag), b'0'))
|
||||||
|
|
||||||
|
def _getPlaylistShuffleSeed(self, tag: bytes) -> int | None:
|
||||||
|
try:
|
||||||
|
return int(self.db[self._keyPlaylistShuffleSeed(tag)])
|
||||||
|
except (ValueError, KeyError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _setPlaylistShuffleSeed(self, tag, seed: int, flush=True):
|
||||||
|
self.db[self._keyPlaylistShuffleSeed(tag)] = str(seed).encode()
|
||||||
|
if flush:
|
||||||
|
self._flush()
|
||||||
|
|
||||||
|
def _getPlaylistLength(self, tag: bytes) -> int:
|
||||||
|
start, end = self._keyPlaylistStartEnd(tag)
|
||||||
|
for k in self.db.keys(end, start, btree.DESC):
|
||||||
|
# There is a bug in btreedb that causes an additional key after 'end' to be returned when iterating in
|
||||||
|
# descending order
|
||||||
|
# Check for this and skip it if needed
|
||||||
|
elements = k.split(b'/')
|
||||||
|
if len(elements) >= 2 and elements[1] == b'playlist':
|
||||||
|
last = k
|
||||||
|
break
|
||||||
|
elements = last.split(b'/')
|
||||||
|
if len(elements) != 3:
|
||||||
|
raise RuntimeError("Malformed playlist key")
|
||||||
|
return int(elements[2])+1
|
||||||
|
|
||||||
|
def _savePlaylist(self, tag, entries, persist, shuffle, flush=True):
|
||||||
|
self._deletePlaylist(tag, False)
|
||||||
|
for idx, entry in enumerate(entries):
|
||||||
|
self.db[self._keyPlaylistEntry(tag, idx)] = entry
|
||||||
|
self.db[self._keyPlaylistPersist(tag)] = persist
|
||||||
|
self.db[self._keyPlaylistShuffle(tag)] = shuffle
|
||||||
|
if flush:
|
||||||
|
self._flush()
|
||||||
|
|
||||||
|
def _deletePlaylist(self, tag, flush=True):
|
||||||
|
start_key, end_key = self._keyPlaylistStartEnd(tag)
|
||||||
|
for k in self.db.keys(start_key, end_key):
|
||||||
|
try:
|
||||||
|
del self.db[k]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
for k in (self._keyPlaylistPos(tag), self._keyPlaylistPosOffset(tag),
|
||||||
|
self._keyPlaylistPersist(tag), self._keyPlaylistShuffle(tag),
|
||||||
|
self._keyPlaylistShuffleSeed(tag)):
|
||||||
|
try:
|
||||||
|
del self.db[k]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
if flush:
|
||||||
|
self._flush()
|
||||||
|
|
||||||
|
def getPlaylistForTag(self, tag: bytes):
|
||||||
|
"""
|
||||||
|
Lookup the playlist for 'tag' and return the Playlist object. Return None if no playlist exists for the given
|
||||||
|
tag.
|
||||||
|
"""
|
||||||
|
persist = self.db.get(self._keyPlaylistPersist(tag), self.PERSIST_TRACK)
|
||||||
|
pos = 0
|
||||||
|
if persist != self.PERSIST_NO and self._keyPlaylistPos(tag) in self.db:
|
||||||
|
try:
|
||||||
|
pos = int(self.db[self._keyPlaylistPos(tag)])
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
if self._keyPlaylistEntry(tag, 0) not in self.db:
|
||||||
|
# Empty playlist
|
||||||
|
return None
|
||||||
|
if self._keyPlaylistEntry(tag, pos) not in self.db:
|
||||||
|
pos = 0
|
||||||
|
shuffle = self.db.get(self._keyPlaylistShuffle(tag), self.SHUFFLE_NO)
|
||||||
|
return self.Playlist(self, tag, pos, persist, shuffle)
|
||||||
|
|
||||||
|
def createPlaylistForTag(self, tag: bytes, entries: typing.Iterable[bytes], persist=PERSIST_TRACK,
|
||||||
|
shuffle=SHUFFLE_NO):
|
||||||
|
"""
|
||||||
|
Create and save a playlist for 'tag' and return the Playlist object. If a playlist already existed for 'tag' it
|
||||||
|
is overwritten.
|
||||||
|
"""
|
||||||
|
assert persist in (self.PERSIST_NO, self.PERSIST_TRACK, self.PERSIST_OFFSET)
|
||||||
|
assert shuffle in (self.SHUFFLE_NO, self.SHUFFLE_YES)
|
||||||
|
self._savePlaylist(tag, entries, persist, shuffle)
|
||||||
|
return self.getPlaylistForTag(tag)
|
||||||
|
|
||||||
|
def validate(self, dump=False):
|
||||||
|
"""
|
||||||
|
Validate the structure of the playlist database.
|
||||||
|
"""
|
||||||
|
result = True
|
||||||
|
|
||||||
|
def fail(msg):
|
||||||
|
nonlocal result
|
||||||
|
print(msg)
|
||||||
|
result = False
|
||||||
|
|
||||||
|
last_tag = None
|
||||||
|
last_pos = None
|
||||||
|
for k in self.db.keys():
|
||||||
|
fields = k.split(b'/')
|
||||||
|
if len(fields) <= 1:
|
||||||
|
fail(f'Malformed key {k!r}')
|
||||||
|
continue
|
||||||
|
if fields[0] == b'settings':
|
||||||
|
# Legacy, not used any more
|
||||||
|
continue
|
||||||
|
if last_tag != fields[0]:
|
||||||
|
last_tag = fields[0]
|
||||||
|
last_pos = None
|
||||||
|
if dump:
|
||||||
|
print(f'Tag {fields[0]}')
|
||||||
|
if fields[1] == b'playlist':
|
||||||
|
if len(fields) != 3:
|
||||||
|
fail(f'Malformed playlist entry: {k!r}')
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
idx = int(fields[2])
|
||||||
|
except ValueError:
|
||||||
|
fail(f'Malformed playlist entry: {k!r}')
|
||||||
|
continue
|
||||||
|
if len(fields[2]) != 5:
|
||||||
|
fail(f'Bad index width for {last_tag} at {idx}')
|
||||||
|
if (last_pos is not None and last_pos + 1 != idx) or \
|
||||||
|
(last_pos is None and idx != 0):
|
||||||
|
fail(f'Bad playlist entry sequence for {last_tag} at {idx}')
|
||||||
|
last_pos = idx
|
||||||
|
if dump:
|
||||||
|
print(f'\tTrack {idx}: {self.db[k]!r}')
|
||||||
|
elif fields[1] == b'playlistpos':
|
||||||
|
val = self.db[k]
|
||||||
|
try:
|
||||||
|
idx = int(val)
|
||||||
|
except ValueError:
|
||||||
|
fail(f'Malformed playlist position: {val!r}')
|
||||||
|
continue
|
||||||
|
if 0 > idx or idx > last_pos:
|
||||||
|
fail(f'Playlist position out of range for {last_tag}: {idx}')
|
||||||
|
elif dump:
|
||||||
|
print(f'\tPosition {idx}')
|
||||||
|
elif fields[1] == b'playlistshuffle':
|
||||||
|
val = self.db[k]
|
||||||
|
if val not in (b'no', b'yes'):
|
||||||
|
fail(f'Bad playlistshuffle value for {last_tag}: {val!r}')
|
||||||
|
if dump and val == b'yes':
|
||||||
|
print('\tShuffle')
|
||||||
|
elif fields[1] == b'playlistpersist':
|
||||||
|
val = self.db[k]
|
||||||
|
if val not in (b'no', b'track', b'offset'):
|
||||||
|
fail(f'Bad playlistpersist value for {last_tag}: {val!r}')
|
||||||
|
elif dump:
|
||||||
|
print(f'\tPersist: {val.decode()}')
|
||||||
|
elif fields[1] == b'playlistshuffleseed':
|
||||||
|
val = self.db[k]
|
||||||
|
try:
|
||||||
|
_ = int(val)
|
||||||
|
except ValueError:
|
||||||
|
fail(f' Bad playlistshuffleseed value for {last_tag}: {val!r}')
|
||||||
|
elif fields[1] == b'playlistposoffset':
|
||||||
|
val = self.db[k]
|
||||||
|
try:
|
||||||
|
_ = int(val)
|
||||||
|
except ValueError:
|
||||||
|
fail(f' Bad playlistposoffset value for {last_tag}: {val!r}')
|
||||||
|
else:
|
||||||
|
fail(f'Unknown key {k!r}')
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class BTreeFileManager:
|
||||||
|
"""
|
||||||
|
Context manager for a BTreeDB playlist db backed by a file in the filesystem.
|
||||||
|
"""
|
||||||
|
def __init__(self, db_path: str | bytes):
|
||||||
|
self.db_path = db_path
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
try:
|
||||||
|
self.db_file = open(self.db_path, 'r+b')
|
||||||
|
except OSError:
|
||||||
|
self.db_file = open(self.db_path, 'w+b')
|
||||||
|
try:
|
||||||
|
self.db = btree.open(self.db_file, pagesize=512, cachesize=1024)
|
||||||
|
btdb = BTreeDB(self.db, lambda: self.db_file.flush())
|
||||||
|
btdb.validate(True) # while testing, validate and dump DB on startup
|
||||||
|
return btdb
|
||||||
|
except Exception:
|
||||||
|
self.db_file.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
self.db.close()
|
||||||
|
self.db_file.close()
|
||||||
39
software/src/utils/sdcontext.py
Normal file
39
software/src/utils/sdcontext.py
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from . import MBRPartition
|
||||||
|
from rp2_sd import SDCard
|
||||||
|
|
||||||
|
|
||||||
|
class SDContext:
|
||||||
|
def __init__(self, mosi, miso, sck, ss, baudrate):
|
||||||
|
self.mosi = mosi
|
||||||
|
self.miso = miso
|
||||||
|
self.sck = sck
|
||||||
|
self.ss = ss
|
||||||
|
self.baudrate = baudrate
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.sdcard = SDCard(self.mosi, self.miso, self.sck, self.ss, self.baudrate)
|
||||||
|
# Try first partition
|
||||||
|
try:
|
||||||
|
self.part = MBRPartition(self.sdcard, 0)
|
||||||
|
os.mount(self.part, '/sd')
|
||||||
|
return self
|
||||||
|
except Exception:
|
||||||
|
print("Failed to mount SDCard partition, trying whole device...")
|
||||||
|
# Try whole device
|
||||||
|
try:
|
||||||
|
os.mount(self.sdcard, '/sd')
|
||||||
|
return self
|
||||||
|
except Exception as ex:
|
||||||
|
self.sdcard.deinit()
|
||||||
|
raise RuntimeError("Could not mount SD card") from ex
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
|
try:
|
||||||
|
os.umount('/sd')
|
||||||
|
finally:
|
||||||
|
self.sdcard.deinit()
|
||||||
85
software/src/utils/timer.py
Normal file
85
software/src/utils/timer.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import heapq
|
||||||
|
import time
|
||||||
|
from utils import safe_callback
|
||||||
|
|
||||||
|
TIMER_DEBUG = True
|
||||||
|
|
||||||
|
|
||||||
|
class TimerManager(object):
|
||||||
|
_instance = None
|
||||||
|
|
||||||
|
def __new__(cls):
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super(TimerManager, cls).__new__(cls)
|
||||||
|
cls._instance.timers = []
|
||||||
|
cls._instance.timer_debug = TIMER_DEBUG
|
||||||
|
cls._instance.task = asyncio.create_task(cls._instance._timer_worker())
|
||||||
|
cls._instance.worker_event = asyncio.Event()
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def schedule(self, when, what):
|
||||||
|
cur_nearest = self.timers[0][0] if len(self.timers) > 0 else None
|
||||||
|
self._remove_timer(what) # Ensure timer is not already scheduled
|
||||||
|
heapq.heappush(self.timers, (when, what))
|
||||||
|
if cur_nearest is None or cur_nearest > self.timers[0][0]:
|
||||||
|
# New timer is closer than previous closest timer
|
||||||
|
if self.timer_debug:
|
||||||
|
print(f'cur_nearest: {cur_nearest}, new next: {self.timers[0][0]}')
|
||||||
|
print("schedule: wake")
|
||||||
|
self.worker_event.set()
|
||||||
|
|
||||||
|
def cancel(self, what):
|
||||||
|
remove_idx = self._remove_timer(what)
|
||||||
|
if remove_idx == 0:
|
||||||
|
# Cancel timer was closest timer
|
||||||
|
if self.timer_debug:
|
||||||
|
print("cancel: wake")
|
||||||
|
self.worker_event.set()
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _remove_timer(self, what):
|
||||||
|
try:
|
||||||
|
(when, _), i = next(filter(lambda item: item[0][1] == what, zip(self.timers, range(len(self.timers)))))
|
||||||
|
except StopIteration:
|
||||||
|
return False
|
||||||
|
del self.timers[i]
|
||||||
|
heapq.heapify(self.timers)
|
||||||
|
return i
|
||||||
|
|
||||||
|
def _next_timeout(self):
|
||||||
|
if len(self.timers) == 0:
|
||||||
|
if self.timer_debug:
|
||||||
|
print("timer: worker: queue empty")
|
||||||
|
return None
|
||||||
|
cur_nearest = self.timers[0][0]
|
||||||
|
next_timeout = cur_nearest - time.ticks_ms()
|
||||||
|
if self.timer_debug:
|
||||||
|
if next_timeout > 0:
|
||||||
|
print(f"timer: worker: next is {self.timers[0]}, sleep {next_timeout} ms")
|
||||||
|
else:
|
||||||
|
print(f"timer: worker: {self.timers[0]} elapsed @{cur_nearest}, delay {-next_timeout} ms")
|
||||||
|
return next_timeout
|
||||||
|
|
||||||
|
async def _wait(self, timeout):
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for_ms(self.worker_event.wait(), timeout)
|
||||||
|
if self.timer_debug:
|
||||||
|
print("timer: worker: event")
|
||||||
|
# got woken up due to event
|
||||||
|
self.worker_event.clear()
|
||||||
|
return True
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def _timer_worker(self):
|
||||||
|
while True:
|
||||||
|
next_timeout = self._next_timeout()
|
||||||
|
if next_timeout is None or next_timeout > 0:
|
||||||
|
await self._wait(next_timeout)
|
||||||
|
else:
|
||||||
|
_, callback = heapq.heappop(self.timers)
|
||||||
|
safe_callback(callback, "timer callback")
|
||||||
84
software/src/webserver.py
Normal file
84
software/src/webserver.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
'''
|
||||||
|
SPDX-License-Identifier: MIT
|
||||||
|
Copyright (c) 2024-2025 Stefan Kratochwil <Kratochwil-LA@gmx.de>
|
||||||
|
'''
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from microdot import Microdot, redirect, send_file
|
||||||
|
|
||||||
|
webapp = Microdot()
|
||||||
|
server = None
|
||||||
|
config = None
|
||||||
|
app = None
|
||||||
|
|
||||||
|
|
||||||
|
def start_webserver(config_, app_):
|
||||||
|
global server, config, app
|
||||||
|
server = asyncio.create_task(webapp.start_server(port=80))
|
||||||
|
config = config_
|
||||||
|
app = app_
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.before_request
|
||||||
|
async def before_request_handler(request):
|
||||||
|
if request.method in ['PUT', 'POST'] and app.is_playing():
|
||||||
|
return "Cannot write to device while playback is active", 503
|
||||||
|
app.reset_idle_timeout()
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route('/api/v1/hello')
|
||||||
|
async def index(request):
|
||||||
|
print("wohoo, a guest :)")
|
||||||
|
print(f" app: {request.app}")
|
||||||
|
print(f" client: {request.client_addr}")
|
||||||
|
print(f" method: {request.method}")
|
||||||
|
print(f" url: {request.url}")
|
||||||
|
print(f" headers: {request.headers}")
|
||||||
|
print(f" cookies: {request.cookies}")
|
||||||
|
return "TonberryPico says 'Hello World!'"
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route('/api/v1/filesystem', methods=['POST'])
|
||||||
|
async def filesystem_post(request):
|
||||||
|
# curl -X POST -d "burp" http://192.168.4.1/api/v1/filesystem
|
||||||
|
print(request)
|
||||||
|
return {'success': False}
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route('/api/v1/playlist', methods=['POST'])
|
||||||
|
async def playlist_post(request):
|
||||||
|
print(request)
|
||||||
|
return {'success': False}
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route('/api/v1/config', methods=['GET'])
|
||||||
|
async def config_get(request):
|
||||||
|
return config.get_config()
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route('/api/v1/config', methods=['PUT'])
|
||||||
|
async def config_put(request):
|
||||||
|
try:
|
||||||
|
config.set_config(request.json)
|
||||||
|
except ValueError as ex:
|
||||||
|
return str(ex), 400
|
||||||
|
return '', 204
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route('/', methods=['GET'])
|
||||||
|
async def root_get(request):
|
||||||
|
return redirect('/index.html')
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route('/index.html', methods=['GET'])
|
||||||
|
async def index_get(request):
|
||||||
|
return send_file('/frontend/index.html.gz', content_type='text/html', compressed='gzip')
|
||||||
|
|
||||||
|
|
||||||
|
@webapp.route('/static/<path:path>', methods=['GET'])
|
||||||
|
async def static(request, path):
|
||||||
|
if '..' in path:
|
||||||
|
# directory traversal is not allowed
|
||||||
|
return 'Not found', 404
|
||||||
|
return send_file('/frontend/static/' + path, max_age=86400)
|
||||||
25
software/tests/mocks/btree.py
Normal file
25
software/tests/mocks/btree.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
from typing import Iterable
|
||||||
|
|
||||||
|
|
||||||
|
class BTree:
|
||||||
|
def close(self): ...
|
||||||
|
|
||||||
|
def values(self, start_key: str | bytes, end_key: str | bytes | None = None, flags=None) -> Iterable[str | bytes]:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __setitem__(self, key: str | bytes, val: str | bytes): ...
|
||||||
|
|
||||||
|
def flush(self): ...
|
||||||
|
|
||||||
|
def get(self, key: str | bytes, default: str | bytes | None = None) -> str | bytes: ...
|
||||||
|
|
||||||
|
|
||||||
|
def open(dbfile) -> BTree:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
DESC = 1
|
||||||
|
INCL = 2
|
||||||
40
software/tests/mocks/machine.py
Normal file
40
software/tests/mocks/machine.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
class Pin:
|
||||||
|
def __init__(self, idx):
|
||||||
|
self.idx = idx
|
||||||
|
|
||||||
|
board = None
|
||||||
|
|
||||||
|
|
||||||
|
class Board:
|
||||||
|
GP0 = Pin(0)
|
||||||
|
GP1 = Pin(1)
|
||||||
|
GP2 = Pin(2)
|
||||||
|
GP3 = Pin(3)
|
||||||
|
GP4 = Pin(4)
|
||||||
|
GP5 = Pin(5)
|
||||||
|
GP6 = Pin(6)
|
||||||
|
GP7 = Pin(7)
|
||||||
|
GP8 = Pin(8)
|
||||||
|
GP9 = Pin(9)
|
||||||
|
GP10 = Pin(10)
|
||||||
|
GP11 = Pin(11)
|
||||||
|
GP12 = Pin(12)
|
||||||
|
GP13 = Pin(13)
|
||||||
|
GP14 = Pin(14)
|
||||||
|
GP15 = Pin(15)
|
||||||
|
GP16 = Pin(16)
|
||||||
|
GP17 = Pin(17)
|
||||||
|
GP18 = Pin(18)
|
||||||
|
GP19 = Pin(19)
|
||||||
|
GP20 = Pin(20)
|
||||||
|
GP21 = Pin(21)
|
||||||
|
GP22 = Pin(22)
|
||||||
|
GP26 = Pin(26)
|
||||||
|
GP27 = Pin(27)
|
||||||
|
GP28 = Pin(28)
|
||||||
|
|
||||||
|
|
||||||
|
Pin.board = Board
|
||||||
5
software/tests/mocks/micropython.py
Normal file
5
software/tests/mocks/micropython.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
def const(x):
|
||||||
|
return x
|
||||||
5
software/tests/mocks/rp2_sd.py
Normal file
5
software/tests/mocks/rp2_sd.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
class SDCard():
|
||||||
|
pass
|
||||||
1
software/tests/requirements.txt
Normal file
1
software/tests/requirements.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
pytest
|
||||||
3
software/tests/test_dummy.py
Normal file
3
software/tests/test_dummy.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
def test_dummy():
|
||||||
|
# This is just a dummy test to make pytest not fail because no tests are defined
|
||||||
|
pass
|
||||||
328
software/tests/test_playerapp.py
Normal file
328
software/tests/test_playerapp.py
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
import app
|
||||||
|
import builtins
|
||||||
|
import pytest # type: ignore
|
||||||
|
import time
|
||||||
|
import utils
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def micropythonify():
|
||||||
|
def time_ticks_ms():
|
||||||
|
return time.time_ns() // 1000000
|
||||||
|
time.ticks_ms = time_ticks_ms
|
||||||
|
yield
|
||||||
|
del time.ticks_ms
|
||||||
|
|
||||||
|
|
||||||
|
class FakeFile:
|
||||||
|
def __init__(self, filename, mode):
|
||||||
|
self.filename = filename
|
||||||
|
self.mode = mode
|
||||||
|
self.closed = False
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self.closed = True
|
||||||
|
|
||||||
|
|
||||||
|
class FakeMp3Player:
|
||||||
|
def __init__(self):
|
||||||
|
self.volume: int | None = None
|
||||||
|
self.track: FakeFile | None = None
|
||||||
|
|
||||||
|
def set_volume(self, vol: int):
|
||||||
|
self.volume = vol
|
||||||
|
|
||||||
|
def play(self, track: FakeFile, offset: int):
|
||||||
|
self.track = track
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
self.track = None
|
||||||
|
|
||||||
|
|
||||||
|
class FakeTimerManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.queued = []
|
||||||
|
|
||||||
|
def cancel(self, timer):
|
||||||
|
self.queued = [(elem[0], elem[1], True) if elem[1] == timer else elem for elem in self.queued]
|
||||||
|
|
||||||
|
def schedule(self, when, what):
|
||||||
|
self.queued.append((when, what, False))
|
||||||
|
|
||||||
|
def testing_run_queued(self):
|
||||||
|
queued = self.queued
|
||||||
|
self.queued = []
|
||||||
|
for when, what, canceled in queued:
|
||||||
|
if not canceled:
|
||||||
|
what()
|
||||||
|
|
||||||
|
|
||||||
|
class FakeNfcReader:
|
||||||
|
tag_callback = None
|
||||||
|
|
||||||
|
def __init__(self, tag_callback=None):
|
||||||
|
FakeNfcReader.tag_callback = tag_callback
|
||||||
|
|
||||||
|
|
||||||
|
class FakeButtons:
|
||||||
|
def __init__(self): pass
|
||||||
|
|
||||||
|
|
||||||
|
class FakePlaylistDb:
|
||||||
|
class FakePlaylist:
|
||||||
|
def __init__(self, parent, pos=0):
|
||||||
|
self.parent = parent
|
||||||
|
self.pos = 0
|
||||||
|
|
||||||
|
def getCurrentPath(self):
|
||||||
|
return self.parent.tracklist[self.pos]
|
||||||
|
|
||||||
|
def getNextPath(self):
|
||||||
|
self.pos += 1
|
||||||
|
if self.pos >= len(self.parent.tracklist):
|
||||||
|
return None
|
||||||
|
return self.parent.tracklist[self.pos]
|
||||||
|
|
||||||
|
def getPlaybackOffset(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def __init__(self, tracklist=[b'test/path.mp3']):
|
||||||
|
self.tracklist = tracklist
|
||||||
|
|
||||||
|
def getPlaylistForTag(self, tag: bytes):
|
||||||
|
return self.FakePlaylist(self)
|
||||||
|
|
||||||
|
def getSetting(self, key: bytes | str):
|
||||||
|
if key == 'tagmode':
|
||||||
|
return 'tagremains'
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
class FakeHwconfig:
|
||||||
|
def __init__(self):
|
||||||
|
self.powered = True
|
||||||
|
self.on_battery = False
|
||||||
|
|
||||||
|
def power_off(self):
|
||||||
|
self.powered = False
|
||||||
|
|
||||||
|
def get_on_battery(self):
|
||||||
|
return self.on_battery
|
||||||
|
|
||||||
|
|
||||||
|
class FakeLeds:
|
||||||
|
IDLE = 0
|
||||||
|
PLAYING = 1
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.state = None
|
||||||
|
|
||||||
|
def set_state(self, state):
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
|
||||||
|
class FakeConfig:
|
||||||
|
def __init__(self): pass
|
||||||
|
|
||||||
|
def get_led_count(self):
|
||||||
|
return 1
|
||||||
|
|
||||||
|
def get_idle_timeout(self):
|
||||||
|
return 60
|
||||||
|
|
||||||
|
def get_tag_timeout(self):
|
||||||
|
return 5
|
||||||
|
|
||||||
|
def get_tagmode(self):
|
||||||
|
return 'tagremains'
|
||||||
|
|
||||||
|
|
||||||
|
def fake_open(filename, mode):
|
||||||
|
return FakeFile(filename, mode)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def faketimermanager(monkeypatch):
|
||||||
|
fake_timer_manager = FakeTimerManager()
|
||||||
|
monkeypatch.setattr(utils.timer.TimerManager, '_instance', fake_timer_manager)
|
||||||
|
yield fake_timer_manager
|
||||||
|
|
||||||
|
|
||||||
|
def _makedeps(mp3player=FakeMp3Player, nfcreader=FakeNfcReader, buttons=FakeButtons,
|
||||||
|
playlistdb=FakePlaylistDb, hwconfig=FakeHwconfig, leds=FakeLeds, config=FakeConfig):
|
||||||
|
return app.Dependencies(mp3player=lambda _: mp3player() if callable(mp3player) else mp3player,
|
||||||
|
nfcreader=lambda x: nfcreader(x) if callable(nfcreader) else nfcreader,
|
||||||
|
buttons=lambda _: buttons() if callable(buttons) else buttons,
|
||||||
|
playlistdb=lambda _: playlistdb() if callable(playlistdb) else playlistdb,
|
||||||
|
hwconfig=lambda _: hwconfig() if callable(hwconfig) else hwconfig,
|
||||||
|
leds=lambda _: leds() if callable(leds) else leds,
|
||||||
|
config=lambda _: config() if callable(config) else config)
|
||||||
|
|
||||||
|
|
||||||
|
def test_construct_app(micropythonify, faketimermanager):
|
||||||
|
fake_mp3 = FakeMp3Player()
|
||||||
|
deps = _makedeps(mp3player=fake_mp3)
|
||||||
|
dut = app.PlayerApp(deps)
|
||||||
|
fake_mp3 = dut.player
|
||||||
|
assert fake_mp3.volume is not None
|
||||||
|
|
||||||
|
|
||||||
|
def test_load_playlist_on_tag(micropythonify, faketimermanager, monkeypatch):
|
||||||
|
fake_db = FakePlaylistDb()
|
||||||
|
fake_mp3 = FakeMp3Player()
|
||||||
|
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
|
||||||
|
app.PlayerApp(deps)
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(builtins, 'open', fake_open)
|
||||||
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||||
|
assert fake_mp3.track is not None
|
||||||
|
assert fake_mp3.track.filename == b'test/path.mp3'
|
||||||
|
assert "r" in fake_mp3.track.mode
|
||||||
|
assert "b" in fake_mp3.track.mode
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_seq(micropythonify, faketimermanager, monkeypatch):
|
||||||
|
fake_db = FakePlaylistDb([b'track1.mp3', b'track2.mp3', b'track3.mp3'])
|
||||||
|
fake_mp3 = FakeMp3Player()
|
||||||
|
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
|
||||||
|
dut = app.PlayerApp(deps)
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(builtins, 'open', fake_open)
|
||||||
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||||
|
assert fake_mp3.track is not None
|
||||||
|
assert fake_mp3.track.filename == b'track1.mp3'
|
||||||
|
|
||||||
|
fake_mp3.track = None
|
||||||
|
dut.onPlaybackDone()
|
||||||
|
assert fake_mp3.track is not None
|
||||||
|
assert fake_mp3.track.filename == b'track2.mp3'
|
||||||
|
|
||||||
|
fake_mp3.track = None
|
||||||
|
dut.onPlaybackDone()
|
||||||
|
assert fake_mp3.track is not None
|
||||||
|
assert fake_mp3.track.filename == b'track3.mp3'
|
||||||
|
|
||||||
|
fake_mp3.track = None
|
||||||
|
dut.onPlaybackDone()
|
||||||
|
assert fake_mp3.track is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_unknown_tag(micropythonify, faketimermanager, monkeypatch):
|
||||||
|
class FakeNoPlaylistDb:
|
||||||
|
def getPlaylistForTag(self, tag):
|
||||||
|
return None
|
||||||
|
|
||||||
|
def getSetting(self, key: bytes | str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
fake_db = FakeNoPlaylistDb()
|
||||||
|
fake_mp3 = FakeMp3Player()
|
||||||
|
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
|
||||||
|
app.PlayerApp(deps)
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(builtins, 'open', fake_open)
|
||||||
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||||
|
assert fake_mp3.track is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_tagmode_startstop(micropythonify, faketimermanager, monkeypatch):
|
||||||
|
class FakeStartStopConfig(FakeConfig):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def get_tagmode(self):
|
||||||
|
return 'tagstartstop'
|
||||||
|
|
||||||
|
fake_db = FakePlaylistDb([b'test/path.mp3'])
|
||||||
|
fake_mp3 = FakeMp3Player()
|
||||||
|
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db, config=FakeStartStopConfig)
|
||||||
|
app.PlayerApp(deps)
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(builtins, 'open', fake_open)
|
||||||
|
# Present tag to start playback
|
||||||
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||||
|
assert fake_mp3.track is not None
|
||||||
|
assert fake_mp3.track.filename == b'test/path.mp3'
|
||||||
|
# Removing tag should not stop playback
|
||||||
|
FakeNfcReader.tag_callback.onTagChange(None)
|
||||||
|
faketimermanager.testing_run_queued()
|
||||||
|
assert fake_mp3.track is not None
|
||||||
|
assert fake_mp3.track.filename == b'test/path.mp3'
|
||||||
|
# Presenting tag should stop playback
|
||||||
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||||
|
assert fake_mp3.track is None
|
||||||
|
# Nothing should change here
|
||||||
|
FakeNfcReader.tag_callback.onTagChange(None)
|
||||||
|
faketimermanager.testing_run_queued()
|
||||||
|
assert fake_mp3.track is None
|
||||||
|
# Presenting tag again should start playback again
|
||||||
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||||
|
assert fake_mp3.track is not None
|
||||||
|
assert fake_mp3.track.filename == b'test/path.mp3'
|
||||||
|
|
||||||
|
|
||||||
|
def test_tagmode_remains(micropythonify, faketimermanager, monkeypatch):
|
||||||
|
fake_db = FakePlaylistDb([b'test/path.mp3'])
|
||||||
|
fake_mp3 = FakeMp3Player()
|
||||||
|
deps = _makedeps(mp3player=fake_mp3, playlistdb=fake_db)
|
||||||
|
app.PlayerApp(deps)
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(builtins, 'open', fake_open)
|
||||||
|
# Present tag to start playback
|
||||||
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||||
|
assert fake_mp3.track is not None
|
||||||
|
assert fake_mp3.track.filename == b'test/path.mp3'
|
||||||
|
# Remove tag to stop playback
|
||||||
|
FakeNfcReader.tag_callback.onTagChange(None)
|
||||||
|
faketimermanager.testing_run_queued()
|
||||||
|
assert fake_mp3.track is None
|
||||||
|
# Presenting tag again should start playback again
|
||||||
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||||
|
assert fake_mp3.track is not None
|
||||||
|
assert fake_mp3.track.filename == b'test/path.mp3'
|
||||||
|
|
||||||
|
|
||||||
|
def test_led_state(micropythonify, faketimermanager, monkeypatch):
|
||||||
|
fake_leds = FakeLeds()
|
||||||
|
deps = _makedeps(leds=fake_leds)
|
||||||
|
app.PlayerApp(deps)
|
||||||
|
assert fake_leds.state == FakeLeds.IDLE
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(builtins, 'open', fake_open)
|
||||||
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||||
|
assert fake_leds.state == FakeLeds.PLAYING
|
||||||
|
FakeNfcReader.tag_callback.onTagChange(None)
|
||||||
|
faketimermanager.testing_run_queued()
|
||||||
|
assert fake_leds.state == FakeLeds.IDLE
|
||||||
|
|
||||||
|
|
||||||
|
def test_idle_shutdown_after_start(micropythonify, faketimermanager, monkeypatch):
|
||||||
|
fake_hwconfig = FakeHwconfig()
|
||||||
|
fake_hwconfig.on_battery = True
|
||||||
|
deps = _makedeps(hwconfig=fake_hwconfig)
|
||||||
|
app.PlayerApp(deps)
|
||||||
|
assert fake_hwconfig.powered
|
||||||
|
faketimermanager.testing_run_queued()
|
||||||
|
assert not fake_hwconfig.powered
|
||||||
|
|
||||||
|
|
||||||
|
def test_idle_shutdown_after_playback(micropythonify, faketimermanager, monkeypatch):
|
||||||
|
fake_hwconfig = FakeHwconfig()
|
||||||
|
fake_hwconfig.on_battery = True
|
||||||
|
deps = _makedeps(hwconfig=fake_hwconfig)
|
||||||
|
app.PlayerApp(deps)
|
||||||
|
assert fake_hwconfig.powered
|
||||||
|
with monkeypatch.context() as m:
|
||||||
|
m.setattr(builtins, 'open', fake_open)
|
||||||
|
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
|
||||||
|
faketimermanager.testing_run_queued()
|
||||||
|
assert fake_hwconfig.powered
|
||||||
|
# Stop playback
|
||||||
|
FakeNfcReader.tag_callback.onTagChange(None)
|
||||||
|
faketimermanager.testing_run_queued()
|
||||||
|
# Elapse idle timer
|
||||||
|
faketimermanager.testing_run_queued()
|
||||||
|
assert not fake_hwconfig.powered
|
||||||
0
software/tests/utils_test/__init__.py
Normal file
0
software/tests/utils_test/__init__.py
Normal file
197
software/tests/utils_test/test_btreedb.py
Normal file
197
software/tests/utils_test/test_btreedb.py
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
# Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>
|
||||||
|
|
||||||
|
import btree
|
||||||
|
import pytest
|
||||||
|
import time
|
||||||
|
from utils import BTreeDB
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def micropythonify():
|
||||||
|
def time_ticks_cpu():
|
||||||
|
return time.time_ns()
|
||||||
|
time.ticks_cpu = time_ticks_cpu
|
||||||
|
yield
|
||||||
|
del time.ticks_cpu
|
||||||
|
|
||||||
|
|
||||||
|
class FakeDB:
|
||||||
|
def __init__(self, contents):
|
||||||
|
self.contents = contents
|
||||||
|
self.saved_contents = dict(contents)
|
||||||
|
|
||||||
|
def flush(self):
|
||||||
|
self.saved_contents = dict(self.contents)
|
||||||
|
|
||||||
|
def values(self, start_key=None, end_key=None, flags=None):
|
||||||
|
res = []
|
||||||
|
for key in sorted(self.contents):
|
||||||
|
if start_key is not None and start_key > key:
|
||||||
|
continue
|
||||||
|
if end_key is not None and end_key <= key:
|
||||||
|
break
|
||||||
|
yield self.contents[key]
|
||||||
|
res.append(self.contents[key])
|
||||||
|
|
||||||
|
def keys(self, start_key=None, end_key=None, flags=None):
|
||||||
|
keys = []
|
||||||
|
if flags is not None and flags & btree.DESC != 0:
|
||||||
|
start_key, end_key = end_key, start_key
|
||||||
|
for key in sorted(self.contents):
|
||||||
|
if start_key is not None and start_key > key:
|
||||||
|
continue
|
||||||
|
if end_key is not None and end_key <= key:
|
||||||
|
break
|
||||||
|
keys.append(key)
|
||||||
|
if flags is not None and flags & btree.DESC != 0:
|
||||||
|
keys.reverse()
|
||||||
|
return iter(keys)
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
return self.contents.get(key, default)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.contents[key]
|
||||||
|
|
||||||
|
def __setitem__(self, key, val):
|
||||||
|
self.contents[key] = val
|
||||||
|
|
||||||
|
def __delitem__(self, key):
|
||||||
|
del self.contents[key]
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
return key in self.contents
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_load():
|
||||||
|
contents = {b'foo/part': b'no',
|
||||||
|
b'foo/playlist/00000': b'track1',
|
||||||
|
b'foo/playlist/00001': b'track2',
|
||||||
|
b'foo/playlisttt': b'no'
|
||||||
|
}
|
||||||
|
uut = BTreeDB(FakeDB(contents))
|
||||||
|
pl = uut.getPlaylistForTag(b'foo')
|
||||||
|
assert list(pl.getPaths()) == [b'track1', b'track2']
|
||||||
|
assert pl.getCurrentPath() == b'track1'
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_nextpath():
|
||||||
|
contents = FakeDB({b'foo/part': b'no',
|
||||||
|
b'foo/playlist/00000': b'track1',
|
||||||
|
b'foo/playlist/00001': b'track2',
|
||||||
|
b'foo/playlisttt': b'no'
|
||||||
|
})
|
||||||
|
uut = BTreeDB(contents)
|
||||||
|
pl = uut.getPlaylistForTag(b'foo')
|
||||||
|
assert pl.getNextPath() == b'track2'
|
||||||
|
assert contents.saved_contents[b'foo/playlistpos'] == b'1'
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_nextpath_last():
|
||||||
|
contents = FakeDB({b'foo/playlist/00000': b'track1',
|
||||||
|
b'foo/playlist/00001': b'track2',
|
||||||
|
b'foo/playlistpos': b'1'
|
||||||
|
})
|
||||||
|
uut = BTreeDB(contents)
|
||||||
|
pl = uut.getPlaylistForTag(b'foo')
|
||||||
|
assert pl.getNextPath() is None
|
||||||
|
assert contents.saved_contents[b'foo/playlistpos'] == b'0'
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_create():
|
||||||
|
contents = FakeDB({b'foo/playlist/00000': b'track1',
|
||||||
|
b'foo/playlist/00001': b'track2',
|
||||||
|
b'foo/playlistpos': b'1'
|
||||||
|
})
|
||||||
|
newplaylist = [b'never gonna give you up.mp3', b'durch den monsun.mp3']
|
||||||
|
uut = BTreeDB(contents)
|
||||||
|
new_pl = uut.createPlaylistForTag(b'foo', newplaylist)
|
||||||
|
assert list(new_pl.getPaths()) == newplaylist
|
||||||
|
assert new_pl.getCurrentPath() == newplaylist[0]
|
||||||
|
assert uut.validate(True)
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_load_notexist():
|
||||||
|
contents = FakeDB({b'foo/playlist/00000': b'track1',
|
||||||
|
b'foo/playlist/00001': b'track2',
|
||||||
|
b'foo/playlistpos': b'1'
|
||||||
|
})
|
||||||
|
uut = BTreeDB(contents)
|
||||||
|
assert uut.getPlaylistForTag(b'notfound') is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_starts_at_beginning_in_persist_no_mode():
|
||||||
|
contents = FakeDB({b'foo/playlist/00000': b'track1',
|
||||||
|
b'foo/playlist/00001': b'track2',
|
||||||
|
b'foo/playlistpersist': b'no',
|
||||||
|
})
|
||||||
|
uut = BTreeDB(contents)
|
||||||
|
pl = uut.getPlaylistForTag(b'foo')
|
||||||
|
assert pl.getCurrentPath() == b'track1'
|
||||||
|
assert pl.getNextPath() == b'track2'
|
||||||
|
del pl
|
||||||
|
pl = uut.getPlaylistForTag(b'foo')
|
||||||
|
assert pl.getCurrentPath() == b'track1'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("mode", [b'no', b'track'])
|
||||||
|
def test_playlist_ignores_offset_in_other_modes(mode):
|
||||||
|
contents = FakeDB({b'foo/playlist/00000': b'track1',
|
||||||
|
b'foo/playlist/00001': b'track2',
|
||||||
|
b'foo/playlistpersist': mode,
|
||||||
|
})
|
||||||
|
uut = BTreeDB(contents)
|
||||||
|
pl = uut.getPlaylistForTag(b'foo')
|
||||||
|
pl.setPlaybackOffset(42)
|
||||||
|
del pl
|
||||||
|
pl = uut.getPlaylistForTag(b'foo')
|
||||||
|
assert pl.getPlaybackOffset() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_stores_offset_in_offset_mode():
|
||||||
|
contents = FakeDB({b'foo/playlist/00000': b'track1',
|
||||||
|
b'foo/playlist/00001': b'track2',
|
||||||
|
b'foo/playlistpersist': b'offset',
|
||||||
|
})
|
||||||
|
uut = BTreeDB(contents)
|
||||||
|
pl = uut.getPlaylistForTag(b'foo')
|
||||||
|
pl.setPlaybackOffset(42)
|
||||||
|
del pl
|
||||||
|
pl = uut.getPlaylistForTag(b'foo')
|
||||||
|
assert pl.getPlaybackOffset() == 42
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_resets_offset_on_next_track():
|
||||||
|
contents = FakeDB({b'foo/playlist/00000': b'track1',
|
||||||
|
b'foo/playlist/00001': b'track2',
|
||||||
|
b'foo/playlistpersist': b'offset',
|
||||||
|
})
|
||||||
|
uut = BTreeDB(contents)
|
||||||
|
pl = uut.getPlaylistForTag(b'foo')
|
||||||
|
pl.setPlaybackOffset(42)
|
||||||
|
assert pl.getNextPath() == b'track2'
|
||||||
|
del pl
|
||||||
|
pl = uut.getPlaylistForTag(b'foo')
|
||||||
|
assert pl.getCurrentPath() == b'track2'
|
||||||
|
assert pl.getPlaybackOffset() == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_playlist_shuffle():
|
||||||
|
contents_dict = {b'foo/playlistpersist': b'track',
|
||||||
|
b'foo/playlistshuffle': b'yes',
|
||||||
|
}
|
||||||
|
for i in range(256):
|
||||||
|
contents_dict['foo/playlist/{:05}'.format(i).encode()] = 'track{}'.format(i).encode()
|
||||||
|
contents = FakeDB(contents_dict)
|
||||||
|
uut = BTreeDB(contents)
|
||||||
|
pl = uut.getPlaylistForTag(b'foo')
|
||||||
|
shuffled = False
|
||||||
|
last_idx = int(pl.getCurrentPath().removeprefix(b'track'))
|
||||||
|
while (t := pl.getNextPath()) is not None:
|
||||||
|
idx = int(t.removeprefix(b'track'))
|
||||||
|
if idx != last_idx + 1:
|
||||||
|
shuffled = True
|
||||||
|
break
|
||||||
|
# A false negative ratr of 1 in 256! should be good enough for this test
|
||||||
|
assert shuffled
|
||||||
1
software/tools/mklittlefs
Submodule
1
software/tools/mklittlefs
Submodule
Submodule software/tools/mklittlefs added at db0513ade5
72
software/tools/standalone-mp3/CMakeLists.txt
Normal file
72
software/tools/standalone-mp3/CMakeLists.txt
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.13)
|
||||||
|
|
||||||
|
# Workaround for pico-sdk host toolchain issue, see directory for details
|
||||||
|
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/../../lib/micropython/ports/rp2/tools_patch")
|
||||||
|
|
||||||
|
# initialize pico-sdk from submodule
|
||||||
|
# note: this must happen before project()
|
||||||
|
include(../../lib/micropython/lib/pico-sdk/pico_sdk_init.cmake)
|
||||||
|
|
||||||
|
project(standalone_mp3)
|
||||||
|
|
||||||
|
option(ENABLE_WRITE_TEST "Enable write test" OFF)
|
||||||
|
option(ENABLE_READ_TEST "Enable read test" ON)
|
||||||
|
option(ENABLE_PLAY_TEST "Enable mp3 playback test" OFF)
|
||||||
|
option(ENABLE_SD_READ_CRC "Enable crc check when reading from sd card" OFF)
|
||||||
|
option(ENABLE_SD_DEBUG "Enable debug output for sd card driver" OFF)
|
||||||
|
|
||||||
|
# initialize the Raspberry Pi Pico SDK
|
||||||
|
pico_sdk_init()
|
||||||
|
|
||||||
|
set(SD_LIB_DIR "${CMAKE_CURRENT_LIST_DIR}/../../modules/rp2_sd")
|
||||||
|
|
||||||
|
set(SD_LIB_SRCS
|
||||||
|
"${SD_LIB_DIR}/sd.c"
|
||||||
|
"${SD_LIB_DIR}/sd_spi.c"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
add_executable(standalone_mp3
|
||||||
|
main.c
|
||||||
|
i2s.c
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/sd_spi_pio.pio.h
|
||||||
|
${CMAKE_CURRENT_BINARY_DIR}/i2s_max98357.pio.h
|
||||||
|
${SD_LIB_SRCS}
|
||||||
|
)
|
||||||
|
|
||||||
|
if(ENABLE_WRITE_TEST)
|
||||||
|
target_compile_definitions(standalone_mp3 PRIVATE WRITE_TEST)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(ENABLE_READ_TEST)
|
||||||
|
target_compile_definitions(standalone_mp3 PRIVATE READ_TEST)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(ENABLE_PLAY_TEST)
|
||||||
|
target_compile_definitions(standalone_mp3 PRIVATE PLAY_TEST)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(ENABLE_SD_READ_CRC)
|
||||||
|
target_compile_definitions(standalone_mp3 PRIVATE SD_READ_CRC_CHECK)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(ENABLE_SD_DEBUG)
|
||||||
|
target_compile_definitions(standalone_mp3 PRIVATE SD_DEBUG)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
pico_generate_pio_header(standalone_mp3 ${SD_LIB_DIR}/sd_spi_pio.pio)
|
||||||
|
|
||||||
|
pico_generate_pio_header(standalone_mp3 ${CMAKE_CURRENT_LIST_DIR}/i2s_max98357.pio)
|
||||||
|
|
||||||
|
add_subdirectory(../../lib/helix_mp3 helix_mp3)
|
||||||
|
|
||||||
|
target_link_libraries(standalone_mp3 PRIVATE pico_stdlib hardware_dma hardware_spi hardware_sync hardware_pio helix_mp3)
|
||||||
|
target_include_directories(standalone_mp3 PRIVATE ${SD_LIB_DIR})
|
||||||
|
target_compile_options(standalone_mp3 PRIVATE -Og -DSD_DEBUG)
|
||||||
|
|
||||||
|
|
||||||
|
pico_add_extra_outputs(standalone_mp3)
|
||||||
|
pico_enable_stdio_uart(standalone_mp3 1)
|
||||||
|
|
||||||
|
set_property(TARGET standalone_mp3 APPEND_STRING PROPERTY LINK_FLAGS "-Wl,--print-memory-usage")
|
||||||
|
|
||||||
116
software/tools/standalone-mp3/i2s.c
Normal file
116
software/tools/standalone-mp3/i2s.c
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
#include "i2s.h"
|
||||||
|
#include "i2s_max98357.pio.h"
|
||||||
|
|
||||||
|
#include <hardware/dma.h>
|
||||||
|
#include <hardware/sync.h>
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#define audiocore_pio pio1
|
||||||
|
|
||||||
|
#define AUDIO_BUFS 3
|
||||||
|
|
||||||
|
struct i2s_context {
|
||||||
|
unsigned pio_program_offset;
|
||||||
|
int pio_sm;
|
||||||
|
int dma_ch;
|
||||||
|
dma_channel_config dma_config;
|
||||||
|
uint32_t dma_buf[AUDIO_BUFS][I2S_DMA_BUF_SIZE];
|
||||||
|
int cur_playing;
|
||||||
|
bool has_data[AUDIO_BUFS];
|
||||||
|
bool playback_active;
|
||||||
|
};
|
||||||
|
|
||||||
|
static struct i2s_context i2s_context;
|
||||||
|
|
||||||
|
#define OUT_PIN 8
|
||||||
|
#define SIDESET_BASE 6
|
||||||
|
|
||||||
|
static void dma_isr(void)
|
||||||
|
{
|
||||||
|
if (!dma_channel_get_irq1_status(i2s_context.dma_ch))
|
||||||
|
return;
|
||||||
|
dma_channel_acknowledge_irq1(i2s_context.dma_ch);
|
||||||
|
const int next_buf = (i2s_context.cur_playing + 1) % AUDIO_BUFS;
|
||||||
|
if (i2s_context.playback_active && i2s_context.has_data[next_buf]) {
|
||||||
|
i2s_context.cur_playing = next_buf;
|
||||||
|
i2s_context.has_data[next_buf] = false;
|
||||||
|
} else {
|
||||||
|
memset(i2s_context.dma_buf[i2s_context.cur_playing], 0, sizeof(uint32_t) * I2S_DMA_BUF_SIZE);
|
||||||
|
if (i2s_context.playback_active)
|
||||||
|
printf("x");
|
||||||
|
}
|
||||||
|
dma_channel_transfer_from_buffer_now(i2s_context.dma_ch, i2s_context.dma_buf[i2s_context.cur_playing], I2S_DMA_BUF_SIZE);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setup_dma_config(void)
|
||||||
|
{
|
||||||
|
i2s_context.dma_config = dma_channel_get_default_config(i2s_context.dma_ch);
|
||||||
|
channel_config_set_dreq(&i2s_context.dma_config, pio_get_dreq(pio1, i2s_context.pio_sm, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t *i2s_next_buf(void)
|
||||||
|
{
|
||||||
|
const long flags = save_and_disable_interrupts();
|
||||||
|
const int next_buf = (i2s_context.cur_playing + 1) % AUDIO_BUFS;
|
||||||
|
uint32_t *ret = NULL;
|
||||||
|
if (!i2s_context.has_data[next_buf]) {
|
||||||
|
ret = i2s_context.dma_buf[next_buf];
|
||||||
|
} else {
|
||||||
|
const int next_buf_2 = (next_buf + 1) % AUDIO_BUFS;
|
||||||
|
if (!i2s_context.has_data[next_buf_2]) {
|
||||||
|
ret = i2s_context.dma_buf[next_buf_2];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
restore_interrupts(flags);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
void i2s_commit_buf(uint32_t *buf)
|
||||||
|
{
|
||||||
|
const long flags = save_and_disable_interrupts();
|
||||||
|
const int next_buf = (i2s_context.cur_playing + 1) % AUDIO_BUFS;
|
||||||
|
const int next_buf_2 = (next_buf + 1) % AUDIO_BUFS;
|
||||||
|
if (i2s_context.dma_buf[next_buf] == buf) {
|
||||||
|
i2s_context.has_data[next_buf] = true;
|
||||||
|
} else if (i2s_context.dma_buf[next_buf_2] == buf) {
|
||||||
|
i2s_context.has_data[next_buf_2] = true;
|
||||||
|
i2s_context.playback_active = true;
|
||||||
|
} else {
|
||||||
|
assert(false);
|
||||||
|
}
|
||||||
|
restore_interrupts(flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool i2s_init(int samplerate)
|
||||||
|
{
|
||||||
|
memset(i2s_context.dma_buf, 0, sizeof(i2s_context.dma_buf[0][0]) * 2* I2S_DMA_BUF_SIZE);
|
||||||
|
if (!pio_can_add_program(audiocore_pio, (const pio_program_t *)&i2s_max98357_program))
|
||||||
|
return false;
|
||||||
|
i2s_context.pio_sm = pio_claim_unused_sm(audiocore_pio, false);
|
||||||
|
if (i2s_context.pio_sm == -1)
|
||||||
|
return false;
|
||||||
|
i2s_context.pio_program_offset = pio_add_program(audiocore_pio, (const pio_program_t *)&i2s_max98357_program);
|
||||||
|
i2s_max98357_program_init(audiocore_pio, i2s_context.pio_sm, i2s_context.pio_program_offset, OUT_PIN, SIDESET_BASE,
|
||||||
|
samplerate);
|
||||||
|
|
||||||
|
i2s_context.dma_ch = dma_claim_unused_channel(false);
|
||||||
|
if (i2s_context.dma_ch == -1)
|
||||||
|
goto out_dma_claim;
|
||||||
|
i2s_context.playback_active = false;
|
||||||
|
setup_dma_config();
|
||||||
|
irq_add_shared_handler(DMA_IRQ_1, &dma_isr, PICO_SHARED_IRQ_HANDLER_DEFAULT_ORDER_PRIORITY);
|
||||||
|
dma_channel_set_irq1_enabled(i2s_context.dma_ch, true);
|
||||||
|
irq_set_enabled(DMA_IRQ_1, true);
|
||||||
|
dma_channel_configure(i2s_context.dma_ch, &i2s_context.dma_config, &audiocore_pio->txf[i2s_context.pio_sm],
|
||||||
|
i2s_context.dma_buf, I2S_DMA_BUF_SIZE, true);
|
||||||
|
pio_sm_set_enabled(audiocore_pio, i2s_context.pio_sm, true);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
out_dma_claim:
|
||||||
|
pio_remove_program(audiocore_pio, (const pio_program_t *)&i2s_max98357_program, i2s_context.pio_program_offset);
|
||||||
|
pio_sm_unclaim(audiocore_pio, i2s_context.pio_sm);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
11
software/tools/standalone-mp3/i2s.h
Normal file
11
software/tools/standalone-mp3/i2s.h
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define I2S_DMA_BUF_SIZE (1152)
|
||||||
|
|
||||||
|
bool i2s_init(int samplerate);
|
||||||
|
|
||||||
|
uint32_t *i2s_next_buf(void);
|
||||||
|
void i2s_commit_buf(uint32_t *buf);
|
||||||
215
software/tools/standalone-mp3/main.c
Normal file
215
software/tools/standalone-mp3/main.c
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
#include "i2s.h"
|
||||||
|
#include "sd.h"
|
||||||
|
|
||||||
|
#include <math.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include <hardware/clocks.h>
|
||||||
|
#include <hardware/gpio.h>
|
||||||
|
#include <hardware/spi.h>
|
||||||
|
#include <hardware/sync.h>
|
||||||
|
#include <pico/stdio.h>
|
||||||
|
#include <pico/stdlib.h>
|
||||||
|
#include <pico/time.h>
|
||||||
|
|
||||||
|
#include "mp3dec.h"
|
||||||
|
#include "sd_spi.h"
|
||||||
|
|
||||||
|
extern void sd_spi_dbg_clk(const int div, const int frac);
|
||||||
|
|
||||||
|
extern void sd_spi_dbg_loop(void);
|
||||||
|
|
||||||
|
#define MAX_VOLUME 0x8000u
|
||||||
|
|
||||||
|
void __time_critical_func(volume_adjust)(int16_t *restrict buf, size_t samples, uint16_t scalef)
|
||||||
|
{
|
||||||
|
for (size_t pos = 0; pos < samples; ++pos) {
|
||||||
|
buf[pos] = ((int32_t)buf[pos] * scalef) >> 15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int __time_critical_func(play_mp3)(struct sd_context *sd_context)
|
||||||
|
{
|
||||||
|
HMP3Decoder mp3dec = MP3InitDecoder();
|
||||||
|
|
||||||
|
if (!i2s_init(44100)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t mp3buffer[4 * 512];
|
||||||
|
for (int i = 0; i < sizeof(mp3buffer) / 512; ++i) {
|
||||||
|
sd_readblock(sd_context, i, mp3buffer + 512 * i);
|
||||||
|
}
|
||||||
|
size_t next_sector = sizeof(mp3buffer) / 512;
|
||||||
|
|
||||||
|
unsigned char *readptr = mp3buffer;
|
||||||
|
int bytes_left = sizeof(mp3buffer);
|
||||||
|
|
||||||
|
bool first = true;
|
||||||
|
bool pending_read = false;
|
||||||
|
bool synced = false;
|
||||||
|
while (true) {
|
||||||
|
/* Get some input data */
|
||||||
|
if (pending_read && sd_readblock_is_complete(sd_context)) {
|
||||||
|
sd_readblock_complete(sd_context);
|
||||||
|
bytes_left += 512;
|
||||||
|
pending_read = false;
|
||||||
|
}
|
||||||
|
if (!pending_read && (sizeof(mp3buffer) - bytes_left >= 512)) {
|
||||||
|
// If there is not enough space for an mp3 frame, or if there is less than one SD block to the end, move
|
||||||
|
// remaining data to start of buffer
|
||||||
|
if (readptr - mp3buffer >= sizeof(mp3buffer) - 1044 ||
|
||||||
|
readptr - mp3buffer > sizeof(mp3buffer) - 512 - bytes_left) {
|
||||||
|
memmove(mp3buffer, readptr, bytes_left);
|
||||||
|
readptr = mp3buffer;
|
||||||
|
}
|
||||||
|
sd_readblock_start(sd_context, next_sector++, readptr + bytes_left);
|
||||||
|
pending_read = true;
|
||||||
|
}
|
||||||
|
if (bytes_left == 0) {
|
||||||
|
// Can't do anything without input, wait and try again
|
||||||
|
__wfe();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Synchronize MP3 stream if neccessary
|
||||||
|
if (!synced) {
|
||||||
|
const int ofs = MP3FindSyncWord(readptr, bytes_left);
|
||||||
|
if (ofs == -1) {
|
||||||
|
printf("MP3 sync word not found\n");
|
||||||
|
readptr += bytes_left;
|
||||||
|
bytes_left = 0;
|
||||||
|
continue; // try again
|
||||||
|
}
|
||||||
|
readptr += ofs;
|
||||||
|
bytes_left -= ofs;
|
||||||
|
printf("MP3 sync word found after %zu bytes\n", ofs);
|
||||||
|
synced = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get an output buffer
|
||||||
|
uint32_t *const buf = i2s_next_buf();
|
||||||
|
if (!buf) {
|
||||||
|
// No output needed, wait and try again
|
||||||
|
__wfe();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode one frame
|
||||||
|
unsigned char *const old_readptr = readptr;
|
||||||
|
const int old_bytes_left = bytes_left;
|
||||||
|
const int status = MP3Decode(mp3dec, &readptr, &bytes_left, (short *)buf, 0);
|
||||||
|
if (status) {
|
||||||
|
if (status == ERR_MP3_INDATA_UNDERFLOW) {
|
||||||
|
readptr = old_readptr;
|
||||||
|
bytes_left = old_bytes_left;
|
||||||
|
printf("INDATA_UNDERFLOW\n");
|
||||||
|
sd_readblock_complete(sd_context);
|
||||||
|
continue;
|
||||||
|
} else /*if (status== ERR_MP3_MAINDATA_UNDERFLOW)*/ {
|
||||||
|
--bytes_left;
|
||||||
|
++readptr;
|
||||||
|
synced = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
printf("MP3Decode failed: %d\n", status);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
MP3FrameInfo info;
|
||||||
|
MP3GetLastFrameInfo(mp3dec, &info);
|
||||||
|
if (first) {
|
||||||
|
printf("bitrate %d, nChans %d, samprate %d, bitsPerSample %d, outputSamps %d, layer %d, version %d\n",
|
||||||
|
info.bitrate, info.nChans, info.samprate, info.bitsPerSample, info.outputSamps, info.layer,
|
||||||
|
info.version);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
if (info.outputSamps != 2304) {
|
||||||
|
printf("Unexpected number of output samples: %d\n", info.outputSamps);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
volume_adjust((int16_t *)buf, info.outputSamps, MAX_VOLUME >> 4);
|
||||||
|
|
||||||
|
i2s_commit_buf(buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_test(struct sd_context *sd_context)
|
||||||
|
{
|
||||||
|
uint8_t data_buffer[4096];
|
||||||
|
do {
|
||||||
|
for (int i = 0; i < sizeof(data_buffer) / SD_SECTOR_SIZE; ++i) {
|
||||||
|
if (!sd_readblock(sd_context, i, data_buffer + SD_SECTOR_SIZE * i)) {
|
||||||
|
printf("sd_readblock(%d) failed\n", i);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int line = 0; line < 32; ++line) {
|
||||||
|
printf("%04hx ", line * 16);
|
||||||
|
for (int item = 0; item < 16; ++item) {
|
||||||
|
printf("%02hhx%c", data_buffer[line * 16 + item], (item == 15) ? '\n' : ' ');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < SD_SECTOR_SIZE; ++i) {
|
||||||
|
data_buffer[i] ^= 0xff;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sd_writeblock(sd_context, 0, data_buffer)) {
|
||||||
|
printf("sd_writeblock failed\n");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sleep_ms(1000);
|
||||||
|
} while (data_buffer[SD_SECTOR_SIZE - 1] != 0xAA);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void read_test(struct sd_context *sd_context)
|
||||||
|
{
|
||||||
|
uint8_t data_buffer[512];
|
||||||
|
const uint64_t before = time_us_64();
|
||||||
|
for (int block = 0; block < 245760; ++block) {
|
||||||
|
if (!sd_readblock(sd_context, block, data_buffer)) {
|
||||||
|
printf("sd_readblock(%d) failed\n", block);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const uint64_t elapsed = time_us_64() - before;
|
||||||
|
printf("%llu ms elapsed, %f kB/s\n", elapsed / 1000LLU, 128 * 1024.f / (elapsed / 1000000.f));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
stdio_init_all();
|
||||||
|
printf("sysclk is %d Hz\n", clock_get_hz(clk_sys));
|
||||||
|
|
||||||
|
struct sd_context sd_context;
|
||||||
|
|
||||||
|
#define DRIVE_STRENGTH GPIO_DRIVE_STRENGTH_8MA
|
||||||
|
#define SLEW_RATE GPIO_SLEW_RATE_SLOW
|
||||||
|
|
||||||
|
gpio_set_drive_strength(2, DRIVE_STRENGTH);
|
||||||
|
gpio_set_slew_rate(2, SLEW_RATE);
|
||||||
|
gpio_set_drive_strength(3, DRIVE_STRENGTH);
|
||||||
|
gpio_set_slew_rate(3, SLEW_RATE);
|
||||||
|
|
||||||
|
if (!sd_init(&sd_context, 3, 4, 2, 5, 25000000)) {
|
||||||
|
printf("sd_init failed\n");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef READ_TEST
|
||||||
|
read_test(&sd_context);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef WRITE_TEST
|
||||||
|
write_test(&sd_context);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef PLAY_TEST
|
||||||
|
play_mp3(&sd_context);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
printf("Done.\n");
|
||||||
|
}
|
||||||
@@ -3,5 +3,8 @@
|
|||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
git submodule update --init lib
|
git submodule update --init lib
|
||||||
git -C lib/micropython submodule update --init lib/pico-sdk lib/mbedtls lib/micropython-lib lib/tinyusb lib/btstack lib/cyw43-driver lib/lwip
|
git -C lib/micropython submodule update --init \
|
||||||
|
lib/pico-sdk lib/mbedtls lib/micropython-lib lib/tinyusb lib/btstack lib/cyw43-driver lib/lwip \
|
||||||
|
lib/berkeley-db-1.xx
|
||||||
git -C lib/micropython/lib/pico-sdk submodule update --init lib
|
git -C lib/micropython/lib/pico-sdk submodule update --init lib
|
||||||
|
git submodule update --init --recursive tools/mklittlefs
|
||||||
|
|||||||
Reference in New Issue
Block a user