13 Commits

Author SHA1 Message Date
419d85209e hw: Final fixes for Rev 1.2
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m23s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Run pytests / Check-Pytest (push) Successful in 11s
- Move RC522 connector a bit further away from the MAX98375 module
  footprint, so a JST-XH connector can be used here too.
- Update custom rules and fix some DRC warnings (no netlist and layout
  changes).

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-11-10 21:45:57 +01:00
0f4f72253c hw: Update metadata, move silkscreen board name + rev
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m27s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 11s
No netlist or routing changes.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-11-10 19:19:28 +01:00
08fdb75297 hw: Add GND point for MAX98357 gain config
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m20s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Run pytests / Check-Pytest (push) Successful in 11s
Add a through-hole pad connected to GND next to the MAX98357 gain config
resistor to allow the user to select from all possible gains.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-11-08 13:47:10 +01:00
d28f0b1c0c hw: Smaller 'production' version of board w/o keys and direct RC522 mount
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m22s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 6s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 11s
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-11-07 22:52:34 +01:00
9147bab5bb hw: Add fifth button, make traces wider
Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-11-07 21:02:55 +01:00
0820ec1fc8 Merge pull request '28-configurable-tag-handling' (#48) from 28-configurable-tag-handling into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m21s
Check code formatting / Check-C-Format (push) Successful in 8s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 11s
Reviewed-on: #48
Reviewed-by: Stefan Kratochwil <kratochwil-la@gmx.de>
2025-11-04 18:54:54 +00:00
7d3cdbabe4 feat(app): Implement tag handling modes according to #28.
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m24s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 6s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Run pytests / Check-Pytest (push) Successful in 11s
If the tagmode setting is 'tagremains' (the default): Play as long as the
tag is on the reader, with a 5 second timeout (i.e. playback stops when
tag is gone for more than 5 seconds).

If the tagmode setting is 'tagstartstop': Playback starts when a tag is
seen. Presenting a different tag causes the playback to stop and
playback for the new tag to start. Re-presenting the same tag used to
start with a no-tag-time of 5 seconds or more in between stops playback.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-11-04 19:25:10 +01:00
b5e3df1054 refactor(app): Refactor tag handling
Split the tag handling in PlayerApp into two parts: The low level
handling of changed/removed tags from the Nfc reader, including the
timeout processing is moved into the nested TagStateMachine class. The
main PlayerApp has two new (internal) callbacks "onNewTag" and
"onTagRemoved" which are called when a new tag is presented or a tag was
actually removed. This will hopefully make the implementation of #28
"Configurable Tag Handling" cleaner.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-11-04 19:25:10 +01:00
55718aa1ff Merge pull request '24-playlist-modes' (#46) from 24-playlist-modes into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m23s
Check code formatting / Check-C-Format (push) Successful in 8s
Check code formatting / Check-Python-Flake8 (push) Successful in 11s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Run pytests / Check-Pytest (push) Successful in 11s
Reviewed-on: #46
Reviewed-by: Stefan Kratochwil <kratochwil-la@gmx.de>
2025-11-04 18:23:57 +00:00
981c75020f Merge pull request 'fix(main): Make upgrade from tag directories more ergonomical' (#47) from fix-no-db into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m20s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 11s
Reviewed-on: #47
Reviewed-by: Stefan Kratochwil <kratochwil-la@gmx.de>
2025-11-02 17:31:57 +00:00
b56e4f36b4 fix(main): Make upgrade from tag directories more ergonomical
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m21s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 4s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 11s
As we don't have an API to upload files and create playlists yet, we
used the convention "tag uid as directory name" to allow testing
playback. With the introduction of the database in #39 "Add playlist db"
a builddb() function to initialize the database from the tag directories
was added, but it is not automatically called. To make the developer
experience more ergonomical, add a check for that automatically runs
builddb() when no database exists.

Signed-off-by: Matthias Blankertz <matthias@blankertz.org>
2025-11-02 18:06:18 +01:00
9ba6e698b9 Merge pull request 'networking' (#33) from networking into main
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m19s
Check code formatting / Check-C-Format (push) Successful in 7s
Check code formatting / Check-Python-Flake8 (push) Successful in 9s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 6s
Run unit tests on host / Run-Unit-Tests (push) Successful in 9s
Run pytests / Check-Pytest (push) Successful in 11s
Reviewed-on: #33
Reviewed-by: Matthias Blankertz <matthias@blankertz.org>
2025-11-02 16:42:29 +00:00
db136ed79a Wifi setup function with initial wlan config and per-board unique ssid
All checks were successful
Build RPi Pico firmware image / Build-Firmware (push) Successful in 3m21s
Check code formatting / Check-C-Format (push) Successful in 9s
Check code formatting / Check-Python-Flake8 (push) Successful in 10s
Check code formatting / Check-Bash-Shellcheck (push) Successful in 5s
Run unit tests on host / Run-Unit-Tests (push) Successful in 8s
Run pytests / Check-Pytest (push) Successful in 11s
2025-11-02 17:21:48 +01:00
9 changed files with 7090 additions and 6986 deletions

View File

@@ -5,18 +5,18 @@
(condition "A.Type == 'pad' && (B.Type == 'text' || B.Type == 'graphic')"))
(rule "drill hole size (mechanical)"
(constraint hole_size (min 0.3mm) (max 6.3mm)))
(constraint hole_size (min 0.2mm) (max 6.3mm)))
(rule "Minimum Via Hole Size"
(constraint hole_size (min 0.3mm))
(constraint hole_size (min 0.2mm))
(condition "A.Type == 'via'"))
(rule "Minimum Via Diameter"
(constraint via_diameter (min 20mil))
(constraint via_diameter (min 0.35mm))
(condition "A.Type == 'via'"))
(rule "PTH Hole Size"
(constraint hole_size (min 12mil) (max 6.35mm))
(constraint hole_size (min 0.2mm) (max 6.35mm))
(condition "A.isPlated()"))
(rule "Minimum Non-plated Hole Size"
@@ -24,5 +24,5 @@
(condition "A.Type == 'pad' && !A.isPlated()"))
(rule "Pad to Track clearance"
(constraint clearance (min 0.2mm))
(condition "A.isPlated() && A.Type != 'Via' && B.Type == 'track'"))
(constraint clearance (min 0.1mm))
(condition "A.isPlated() && A.Type != 'Via' && B.Type == 'track'"))

File diff suppressed because it is too large Load Diff

View File

@@ -18,17 +18,17 @@
"zones": 0.6
},
"selection_filter": {
"dimensions": true,
"footprints": true,
"dimensions": false,
"footprints": false,
"graphics": true,
"keepouts": true,
"keepouts": false,
"lockedItems": false,
"otherItems": true,
"pads": true,
"otherItems": false,
"pads": false,
"text": true,
"tracks": true,
"vias": true,
"zones": true
"zones": false
},
"visible_items": [
"vias",
@@ -51,7 +51,7 @@
"conflict_shadows",
"shapes"
],
"visible_layers": "00000000_00000000_0fffffff_ffffffff",
"visible_layers": "00000000_00000000_0ffffff7_ffffffff",
"zone_display_mode": 0
},
"git": {

View File

@@ -37,9 +37,9 @@
"other_text_thickness": 0.15,
"other_text_upright": false,
"pads": {
"drill": 3.2,
"height": 3.2,
"width": 3.2
"drill": 0.8,
"height": 1.6,
"width": 1.6
},
"silk_line_width": 0.1,
"silk_text_italic": false,
@@ -60,35 +60,11 @@
],
"drc_exclusions": [
[
"items_not_allowed|117348000|131000000|4c513f4f-0c90-4a64-b2d1-bf0189c761db|00000000-0000-0000-0000-000000000000",
"lib_footprint_mismatch|148587019|59126370|4f17c230-1bc1-45e3-8816-9cf9d13e3a5c|00000000-0000-0000-0000-000000000000",
""
],
[
"items_not_allowed|117348000|131000000|fea7aba8-73c4-4553-8cc8-83472c47a83b|00000000-0000-0000-0000-000000000000",
""
],
[
"items_not_allowed|122420000|136340000|8ffc6a0d-806e-4b99-922f-2baead2bd98b|00000000-0000-0000-0000-000000000000",
""
],
[
"items_not_allowed|148270000|136340000|804b9d74-91fa-4f75-bc54-8f55b801562f|00000000-0000-0000-0000-000000000000",
""
],
[
"items_not_allowed|148720706|135916802|f2c1e20e-a272-42d8-a551-3e9074b96921|00000000-0000-0000-0000-000000000000",
""
],
[
"nonmirrored_text_on_back_layer|210811000|131073000|e87ca627-5327-4f7e-ac1a-c1eadd662c0a|00000000-0000-0000-0000-000000000000",
""
],
[
"silk_edge_clearance|115549999|139536698|483993e4-aad9-46a1-bcd0-7f427e76aa64|cd433b4c-40ac-48cc-b03e-5acf3357d88b",
""
],
[
"silk_overlap|115550000|131000000|cd433b4c-40ac-48cc-b03e-5acf3357d88b|fea7aba8-73c4-4553-8cc8-83472c47a83b",
"nonmirrored_text_on_back_layer|213233000|81790000|e87ca627-5327-4f7e-ac1a-c1eadd662c0a|00000000-0000-0000-0000-000000000000",
""
]
],
@@ -157,22 +133,22 @@
},
"rules": {
"max_error": 0.005,
"min_clearance": 0.2032,
"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.45,
"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.3,
"min_track_width": 0.2032,
"min_through_hole_diameter": 0.2,
"min_track_width": 0.15,
"min_via_annular_width": 0.15,
"min_via_diameter": 0.5,
"min_via_diameter": 0.35,
"solder_mask_to_copper_clearance": 0.005,
"use_height_for_length_calcs": true
},

View File

@@ -4,7 +4,127 @@
(generator_version "9.0")
(uuid "7226ca0f-138a-46b4-89f3-dc32dfb1f9f7")
(paper "A4")
(title_block
(title "Tonberry Pico")
(date "2025-11-08")
(rev "1.2")
(comment 1 "Copyright (c) 2025 Matthias Blankertz <matthias@blankertz.org>")
(comment 2 "SPDX-License-Identifier: MIT")
)
(lib_symbols
(symbol "Connector_Generic:Conn_01x01"
(pin_names
(offset 1.016)
(hide yes)
)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(property "Reference" "J"
(at 0 2.54 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" "Conn_01x01"
(at 0 -2.54 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Footprint" ""
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Datasheet" "~"
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Description" "Generic connector, single row, 01x01, script generated (kicad-library-utils/schlib/autogen/connector/)"
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "ki_keywords" "connector"
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "ki_fp_filters" "Connector*:*_1x??_*"
(at 0 0 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(symbol "Conn_01x01_1_1"
(rectangle
(start -1.27 1.27)
(end 1.27 -1.27)
(stroke
(width 0.254)
(type default)
)
(fill
(type background)
)
)
(rectangle
(start -1.27 0.127)
(end 0 -0.127)
(stroke
(width 0.1524)
(type default)
)
(fill
(type none)
)
)
(pin passive line
(at -5.08 0 0)
(length 3.81)
(name "Pin_1"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "1"
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)
(symbol "Connector_Generic:Conn_01x03"
(pin_names
(offset 1.016)
@@ -176,7 +296,7 @@
)
(embedded_fonts no)
)
(symbol "Connector_Generic:Conn_01x05"
(symbol "Connector_Generic:Conn_01x06"
(pin_names
(offset 1.016)
(hide yes)
@@ -192,8 +312,8 @@
)
)
)
(property "Value" "Conn_01x05"
(at 0 -7.62 0)
(property "Value" "Conn_01x06"
(at 0 -10.16 0)
(effects
(font
(size 1.27 1.27)
@@ -218,7 +338,7 @@
(hide yes)
)
)
(property "Description" "Generic connector, single row, 01x05, script generated (kicad-library-utils/schlib/autogen/connector/)"
(property "Description" "Generic connector, single row, 01x06, script generated (kicad-library-utils/schlib/autogen/connector/)"
(at 0 0 0)
(effects
(font
@@ -245,10 +365,10 @@
(hide yes)
)
)
(symbol "Conn_01x05_1_1"
(symbol "Conn_01x06_1_1"
(rectangle
(start -1.27 6.35)
(end 1.27 -6.35)
(end 1.27 -8.89)
(stroke
(width 0.254)
(type default)
@@ -312,6 +432,17 @@
(type none)
)
)
(rectangle
(start -1.27 -7.493)
(end 0 -7.747)
(stroke
(width 0.1524)
(type default)
)
(fill
(type none)
)
)
(pin passive line
(at -5.08 5.08 0)
(length 3.81)
@@ -402,6 +533,24 @@
)
)
)
(pin passive line
(at -5.08 -7.62 0)
(length 3.81)
(name "Pin_6"
(effects
(font
(size 1.27 1.27)
)
)
)
(number "6"
(effects
(font
(size 1.27 1.27)
)
)
)
)
)
(embedded_fonts no)
)
@@ -3809,7 +3958,7 @@
)
(rectangle
(start 242.57 60.96)
(end 283.21 154.94)
(end 283.21 163.83)
(stroke
(width 0)
(type default)
@@ -3859,16 +4008,6 @@
)
(uuid "466265fb-ac3b-4fe8-8af4-502fa85087f9")
)
(text "Pin changes for revision 1 (compared to breadboard):\n- RC522_RST to GPIO14 instead of GPIO9\n- RC522_IRQ to GPIO15 instead of GPIO14\n- I2S_SD to GPIO9 instead of NC\n- I2S_LRCLK to GPIO6 instead of GPIO7\n- I2S_DCLK to GPIO7 instead of GPIO6"
(exclude_from_sim no)
(at 256.032 174.244 0)
(effects
(font
(size 1.27 1.27)
)
)
(uuid "6843afc4-f352-41ed-b1f0-21b2625ea765")
)
(text "Sparkfun board has pullup\nfor mono mode by default"
(exclude_from_sim no)
(at 70.104 69.85 0)
@@ -3899,6 +4038,16 @@
)
(uuid "dcabf837-4155-4957-820b-e0a63b5947c8")
)
(text "Place GND through-hole pad in line with +5V side of R2\nto allow user to select different gain for MAX98357A"
(exclude_from_sim no)
(at 40.894 32.766 0)
(effects
(font
(size 1.27 1.27)
)
)
(uuid "dcca3a95-1162-4543-8f05-573beea44e38")
)
(text "Battery power"
(exclude_from_sim no)
(at 21.59 111.76 0)
@@ -3927,6 +4076,12 @@
(color 0 0 0 0)
(uuid "7b8323c0-6656-4c4e-b086-636ff2b9c6b7")
)
(junction
(at 276.86 148.59)
(diameter 0)
(color 0 0 0 0)
(uuid "9a1919a6-5883-47bb-82e5-3e3ecca52cb1")
)
(junction
(at 55.88 45.72)
(diameter 0)
@@ -4005,10 +4160,6 @@
(at 132.08 41.91)
(uuid "327eca18-19ed-4df2-b4a4-b6473b4637be")
)
(no_connect
(at 177.8 59.69)
(uuid "476000a3-9e7f-4cbf-9a8d-0e319caba0cd")
)
(no_connect
(at 177.8 69.85)
(uuid "50c74a0a-6abe-49d5-b374-0fde9fb9680b")
@@ -4289,6 +4440,16 @@
)
(uuid "31e9b0f8-7cda-4a52-89a1-0e733bdf35fb")
)
(wire
(pts
(xy 276.86 148.59) (xy 276.86 151.13)
)
(stroke
(width 0)
(type default)
)
(uuid "382ff189-be47-4469-ba80-056083381655")
)
(wire
(pts
(xy 254 138.43) (xy 265.43 138.43)
@@ -4389,6 +4550,16 @@
)
(uuid "48ccc495-741c-4b2b-9585-fde090268ab2")
)
(wire
(pts
(xy 275.59 148.59) (xy 276.86 148.59)
)
(stroke
(width 0)
(type default)
)
(uuid "4c57d923-2d70-4cfe-aa54-f3da164b16e8")
)
(wire
(pts
(xy 276.86 128.27) (xy 276.86 138.43)
@@ -4409,6 +4580,16 @@
)
(uuid "4f930ab5-0db4-4b67-a7e7-7fda8dc25972")
)
(wire
(pts
(xy 254 87.63) (xy 266.7 87.63)
)
(stroke
(width 0)
(type default)
)
(uuid "50622824-c775-4a38-abd2-79f7254111d1")
)
(wire
(pts
(xy 85.09 170.18) (xy 72.39 170.18)
@@ -4421,7 +4602,7 @@
)
(wire
(pts
(xy 276.86 138.43) (xy 276.86 139.7)
(xy 276.86 138.43) (xy 276.86 148.59)
)
(stroke
(width 0)
@@ -4599,6 +4780,16 @@
)
(uuid "79bb88bf-f554-4e83-bbcb-698a1df2a819")
)
(wire
(pts
(xy 36.83 46.99) (xy 36.83 49.53)
)
(stroke
(width 0)
(type default)
)
(uuid "7ac8879d-9fc7-40b2-b550-c83532147c07")
)
(wire
(pts
(xy 55.88 45.72) (xy 55.88 44.45)
@@ -4809,6 +5000,16 @@
)
(uuid "9c334529-8798-4c90-875a-da73ae41ede1")
)
(wire
(pts
(xy 177.8 59.69) (xy 182.88 59.69)
)
(stroke
(width 0)
(type default)
)
(uuid "9d9ed882-a31b-445f-833c-142b7ba4e54b")
)
(wire
(pts
(xy 49.53 162.56) (xy 49.53 190.5)
@@ -5169,6 +5370,16 @@
)
(uuid "ccf59af6-59fd-48d7-a33f-49c9aeb1ece9")
)
(wire
(pts
(xy 254 148.59) (xy 265.43 148.59)
)
(stroke
(width 0)
(type default)
)
(uuid "cdc6967b-fa8e-4d0e-8256-ef9790895e47")
)
(wire
(pts
(xy 52.07 55.88) (xy 52.07 69.85)
@@ -5713,6 +5924,28 @@
)
)
)
(global_label "BUTTON4"
(shape output)
(at 254 87.63 180)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
)
(justify right)
)
(uuid "21ff2bc3-e911-4f23-a993-f08a650905a9")
(property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 242.6086 87.63 0)
(effects
(font
(size 1.27 1.27)
)
(justify right)
(hide yes)
)
)
)
(global_label "BUTTON1"
(shape output)
(at 254 80.01 180)
@@ -5735,6 +5968,28 @@
)
)
)
(global_label "BUTTON4"
(shape input)
(at 182.88 59.69 0)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
)
(justify left)
)
(uuid "2736ef81-384c-4324-a05c-697c55fa029e")
(property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 194.2714 59.69 0)
(effects
(font
(size 1.27 1.27)
)
(justify left)
(hide yes)
)
)
)
(global_label "BUTTON2"
(shape output)
(at 254 128.27 180)
@@ -5779,6 +6034,28 @@
)
)
)
(global_label "BUTTON4"
(shape output)
(at 254 148.59 180)
(fields_autoplaced yes)
(effects
(font
(size 1.27 1.27)
)
(justify right)
)
(uuid "47d67395-d5c6-4e13-8fd7-2f3ddfb71aa2")
(property "Intersheetrefs" "${INTERSHEET_REFS}"
(at 242.6086 148.59 0)
(effects
(font
(size 1.27 1.27)
)
(justify right)
(hide yes)
)
)
)
(global_label "POWER_BUTTON"
(shape input)
(at 83.82 157.48 0)
@@ -6115,7 +6392,7 @@
(justify left)
)
)
(property "Footprint" "Connector_PinSocket_2.54mm:PinSocket_1x03_P2.54mm_Vertical"
(property "Footprint" "Connector_JST:JST_XH_B3B-XH-A_1x03_P2.50mm_Vertical"
(at 207.01 49.53 0)
(effects
(font
@@ -6160,6 +6437,72 @@
)
)
)
(symbol
(lib_id "power:GND")
(at 36.83 49.53 0)
(unit 1)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(dnp no)
(uuid "0e3366a9-cc30-4574-9a85-1f934576d050")
(property "Reference" "#PWR018"
(at 36.83 55.88 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Value" "GND"
(at 38.862 53.34 0)
(effects
(font
(size 1.27 1.27)
)
(justify right)
)
)
(property "Footprint" ""
(at 36.83 49.53 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Datasheet" ""
(at 36.83 49.53 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Description" "Power symbol creates a global label with name \"GND\" , ground"
(at 36.83 49.53 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(pin "1"
(uuid "4411b33e-7a34-433c-a726-528c62666fb1")
)
(instances
(project "tonberry-pico"
(path "/7226ca0f-138a-46b4-89f3-dc32dfb1f9f7"
(reference "#PWR018")
(unit 1)
)
)
)
)
(symbol
(lib_id "Jumper:Jumper_2_Bridged")
(at 93.98 69.85 0)
@@ -6814,7 +7157,7 @@
(justify left)
)
)
(property "Footprint" "Package_TO_SOT_THT:TO-92_Inline"
(property "Footprint" "Package_TO_SOT_THT:TO-92_Inline_Wide"
(at 58.42 172.085 0)
(effects
(font
@@ -6935,8 +7278,8 @@
(unit 1)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(dnp no)
(on_board no)
(dnp yes)
(fields_autoplaced yes)
(uuid "6107dfa8-3a58-4296-84b3-7c3671cb0432")
(property "Reference" "SW3"
@@ -7003,8 +7346,8 @@
(unit 1)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(dnp no)
(on_board no)
(dnp yes)
(uuid "623caba9-5088-4f71-b179-4289ab57e20f")
(property "Reference" "SW4"
(at 270.51 96.52 0)
@@ -7090,7 +7433,7 @@
)
)
)
(property "Footprint" "Connector_PinHeader_2.54mm:PinHeader_1x02_P2.54mm_Vertical"
(property "Footprint" "PCM_JLCPCB:SW_TS-1088-AR02016"
(at 266.7 20.32 0)
(effects
(font
@@ -7586,8 +7929,8 @@
(unit 1)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(dnp no)
(on_board no)
(dnp yes)
(uuid "79f78486-f390-4ddb-b9f4-4a3a2c11d855")
(property "Reference" "SW1"
(at 270.51 110.49 0)
@@ -7653,8 +7996,8 @@
(unit 1)
(exclude_from_sim no)
(in_bom yes)
(on_board yes)
(dnp no)
(on_board no)
(dnp yes)
(fields_autoplaced yes)
(uuid "7cf33776-e3ad-49e6-8da4-da9b4c4012c2")
(property "Reference" "SW2"
@@ -7787,7 +8130,7 @@
)
(symbol
(lib_id "power:GND")
(at 276.86 139.7 0)
(at 276.86 151.13 0)
(unit 1)
(exclude_from_sim no)
(in_bom yes)
@@ -7796,7 +8139,7 @@
(fields_autoplaced yes)
(uuid "7f17a4cc-3063-4ad7-9820-0d414285324e")
(property "Reference" "#PWR013"
(at 276.86 146.05 0)
(at 276.86 157.48 0)
(effects
(font
(size 1.27 1.27)
@@ -7805,7 +8148,7 @@
)
)
(property "Value" "GND"
(at 276.86 144.78 0)
(at 276.86 156.21 0)
(effects
(font
(size 1.27 1.27)
@@ -7813,7 +8156,7 @@
)
)
(property "Footprint" ""
(at 276.86 139.7 0)
(at 276.86 151.13 0)
(effects
(font
(size 1.27 1.27)
@@ -7822,7 +8165,7 @@
)
)
(property "Datasheet" ""
(at 276.86 139.7 0)
(at 276.86 151.13 0)
(effects
(font
(size 1.27 1.27)
@@ -7831,7 +8174,7 @@
)
)
(property "Description" "Power symbol creates a global label with name \"GND\" , ground"
(at 276.86 139.7 0)
(at 276.86 151.13 0)
(effects
(font
(size 1.27 1.27)
@@ -8009,7 +8352,7 @@
)
)
)
(property "Footprint" "Modules:AZ_Delivery_RC522"
(property "Footprint" "Connector_PinSocket_2.54mm:PinSocket_1x08_P2.54mm_Vertical"
(at 74.93 90.17 0)
(effects
(font
@@ -8164,7 +8507,7 @@
(justify left)
)
)
(property "Footprint" "Package_TO_SOT_THT:TO-92_Inline"
(property "Footprint" "Package_TO_SOT_THT:TO-92_Inline_Wide"
(at 46.99 159.385 0)
(effects
(font
@@ -8720,6 +9063,73 @@
)
)
)
(symbol
(lib_id "Connector_Generic:Conn_01x01")
(at 36.83 41.91 90)
(unit 1)
(exclude_from_sim no)
(in_bom no)
(on_board yes)
(dnp no)
(fields_autoplaced yes)
(uuid "d3bcb0bc-df35-41fa-87ea-2781bf254e68")
(property "Reference" "J8"
(at 39.37 40.6399 90)
(effects
(font
(size 1.27 1.27)
)
(justify right)
)
)
(property "Value" "Conn_01x01"
(at 39.37 43.1799 90)
(effects
(font
(size 1.27 1.27)
)
(justify right)
)
)
(property "Footprint" "Connector_Wire:SolderWire-0.25sqmm_1x01_D0.65mm_OD1.7mm"
(at 36.83 41.91 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Datasheet" "~"
(at 36.83 41.91 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Description" "Generic connector, single row, 01x01, script generated (kicad-library-utils/schlib/autogen/connector/)"
(at 36.83 41.91 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(pin "1"
(uuid "516d40fb-50f8-4c22-aeaf-d075175736e6")
)
(instances
(project ""
(path "/7226ca0f-138a-46b4-89f3-dc32dfb1f9f7"
(reference "J8")
(unit 1)
)
)
)
)
(symbol
(lib_id "Connector_Generic:Conn_01x07")
(at 73.66 49.53 0)
@@ -9427,6 +9837,74 @@
)
)
)
(symbol
(lib_id "Switch:SW_Push")
(at 270.51 148.59 0)
(unit 1)
(exclude_from_sim no)
(in_bom yes)
(on_board no)
(dnp yes)
(fields_autoplaced yes)
(uuid "fb95655b-d8ba-47f5-91f3-286848a1e70f")
(property "Reference" "SW6"
(at 270.51 140.97 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Value" "SW_Push"
(at 270.51 143.51 0)
(effects
(font
(size 1.27 1.27)
)
)
)
(property "Footprint" "Button_Switch_THT:Push_E-Switch_KS01Q01"
(at 270.51 143.51 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Datasheet" "~"
(at 270.51 143.51 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(property "Description" "Push button switch, generic, two pins"
(at 270.51 148.59 0)
(effects
(font
(size 1.27 1.27)
)
(hide yes)
)
)
(pin "1"
(uuid "6694cff6-47a5-4135-b781-b53e5eb0d46c")
)
(pin "2"
(uuid "aa2c7820-7193-44e1-aac3-be02505e4c11")
)
(instances
(project "tonberry-pico"
(path "/7226ca0f-138a-46b4-89f3-dc32dfb1f9f7"
(reference "SW6")
(unit 1)
)
)
)
)
(symbol
(lib_id "Connector_Generic:Conn_01x03")
(at 115.57 31.75 90)
@@ -9572,7 +10050,7 @@
)
)
(symbol
(lib_id "Connector_Generic:Conn_01x05")
(lib_id "Connector_Generic:Conn_01x06")
(at 271.78 80.01 0)
(unit 1)
(exclude_from_sim no)
@@ -9582,7 +10060,7 @@
(fields_autoplaced yes)
(uuid "fccc1f29-a663-4617-8514-4cf1edd994cd")
(property "Reference" "J7"
(at 274.32 78.7399 0)
(at 274.32 80.0099 0)
(effects
(font
(size 1.27 1.27)
@@ -9591,7 +10069,7 @@
)
)
(property "Value" "Buttons"
(at 274.32 81.2799 0)
(at 274.32 82.5499 0)
(effects
(font
(size 1.27 1.27)
@@ -9599,7 +10077,7 @@
(justify left)
)
)
(property "Footprint" "Connector_PinSocket_2.54mm:PinSocket_1x05_P2.54mm_Vertical"
(property "Footprint" "Connector_JST:JST_XH_B6B-XH-A_1x06_P2.50mm_Vertical"
(at 271.78 80.01 0)
(effects
(font
@@ -9617,7 +10095,7 @@
(hide yes)
)
)
(property "Description" "Generic connector, single row, 01x05, script generated (kicad-library-utils/schlib/autogen/connector/)"
(property "Description" "Generic connector, single row, 01x06, script generated (kicad-library-utils/schlib/autogen/connector/)"
(at 271.78 80.01 0)
(effects
(font
@@ -9641,6 +10119,9 @@
(pin "1"
(uuid "46257db4-53cd-407e-8cb7-103df4c3a13d")
)
(pin "6"
(uuid "cabc7efd-b7fb-49c1-8a3e-beca01096a1a")
)
(instances
(project "tonberry-pico"
(path "/7226ca0f-138a-46b4-89f3-dc32dfb1f9f7"

View File

@@ -13,13 +13,40 @@ VOLUME_CURVE = [1, 2, 4, 8, 16, 32, 63, 126, 251]
class PlayerApp:
class TagStateMachine:
def __init__(self, parent, timer_manager):
self.parent = parent
self.timer_manager = timer_manager
self.current_tag = None
self.current_tag_time = time.ticks_ms()
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() + 5000, 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.current_tag = None
self.current_tag_time = time.ticks_ms()
self.timer_manager = TimerManager()
self.tag_state_machine = self.TagStateMachine(self, self.timer_manager)
self.player = deps.mp3player(self)
self.nfc = deps.nfcreader(self)
self.nfc = deps.nfcreader(self.tag_state_machine)
self.playlist_db = deps.playlistdb(self)
self.tag_mode = self.playlist_db.getSetting('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
@@ -30,28 +57,26 @@ class PlayerApp:
self.mp3file.close()
self.mp3file = None
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
uid_str = b''.join('{:02x}'.format(x).encode() for x in new_tag)
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)
else:
self.timer_manager.schedule(time.ticks_ms() + 5000, self.onTagRemoveDelay)
self.playing_tag = new_tag
elif self.tag_mode == 'tagstartstop':
print('Tag presented again, stopping playback')
self._unset_playlist()
self.playing_tag = None
def onTagRemoveDelay(self):
if self.current_tag is not 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.current_tag = None
if self.playlist is not None:
pos = self.player.stop()
if pos is not None:
self.playlist.setPlaybackOffset(pos)
self._unset_playlist()
def onButtonPressed(self, what):
assert self.buttons is not None
@@ -71,10 +96,18 @@ class PlayerApp:
self._play_next()
def _set_playlist(self, tag: bytes):
self._unset_playlist()
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()
if pos is not None:
self.playlist.setPlaybackOffset(pos)
self.playlist = None
def _play_next(self):
if self.playlist is None:
return
@@ -82,6 +115,7 @@ class PlayerApp:
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:

View File

@@ -3,8 +3,11 @@
import aiorepl # type: ignore
import asyncio
from errno import ENOENT
import machine
import micropython
import network
import os
import time
from math import pi, sin, pow
@@ -54,35 +57,57 @@ async def rainbow(np, period=10):
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)
DB_PATH = '/sd/tonberry.db'
def run():
asyncio.new_event_loop()
# Setup LEDs
np = NeoPixel(hwconfig.LED_DIN, hwconfig.LED_COUNT, sm=1)
asyncio.create_task(rainbow(np))
# 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), \
BTreeFileManager('/sd/tonberry.db') as playlistdb, \
AudioContext(hwconfig.I2S_DIN, hwconfig.I2S_DCLK, hwconfig.I2S_LRCLK) as audioctx:
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()
# 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)
with BTreeFileManager(DB_PATH) as playlistdb, \
AudioContext(hwconfig.I2S_DIN, hwconfig.I2S_DCLK, hwconfig.I2S_LRCLK) as audioctx:
# 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, pin_volup=hwconfig.BUTTON_VOLUP,
pin_voldown=hwconfig.BUTTON_VOLDOWN,
pin_next=hwconfig.BUTTON_NEXT),
playlistdb=lambda _: playlistdb)
the_app = app.PlayerApp(deps)
# 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)
# Start
asyncio.create_task(aiorepl.task({'timer_manager': TimerManager(),
'app': the_app}))
asyncio.get_event_loop().run_forever()
# 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, pin_volup=hwconfig.BUTTON_VOLUP,
pin_voldown=hwconfig.BUTTON_VOLDOWN,
pin_next=hwconfig.BUTTON_NEXT),
playlistdb=lambda _: playlistdb)
the_app = app.PlayerApp(deps)
# Start
asyncio.create_task(aiorepl.task({'timer_manager': TimerManager(),
'app': the_app}))
asyncio.get_event_loop().run_forever()
def builddb():
@@ -90,10 +115,11 @@ 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.
"""
import os
os.unlink('/sd/tonberry.db')
with BTreeFileManager('/sd/tonberry.db') as db:
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

View File

@@ -31,6 +31,9 @@ class BTreeDB(IPlaylistDB):
PERSIST_NO = b'no'
PERSIST_TRACK = b'track'
PERSIST_OFFSET = b'offset'
DEFAULT_SETTINGS = {
b'tagmode': b'tagremains'
}
class Playlist(IPlaylist):
def __init__(self, parent: "BTreeDB", tag: bytes, pos: int, persist, shuffle):
@@ -269,6 +272,11 @@ class BTreeDB(IPlaylistDB):
self._savePlaylist(tag, entries, persist, shuffle)
return self.getPlaylistForTag(tag)
def getSetting(self, key: bytes | str) -> str:
if key is str:
key = key.encode()
return self.db.get(b'settings/' + key, self.DEFAULT_SETTINGS[key]).decode()
def validate(self, dump=False):
"""
Validate the structure of the playlist database.
@@ -286,6 +294,11 @@ class BTreeDB(IPlaylistDB):
fields = k.split(b'/')
if len(fields) <= 1:
fail(f'Malformed key {k!r}')
continue
if fields[0] == b'settings':
val = self.db[k].decode()
print(f'Setting {fields[1].decode()} = {val}')
continue
if last_tag != fields[0]:
last_tag = fields[0]
last_pos = None

View File

@@ -38,14 +38,23 @@ class FakeMp3Player:
def play(self, track: FakeFile, offset: int):
self.track = track
def stop(self):
self.track = None
class FakeTimerManager:
def __init__(self): pass
def cancel(self, timer): pass
def schedule(self, when, what):
what()
class FakeNfcReader:
def __init__(self): pass
tag_callback = None
def __init__(self, tag_callback=None):
FakeNfcReader.tag_callback = tag_callback
class FakeButtons:
@@ -76,6 +85,11 @@ class FakePlaylistDb:
def getPlaylistForTag(self, tag: bytes):
return self.FakePlaylist(self)
def getSetting(self, key: bytes | str):
if key == 'tagmode':
return 'tagremains'
return None
def fake_open(filename, mode):
return FakeFile(filename, mode)
@@ -100,13 +114,13 @@ def test_load_playlist_on_tag(micropythonify, faketimermanager, monkeypatch):
fake_db = FakePlaylistDb()
fake_mp3 = FakeMp3Player()
deps = app.Dependencies(mp3player=lambda _: fake_mp3,
nfcreader=lambda _: FakeNfcReader(),
nfcreader=lambda x: FakeNfcReader(x),
buttons=lambda _: FakeButtons(),
playlistdb=lambda _: fake_db)
dut = app.PlayerApp(deps)
app.PlayerApp(deps)
with monkeypatch.context() as m:
m.setattr(builtins, 'open', fake_open)
dut.onTagChange([23, 42, 1, 2, 3])
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
@@ -117,13 +131,13 @@ def test_playlist_seq(micropythonify, faketimermanager, monkeypatch):
fake_db = FakePlaylistDb([b'track1.mp3', b'track2.mp3', b'track3.mp3'])
fake_mp3 = FakeMp3Player()
deps = app.Dependencies(mp3player=lambda _: fake_mp3,
nfcreader=lambda _: FakeNfcReader(),
nfcreader=lambda x: FakeNfcReader(x),
buttons=lambda _: FakeButtons(),
playlistdb=lambda _: fake_db)
dut = app.PlayerApp(deps)
with monkeypatch.context() as m:
m.setattr(builtins, 'open', fake_open)
dut.onTagChange([23, 42, 1, 2, 3])
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
assert fake_mp3.track is not None
assert fake_mp3.track.filename == b'track1.mp3'
@@ -147,14 +161,88 @@ def test_playlist_unknown_tag(micropythonify, faketimermanager, monkeypatch):
def getPlaylistForTag(self, tag):
return None
def getSetting(self, key: bytes | str):
return None
fake_db = FakeNoPlaylistDb()
fake_mp3 = FakeMp3Player()
deps = app.Dependencies(mp3player=lambda _: fake_mp3,
nfcreader=lambda _: FakeNfcReader(),
nfcreader=lambda x: FakeNfcReader(x),
buttons=lambda _: FakeButtons(),
playlistdb=lambda _: fake_db)
dut = app.PlayerApp(deps)
app.PlayerApp(deps)
with monkeypatch.context() as m:
m.setattr(builtins, 'open', fake_open)
dut.onTagChange([23, 42, 1, 2, 3])
FakeNfcReader.tag_callback.onTagChange([23, 42, 1, 2, 3])
assert fake_mp3.track is None
def test_tagmode_startstop(micropythonify, faketimermanager, monkeypatch):
class MyFakePlaylistDb(FakePlaylistDb):
def __init__(self, tracklist=[b'test/path.mp3']):
super().__init__(tracklist)
def getSetting(self, key: bytes | str):
if key == 'tagmode':
return 'tagstartstop'
return None
fake_db = MyFakePlaylistDb()
fake_mp3 = FakeMp3Player()
deps = app.Dependencies(mp3player=lambda _: fake_mp3,
nfcreader=lambda x: FakeNfcReader(x),
buttons=lambda _: FakeButtons(),
playlistdb=lambda _: 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'
# Removing tag should not stop playback
FakeNfcReader.tag_callback.onTagChange(None)
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)
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):
class MyFakePlaylistDb(FakePlaylistDb):
def __init__(self, tracklist=[b'test/path.mp3']):
super().__init__(tracklist)
def getSetting(self, key: bytes | str):
if key == 'tagmode':
return 'tagremains'
return None
fake_db = MyFakePlaylistDb()
fake_mp3 = FakeMp3Player()
deps = app.Dependencies(mp3player=lambda _: fake_mp3,
nfcreader=lambda x: FakeNfcReader(x),
buttons=lambda _: FakeButtons(),
playlistdb=lambda _: 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)
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'